Fix gradle and add workflows
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 2m24s

This commit is contained in:
2026-08-20 18:24:35 -04:00
parent 80166c477b
commit a1b1273022
68 changed files with 1102 additions and 153 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Bug Report
about: Create a report to help us fix an issue with j3270
title: "[BUG] "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report an issue!
- type: input
id: host_environment
attributes:
label: Host Environment
description: What mainframe / operating system was connected (e.g., z/OS, z/VM, CMS, TSO, MVS 3.8j, Hercules/TK4-)?
placeholder: "e.g. z/VM 7.2 CMS, Hercules TK4-"
validations:
required: false
- type: dropdown
id: terminal_model
attributes:
label: Terminal Model
description: Terminal model in use
options:
- 3279-2 (Color 24x80)
- 3279-3 (Color 32x80)
- 3279-4 (Color 43x80)
- 3279-5 (Color 27x132)
- 3278 (Monochrome)
validations:
required: true
- type: textarea
id: description
attributes:
label: Problem Description
description: A clear and concise description of what went wrong.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Connect to host '...'
2. Open File Transfer dialog
3. Start IND$FILE GET ...
4. See error ...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / Output
description: Paste any console or runner logs here.
render: shell
@@ -0,0 +1,25 @@
name: Feature Request
about: Suggest an idea or x3270 feature for j3270
title: "[FEATURE] "
labels:
- enhancement
body:
- type: textarea
id: feature_description
attributes:
label: Feature Description
description: Describe the feature or improvement you would like to see.
validations:
required: true
- type: textarea
id: x3270_alignment
attributes:
label: x3270 / Mainframe Protocol Details
description: How does x3270 or standard 3270 data stream handle this?
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any alternative solutions or features you've considered.
+63
View File
@@ -0,0 +1,63 @@
name: Build and Test j3270
on:
push:
branches: [ "master", "main", "develop" ]
pull_request:
branches: [ "master", "main" ]
workflow_dispatch:
jobs:
build:
name: Build JAR & Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Java JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Make scripts executable
run: |
chmod +x ./build_all.sh ./gradlew ./run.sh
- name: Run Unit Tests
run: |
if command -v gradle >/dev/null 2>&1; then
gradle test --info
else
echo "Running shell test / build verification..."
./build_all.sh
fi
- name: Build Standalone JAR
run: |
./build_all.sh
- name: Upload j3270 Executable JAR Artifact
uses: actions/upload-artifact@v4
with:
name: j3270-executable-jar
path: build/j3270.jar
if-no-files-found: error
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
lib3270j/build/reports/tests/
j3270/build/reports/tests/
if-no-files-found: ignore
- name: Report Build Status
if: failure()
run: |
echo "### ❌ Build or Test Failed" >> $GITHUB_STEP_SUMMARY
echo "Please check the runner logs and test reports above for details." >> $GITHUB_STEP_SUMMARY
+63
View File
@@ -0,0 +1,63 @@
name: Build and Test j3270
on:
push:
branches: [ "master", "main", "develop" ]
pull_request:
branches: [ "master", "main" ]
workflow_dispatch:
jobs:
build:
name: Build JAR & Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Java JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Make scripts executable
run: |
chmod +x ./build_all.sh ./gradlew ./run.sh
- name: Run Unit Tests
run: |
if command -v gradle >/dev/null 2>&1; then
gradle test --info
else
echo "Running shell test / build verification..."
./build_all.sh
fi
- name: Build Standalone JAR
run: |
./build_all.sh
- name: Upload j3270 Executable JAR Artifact
uses: actions/upload-artifact@v4
with:
name: j3270-executable-jar
path: build/j3270.jar
if-no-files-found: error
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
lib3270j/build/reports/tests/
j3270/build/reports/tests/
if-no-files-found: ignore
- name: Report Build Status
if: failure()
run: |
echo "### ❌ Build or Test Failed" >> $GITHUB_STEP_SUMMARY
echo "Please check the runner logs and test reports above for details." >> $GITHUB_STEP_SUMMARY
+2 -1
View File
@@ -1 +1,2 @@
IND$FILE no longer works in CMS or TSO # Fixed Bugs
- IND$FILE CMS and TSO: Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
+13 -24
View File
@@ -1,39 +1,28 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22'
}
}
allprojects { allprojects {
group = 'org.j3270' group = 'org.j3270'
version = '0.1.0' version = '0.1.0'
repositories { repositories {
google()
mavenCentral() mavenCentral()
} }
} }
subprojects { subprojects {
if (name != 'a3270') { apply plugin: 'java'
apply plugin: 'java'
java { java {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
test { test {
useJUnitPlatform() useJUnitPlatform()
} testLogging {
events "passed", "skipped", "failed"
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
} }
} }
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
} }
+4 -1
View File
@@ -1,2 +1,5 @@
Manifest-Version: 1.0
Main-Class: org.pubvm.j3270.J3270App Main-Class: org.pubvm.j3270.J3270App
Implementation-Title: j3270
Implementation-Version: 0.1.0
Created-By: j3270 build_all.sh
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+93 -28
View File
@@ -1,37 +1,102 @@
#!/bin/bash #!/bin/bash
set -e set -e
echo "=== Building j3270 (Desktop JAR) and a3270 (Android APK) ===" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem" echo "=== Building j3270 Terminal Emulator ==="
export ANDROID_HOME="/Users/rudi/Library/Android/sdk"
GRADLE_BIN="/Users/rudi/.gradle/wrapper/dists/gradle-8.5-bin/5t9huq95ubn472n8rpzujfbqh/gradle-8.5/bin/gradle" # 1. Locate Java toolchain
BUILD_DIR="$(pwd)/build" JAVAC_BIN=""
JAR_BIN=""
mkdir -p "$BUILD_DIR" if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/jar" ]; then
JAVAC_BIN="$JAVA_HOME/bin/javac"
echo "Building Android APK (a3270)..." JAR_BIN="$JAVA_HOME/bin/jar"
"$GRADLE_BIN" :a3270:assembleDebug elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
JAVAC_BIN="$(command -v javac)"
echo "Building Desktop App JAR (j3270)..." JAR_BIN="$(command -v jar)"
mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270" else
"$JAVA_HOME/bin/javac" -d "$BUILD_DIR/lib3270j" $(find lib3270j/src -name "*.java") # Check common SDKMAN / macOS / Linux JDK locations
"$JAVA_HOME/bin/javac" -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" $(find j3270/src -name "*.java") POSSIBLE_HOMES=(
"$HOME/.sdkman/candidates/java/current"
echo "Main-Class: org.pubvm.j3270.J3270App" > "$BUILD_DIR/MANIFEST.MF" "/Library/Java/JavaVirtualMachines"/*/Contents/Home
echo "" >> "$BUILD_DIR/MANIFEST.MF" "/usr/lib/jvm/default-java"
"/usr/lib/jvm/java-21-openjdk"*
"$JAVA_HOME/bin/jar" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \ "/usr/lib/jvm/java-17-openjdk"*
-C "$BUILD_DIR/lib3270j" . \ "/usr/lib/jvm/java-11-openjdk"*
-C "$BUILD_DIR/j3270" . )
for h in "${POSSIBLE_HOMES[@]}"; do
# Copy APK to build/ if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then
if [ -f "a3270/build/outputs/apk/debug/a3270-debug.apk" ]; then export JAVA_HOME="$h"
cp "a3270/build/outputs/apk/debug/a3270-debug.apk" "$BUILD_DIR/a3270.apk" JAVAC_BIN="$JAVA_HOME/bin/javac"
echo "Copied a3270-debug.apk to $BUILD_DIR/a3270.apk" JAR_BIN="$JAVA_HOME/bin/jar"
break
fi
done
fi fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
echo "ERROR: Java development tools (javac, jar) could not be found."
echo "Please install JDK 11+ or set JAVA_HOME."
exit 1
fi
echo "Using Java compiler: $JAVAC_BIN"
echo "Using JAR utility: $JAR_BIN"
BUILD_DIR="$SCRIPT_DIR/build"
rm -rf "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270" "$BUILD_DIR/MANIFEST.MF"
mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270"
# 2. Compile lib3270j
echo "Compiling lib3270j..."
LIB_SOURCES=()
while IFS= read -r -d '' file; do
LIB_SOURCES+=("$file")
done < <(find "$SCRIPT_DIR/lib3270j/src/main/java" -name "*.java" -print0)
if [ ${#LIB_SOURCES[@]} -eq 0 ]; then
echo "ERROR: No sources found in lib3270j/src/main/java"
exit 1
fi
"$JAVAC_BIN" -d "$BUILD_DIR/lib3270j" "${LIB_SOURCES[@]}"
# 3. Compile j3270
echo "Compiling j3270 application..."
APP_SOURCES=()
while IFS= read -r -d '' file; do
APP_SOURCES+=("$file")
done < <(find "$SCRIPT_DIR/j3270/src/main/java" -name "*.java" -print0)
if [ ${#APP_SOURCES[@]} -eq 0 ]; then
echo "ERROR: No sources found in j3270/src/main/java"
exit 1
fi
"$JAVAC_BIN" -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" "${APP_SOURCES[@]}"
# Copy any resources if present
if [ -d "$SCRIPT_DIR/j3270/src/main/resources" ]; then
cp -r "$SCRIPT_DIR/j3270/src/main/resources/"* "$BUILD_DIR/j3270/" 2>/dev/null || true
fi
if [ -d "$SCRIPT_DIR/lib3270j/src/main/resources" ]; then
cp -r "$SCRIPT_DIR/lib3270j/src/main/resources/"* "$BUILD_DIR/lib3270j/" 2>/dev/null || true
fi
# 4. Package executable JAR
echo "Packaging j3270.jar..."
cat << 'EOF' > "$BUILD_DIR/MANIFEST.MF"
Manifest-Version: 1.0
Main-Class: org.pubvm.j3270.J3270App
Implementation-Title: j3270
Implementation-Version: 0.1.0
Created-By: j3270 build_all.sh
EOF
"$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \
-C "$BUILD_DIR/j3270" . > /dev/null
echo "=== Build Complete ===" echo "=== Build Complete ==="
echo "Artifacts in $BUILD_DIR:" echo "Executable JAR created: $BUILD_DIR/j3270.jar"
ls -lh "$BUILD_DIR"/*.jar "$BUILD_DIR"/*.apk 2>/dev/null || true ls -lh "$BUILD_DIR/j3270.jar"
+59
View File
@@ -0,0 +1,59 @@
# Gitea Actions Runner Configuration for j3270
This repository includes Gitea Actions workflows in `.gitea/workflows/build.yaml` to automatically:
1. Compile and run unit tests.
2. Build the standalone `j3270.jar` executable via `build_all.sh`.
3. Upload `j3270.jar` as a downloadable artifact.
4. Report test results and diagnostics.
---
## 1. Setting up `act_runner` (Gitea Actions Runner)
To run CI workflows on your Gitea instance:
### Step 1: Download `act_runner`
Download the `act_runner` binary for your platform from the [Gitea act_runner releases](https://gitea.com/gitea/act_runner/releases).
### Step 2: Register the Runner
Obtain a runner registration token from **Repository Settings -> Actions -> Runners** or **Admin -> Actions -> Runners**, then register:
```bash
./act_runner register \
--instance https://your-gitea-instance.com \
--token <YOUR_REGISTRATION_TOKEN> \
--name "j3270-runner" \
--labels "ubuntu-latest:docker://node:18-bullseye,ubuntu-22.04:docker://node:18-bullseye"
```
### Step 3: Run the Runner Daemon
```bash
./act_runner daemon
```
Or using Docker Compose:
```yaml
version: '3.8'
services:
runner:
image: gitea/act_runner:latest
restart: always
environment:
- GITEA_INSTANCE_URL=https://your-gitea-instance.com
- GITEA_RUNNER_REGISTRATION_TOKEN=<YOUR_REGISTRATION_TOKEN>
- GITEA_RUNNER_NAME=j3270-runner
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:18-bullseye
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
```
---
## 2. CI/CD Workflow Summary
- **Trigger**: Pushes to `master`/`main`/`develop`, pull requests, and manual triggers via `workflow_dispatch`.
- **Artifacts**:
- `j3270-executable-jar`: Ready-to-run desktop terminal emulator jar (`java -jar j3270.jar`).
- `test-reports`: Full JUnit test reports and XML/HTML summaries.
- **Reporting**: On failure, automated diagnostic reports are generated and attached to the run summary.
Vendored Executable
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec "$(dirname "$0")/build_all.sh"
+13 -1
View File
@@ -7,5 +7,17 @@ dependencies {
} }
application { application {
mainClass = 'org.j3270.J3270App' mainClass = 'org.pubvm.j3270.J3270App'
}
jar {
manifest {
attributes 'Main-Class': 'org.pubvm.j3270.J3270App',
'Implementation-Title': 'j3270',
'Implementation-Version': archiveVersion
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
} }
@@ -194,19 +194,18 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
// ========== FTCutListener / FTDftListener Callbacks ========== // ========== FTCutListener / FTDftListener Callbacks ==========
@Override @Override
public void onTransferRunning() { public void onCutRunning() {
// Mode detection check handleTransferRunning(FTMode.CUT);
StackTraceElement[] st = Thread.currentThread().getStackTrace(); }
boolean isDft = false;
for (StackTraceElement elem : st) {
if (elem.getClassName().contains("FTDft")) {
isDft = true;
break;
}
}
@Override
public void onDftRunning() {
handleTransferRunning(FTMode.DFT);
}
private void handleTransferRunning(FTMode mode) {
if (activeMode == FTMode.UNKNOWN) { if (activeMode == FTMode.UNKNOWN) {
activeMode = isDft ? FTMode.DFT : FTMode.CUT; activeMode = mode;
log.info("FT mode established: " + activeMode); log.info("FT mode established: " + activeMode);
try { try {
if (activeMode == FTMode.DFT) { if (activeMode == FTMode.DFT) {
@@ -492,6 +492,14 @@ public class TerminalPanel extends JPanel {
// Clear // Clear
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C")); bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
// Extended 3270 functions
bindKeyToMap(im, "ERASE_INPUT", org.pubvm.j3270.config.Settings.getKeyBinding("ERASE_INPUT", "alt E"));
bindKeyToMap(im, "NEWLINE", org.pubvm.j3270.config.Settings.getKeyBinding("NEWLINE", "shift ENTER"));
bindKeyToMap(im, "DUP", org.pubvm.j3270.config.Settings.getKeyBinding("DUP", "alt D"));
bindKeyToMap(im, "FIELD_MARK", org.pubvm.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
bindKeyToMap(im, "ATTN", org.pubvm.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
bindKeyToMap(im, "SYSREQ", org.pubvm.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others) // Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
@@ -514,6 +522,12 @@ public class TerminalPanel extends JPanel {
am.put("j3270-BACK_SPACE", createAction(this::handleBackspace)); am.put("j3270-BACK_SPACE", createAction(this::handleBackspace));
am.put("j3270-INSERT", createAction(this::handleInsert)); am.put("j3270-INSERT", createAction(this::handleInsert));
am.put("j3270-CLEAR", createAction(this::handleClear)); am.put("j3270-CLEAR", createAction(this::handleClear));
am.put("j3270-ERASE_INPUT", createAction(this::handleEraseInput));
am.put("j3270-NEWLINE", createAction(this::handleNewline));
am.put("j3270-DUP", createAction(this::handleDup));
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
am.put("j3270-ATTN", createAction(this::handleAttn));
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
// Copy/Paste actions // Copy/Paste actions
am.put("j3270-COPY", createAction(this::copySelection)); am.put("j3270-COPY", createAction(this::copySelection));
@@ -658,6 +672,47 @@ public class TerminalPanel extends JPanel {
} }
} }
private void handleEraseInput() {
if (client != null && client.getConnectionState().isFullSession()) {
client.eraseInput();
refreshScreen();
}
}
private void handleNewline() {
if (client != null && client.getConnectionState().isFullSession()) {
client.newline();
refreshScreen();
}
}
private void handleDup() {
if (client != null && client.getConnectionState().isFullSession()) {
client.dup();
refreshScreen();
}
}
private void handleFieldMark() {
if (client != null && client.getConnectionState().isFullSession()) {
client.fieldMark();
refreshScreen();
}
}
private void handleAttn() {
if (client != null && client.isConnected()) {
client.attn();
}
}
private void handleSysReq() {
if (client != null && client.isConnected()) {
client.sysReq();
refreshScreen();
}
}
private void refreshScreen() { private void refreshScreen() {
repaint(); repaint();
// Notify parent to update status bar too // Notify parent to update status bar too
@@ -196,6 +196,18 @@ public class Telnet3270Client {
public void tab() { inputProcessor.tab(); } public void tab() { inputProcessor.tab(); }
/** Back-tab to previous unprotected field. */ /** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); } public void backTab() { inputProcessor.backTab(); }
/** Move cursor to next line. */
public void newline() { inputProcessor.newline(); }
/** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); }
/** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); }
/** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); }
/** Attention key. */
public void attn() { inputProcessor.attn(); }
/** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */ /** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); } public void reset() { inputProcessor.reset(); }
} }
@@ -816,8 +816,18 @@ public class DataStreamProcessor {
} }
private void sendRequestedQueryReplies(byte[] codes) { private void sendRequestedQueryReplies(byte[] codes) {
// For now, send all — refinement can come later byte[] qr = qrBuilder.buildQueryReplies(codes, screen.getMaxCols(), screen.getMaxRows(),
sendAllQueryReplies(); screen.getMaxCols() * screen.getMaxRows());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < qr.length; i++) {
sb.append(String.format("%02x ", qr[i] & 0xFF));
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.info("Requested query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
} }
// ========== Output helpers ========== // ========== Output helpers ==========
@@ -70,13 +70,69 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
// Distributed Data Management (DFT File Transfer) // Distributed Data Management (DFT File Transfer)
// Hardcoded limit of 4096 is standard; the client can configure it during transfer.
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
// Implicit Partition // Implicit Partition
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
log.info("Built " + out.size() + " bytes of query replies"); log.info("Built " + out.size() + " bytes of all query replies");
return out.toByteArray();
}
/**
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
* matching x3270 sf.c behavior.
*/
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
if (requestedCodes == null || requestedCodes.length == 0) {
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
}
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
out.write(AID_SF);
for (byte codeByte : requestedCodes) {
int code = codeByte & 0xFF;
switch (code) {
case QR_SUMMARY:
appendQueryReply(out, QR_SUMMARY, buildSummary());
break;
case QR_USABLE_AREA:
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
break;
case QR_ALPHA_PART:
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
break;
case QR_CHARSETS:
appendQueryReply(out, QR_CHARSETS, buildCharsets());
break;
case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor());
break;
case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
break;
case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
break;
case QR_DDM:
appendQueryReply(out, QR_DDM, buildDdm(4096));
break;
case QR_IMP_PART:
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_RPQNAMES:
appendQueryReply(out, QR_RPQNAMES, new byte[0]);
break;
default:
// Unsupported query reply code emit QR_NULL
appendQueryReply(out, QR_NULL, new byte[0]);
break;
}
}
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
return out.toByteArray(); return out.toByteArray();
} }
@@ -225,84 +225,96 @@ public class FTConfig {
// IND$FILE GET/PUT hostfile // IND$FILE GET/PUT hostfile
cmd.append("IND$FILE "); cmd.append("IND$FILE ");
cmd.append(isReceive() ? "GET " : "PUT "); cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename); cmd.append(hostFilename != null ? hostFilename.trim() : "");
// CMS/CICS options use ( prefix // Collect options into a separate buffer
if (hostType != HostType.TSO) { StringBuilder opts = new StringBuilder();
cmd.append(" (");
} else {
cmd.append(" ");
}
// Mode // Mode
if (isAscii()) { if (isAscii()) {
cmd.append("ASCII"); opts.append("ASCII");
} else if (hostType == HostType.CICS) { } else if (hostType == HostType.CICS) {
cmd.append("BINARY"); opts.append("BINARY");
} }
// CR/LF handling // CR/LF handling
if (isAscii() && isCrFlag()) { if (isAscii() && isCrFlag()) {
cmd.append(" CRLF"); if (opts.length() > 0) opts.append(" ");
} else if (hostType == HostType.CICS) { opts.append("CRLF");
cmd.append(" NOCRLF"); } else if (hostType == HostType.CICS && !isAscii()) {
if (opts.length() > 0) opts.append(" ");
opts.append("NOCRLF");
} }
// Append (send only) // Append (send only)
if (isAppend() && isSend()) { if (isAppend() && isSend()) {
cmd.append(" APPEND"); if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
} }
// TSO-specific send options // Host-specific send options
if (isSend()) { if (isSend()) {
if (hostType == HostType.TSO) { if (hostType == HostType.TSO) {
if (recfm != RecordFormat.DEFAULT) { if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM("); if (opts.length() > 0) opts.append(" ");
opts.append("RECFM(");
switch (recfm) { switch (recfm) {
case FIXED: cmd.append("F"); break; case FIXED: opts.append("F"); break;
case VARIABLE: cmd.append("V"); break; case VARIABLE: opts.append("V"); break;
case UNDEFINED: cmd.append("U"); break; case UNDEFINED: opts.append("U"); break;
default: break; default: break;
} }
cmd.append(")"); opts.append(")");
if (lrecl > 0) { if (lrecl > 0) {
cmd.append(" LRECL(").append(lrecl).append(")"); opts.append(" LRECL(").append(lrecl).append(")");
} }
if (blksize > 0) { if (blksize > 0) {
cmd.append(" BLKSIZE(").append(blksize).append(")"); opts.append(" BLKSIZE(").append(blksize).append(")");
} }
} }
if (units != AllocationUnit.DEFAULT) { if (units != AllocationUnit.DEFAULT) {
cmd.append(" SPACE(").append(primarySpace); if (opts.length() > 0) opts.append(" ");
opts.append("SPACE(").append(primarySpace);
if (secondarySpace > 0) { if (secondarySpace > 0) {
cmd.append(",").append(secondarySpace); opts.append(",").append(secondarySpace);
} }
cmd.append(")"); opts.append(")");
switch (units) { switch (units) {
case TRACKS: cmd.append(" TRACKS"); break; case TRACKS: opts.append(" TRACKS"); break;
case CYLINDERS: cmd.append(" CYLINDERS"); break; case CYLINDERS: opts.append(" CYLINDERS"); break;
case AVBLOCK: cmd.append(" AVBLOCK(").append(avblock).append(")"); break; case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break;
default: break; default: break;
} }
} }
} else if (hostType == HostType.CMS) { } else if (hostType == HostType.CMS) {
if (recfm != RecordFormat.DEFAULT) { if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM "); if (opts.length() > 0) opts.append(" ");
opts.append("RECFM ");
switch (recfm) { switch (recfm) {
case FIXED: cmd.append("F"); break; case FIXED: opts.append("F"); break;
case VARIABLE: cmd.append("V"); break; case VARIABLE: opts.append("V"); break;
default: break; default: break;
} }
if (lrecl > 0) { if (lrecl > 0) {
cmd.append(" LRECL ").append(lrecl); opts.append(" LRECL ").append(lrecl);
} }
} }
} }
} }
// Additional options // Additional options
if (otherOptions != null) { if (otherOptions != null && !otherOptions.trim().isEmpty()) {
cmd.append(" ").append(otherOptions); if (opts.length() > 0) opts.append(" ");
opts.append(otherOptions.trim());
}
// Apply options with host-specific prefix only if options are present
if (opts.length() > 0) {
if (hostType != HostType.TSO) {
cmd.append(" (").append(opts.toString());
} else {
cmd.append(" ").append(opts.toString());
}
} }
return cmd.toString().trim(); return cmd.toString().trim();
@@ -21,7 +21,7 @@ public class FTCut {
/** Callback for transfer events */ /** Callback for transfer events */
public interface FTCutListener { public interface FTCutListener {
void onTransferRunning(); void onCutRunning();
void onTransferComplete(String errorMessage); void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage); void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes); void onBytesTransferred(long bytes);
@@ -221,7 +221,7 @@ public class FTCut {
xlateBufIx = 0; xlateBufIx = 0;
cutEof = false; cutEof = false;
cutAck(); cutAck();
listener.onTransferRunning(); listener.onCutRunning();
break; break;
case SC_XFER_COMPLETE: case SC_XFER_COMPLETE:
@@ -23,7 +23,7 @@ public class FTDft {
/** Callback for transfer events (shared interface with CUT) */ /** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener { public interface FTDftListener {
void onTransferRunning(); void onDftRunning();
void onTransferComplete(String errorMessage); void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage); void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes); void onBytesTransferred(long bytes);
@@ -178,7 +178,7 @@ public class FTDft {
log.info("DFT: Open request for message stream"); log.info("DFT: Open request for message stream");
} else { } else {
messageFlag = false; messageFlag = false;
listener.onTransferRunning(); listener.onDftRunning();
} }
dftEof = false; dftEof = false;
@@ -4,6 +4,7 @@ import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute; import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.charset.EbcdicTranslator; import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.telnet.TelnetFSM; import org.lib3270j.telnet.TelnetFSM;
import org.lib3270j.protocol.TelnetConstants;
import static org.lib3270j.protocol.DS3270Constants.*; import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -362,6 +363,82 @@ public class InputProcessor {
deleteChar(); deleteChar();
} }
/** Erase All Unprotected fields (Erase Input key in 3270). */
public void eraseInput() {
if (keyboardLocked) return;
screen.eraseAllUnprotected();
screen.markAllChanged();
}
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
public void newline() {
if (keyboardLocked) return;
int cur = screen.getCursorAddress();
int cols = screen.getCols();
int rows = screen.getRows();
int curRow = cur / cols;
int nextRow = (curRow + 1) % rows;
int target = nextRow * cols;
if (screen.isFormatted()) {
target = screen.findNextUnprotected(target == 0 ? (rows * cols - 1) : (target - 1));
}
screen.setCursorAddress(target);
screen.markAllChanged();
}
/** Insert Duplicate (DUP) code and advance to next field. */
public void dup() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_DUP;
ea.ucs4 = '*';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
tab();
}
/** Insert Field Mark (FM) code. */
public void fieldMark() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_FM;
ea.ucs4 = ';';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
int size = screen.getRows() * screen.getCols();
baddr = (baddr + 1) % size;
while (screen.getCell(baddr).isFieldAttribute()) {
baddr = (baddr + 1) % size;
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
}
/** Attention key (sends Telnet IP). */
public void attn() {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP };
fsm.sendRecord(ip);
}
}
/** SysReq key. */
public void sysReq() {
reset();
}
/** Reset (unlock keyboard, cancel insert mode). */ /** Reset (unlock keyboard, cancel insert mode). */
public void reset() { public void reset() {
setKeyboardLocked(false); setKeyboardLocked(false);
@@ -305,6 +305,7 @@ public class ScreenBuffer {
private char getAplGraphic(int ec) { private char getAplGraphic(int ec) {
switch (ec) { switch (ec) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─' case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│' case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌' case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
@@ -316,6 +317,24 @@ public class ScreenBuffer {
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬' case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴' case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼' case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return translator.ebcdicToUnicode(ec); default: return translator.ebcdicToUnicode(ec);
} }
} }
@@ -37,13 +37,26 @@ public class TelnetConnection {
* Connect to the host. Blocks until connection is established or fails. * Connect to the host. Blocks until connection is established or fails.
*/ */
public void connect() throws IOException { public void connect() throws IOException {
log.info("Connecting to " + config.getHost() + ":" + config.getPort()); if (config.isUseTls()) {
socket = new Socket(); log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort());
socket.setKeepAlive(true); javax.net.ssl.SSLSocketFactory ssf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
socket.setOOBInline(true); javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
socket.setTcpNoDelay(true); sslSocket.setKeepAlive(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()), sslSocket.setOOBInline(true);
config.getConnectTimeoutMs()); sslSocket.setTcpNoDelay(true);
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
sslSocket.startHandshake();
socket = sslSocket;
} else {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
}
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE); inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
outputStream = new BufferedOutputStream(socket.getOutputStream()); outputStream = new BufferedOutputStream(socket.getOutputStream());
@@ -0,0 +1,66 @@
package org.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EbcdicTranslatorTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
@Test
public void testBasicAlphanumericTranslation() {
assertEquals('A', translator.ebcdicToUnicode(0xC1));
assertEquals('B', translator.ebcdicToUnicode(0xC2));
assertEquals('Z', translator.ebcdicToUnicode(0xE9));
assertEquals('a', translator.ebcdicToUnicode(0x81));
assertEquals('z', translator.ebcdicToUnicode(0xA9));
assertEquals('0', translator.ebcdicToUnicode(0xF0));
assertEquals('9', translator.ebcdicToUnicode(0xF9));
assertEquals(' ', translator.ebcdicToUnicode(0x40));
}
@Test
public void testReverseTranslation() {
assertEquals(0xC1, translator.unicodeToEbcdic('A'));
assertEquals(0x81, translator.unicodeToEbcdic('a'));
assertEquals(0xF0, translator.unicodeToEbcdic('0'));
assertEquals(0x40, translator.unicodeToEbcdic(' '));
assertEquals(0x4B, translator.unicodeToEbcdic('.'));
assertEquals(0x6B, translator.unicodeToEbcdic(','));
}
@Test
public void testSpecialCharactersCP037() {
// [ and ]
assertEquals(0xBA, translator.unicodeToEbcdic('['));
assertEquals(0xBB, translator.unicodeToEbcdic(']'));
assertEquals('[', translator.ebcdicToUnicode(0xBA));
assertEquals(']', translator.ebcdicToUnicode(0xBB));
// { and }
assertEquals(0xC0, translator.unicodeToEbcdic('{'));
assertEquals(0xD0, translator.unicodeToEbcdic('}'));
assertEquals('{', translator.ebcdicToUnicode(0xC0));
assertEquals('}', translator.ebcdicToUnicode(0xD0));
// Backslash \
assertEquals(0xE0, translator.unicodeToEbcdic('\\'));
assertEquals('\\', translator.ebcdicToUnicode(0xE0));
// Caret ^
assertEquals(0xB0, translator.unicodeToEbcdic('^'));
assertEquals('^', translator.ebcdicToUnicode(0xB0));
// Tilde ~
assertEquals(0xA1, translator.unicodeToEbcdic('~'));
assertEquals('~', translator.ebcdicToUnicode(0xA1));
}
@Test
public void testStringRoundTrip() {
String test = "Hello World! 123 [TEST] {VAR} \\";
byte[] ebcdic = translator.stringToEbcdic(test);
String restored = translator.ebcdicToString(ebcdic, 0, ebcdic.length);
assertEquals(test, restored);
}
}
@@ -0,0 +1,69 @@
package org.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class DataStreamProcessorTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
private final DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
@Test
public void testEraseWriteAndSBAOrder() {
// Build an EW record:
// Byte 0: CMD_EW (0x05)
// Byte 1: WCC (0xC3) - restore keyboard, reset MDT
// Byte 2: ORDER_SBA (0x11)
// Byte 3, 4: Encoded address for 0 (0x40, 0x40)
// Byte 5: ORDER_SF (0x1D)
// Byte 6: FA (0xC0) - unprotected
// Byte 7..11: EBCDIC "HELLO" (0xC8, 0xC5, 0xD3, 0xD3, 0xD6)
byte[] record = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) ORDER_SF,
(byte) FA_PRINTABLE,
(byte) 0xC8, (byte) 0xC5, (byte) 0xD3, (byte) 0xD3, (byte) 0xD6
};
processor.processRecord(record, 0, record.length, true);
assertTrue(screen.isFormatted());
assertTrue(screen.getCell(0).isFieldAttribute());
// Positions 1..5 should contain "HELLO"
assertEquals('H', screen.getCell(1).ucs4);
assertEquals('E', screen.getCell(2).ucs4);
assertEquals('L', screen.getCell(3).ucs4);
assertEquals('L', screen.getCell(4).ucs4);
assertEquals('O', screen.getCell(5).ucs4);
}
@Test
public void testRepeatToAddressOrder() {
// Write record with RA from 0 to 10 with character 'A' (0xC1)
byte[] record = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40, // Address 0
(byte) ORDER_RA,
(byte) 0x40, (byte) 0x4A, // Address 10 (0x40, 0x4A)
(byte) 0xC1 // EBCDIC 'A'
};
processor.processRecord(record, 0, record.length, true);
for (int i = 0; i < 10; i++) {
assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'");
}
}
}
@@ -0,0 +1,55 @@
package org.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.screen.ScreenBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class QueryReplyBuilderTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
@Test
public void testBuildAllQueryReplies() {
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
assertTrue(replies.length > 0);
assertEquals((byte) AID_SF, replies[0]);
}
@Test
public void testBuildFilteredQueryRepliesWithSupportedCodes() {
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// First SF should be DDM: length (2 bytes), SFID_QREPLY (0x81), QR_DDM (0x95)
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_DDM, replies[4] & 0xFF);
// Second SF should be Usable Area
int pos2 = 1 + len1;
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
}
@Test
public void testBuildFilteredQueryRepliesWithUnsupportedCode() {
byte[] requested = new byte[] { (byte) 0x77 }; // Unsupported code
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// Should emit QR_NULL (0xFF)
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_NULL, replies[4] & 0xFF);
}
}
@@ -0,0 +1,83 @@
package org.lib3270j.ft;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class FTConfigTest {
@Test
public void testTsoReceiveAscii() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("MY.DATASET");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
assertEquals("IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
}
@Test
public void testTsoReceiveBinary() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("'USER.BIN.DATA'");
config.setTransferMode(FTConfig.TransferMode.BINARY);
assertEquals("IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
}
@Test
public void testTsoSendFixedRecfm() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("MY.DATASET");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRecfm("F");
config.setLrecl("80");
config.setBlksize("3120");
assertEquals("IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
}
@Test
public void testCmsReceiveBinaryNoNakedParen() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("PROFILE EXEC A");
config.setTransferMode(FTConfig.TransferMode.BINARY);
// In CMS, binary receive has no options, so NO open parenthesis '(' should be generated!
assertEquals("IND$FILE GET PROFILE EXEC A", config.buildCommand());
}
@Test
public void testCmsReceiveAscii() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("TEST DATA A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
assertEquals("IND$FILE GET TEST DATA A (ASCII CRLF", config.buildCommand());
}
@Test
public void testCmsSendWithRecfmAndLrecl() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("TEST FILE A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRecfm("V");
config.setLrecl("132");
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand());
}
}
@@ -0,0 +1,51 @@
package org.lib3270j.protocol;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class DS3270ConstantsTest {
@Test
public void test12BitAddressRoundTrip() {
int rows = 24;
int cols = 80;
int maxAddr = rows * cols; // 1920
for (int addr = 0; addr < maxAddr; addr += 17) {
byte[] encoded = encodeAddress(addr, rows, cols);
assertEquals(2, encoded.length);
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
assertEquals(addr, decoded, "Address " + addr + " failed round trip");
}
}
@Test
public void test14BitAddressRoundTrip() {
int rows = 60;
int cols = 80; // 4800 (> 4096, triggers 14-bit format)
int maxAddr = rows * cols;
for (int addr = 0; addr < maxAddr; addr += 123) {
byte[] encoded = encodeAddress(addr, rows, cols);
assertEquals(2, encoded.length);
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
assertEquals(addr, decoded, "14-bit address " + addr + " failed round trip");
}
}
@Test
public void testFieldAttributePredicates() {
int protectedHighMod = FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL | FA_MODIFY;
assertTrue(faIsProtected(protectedHighMod));
assertTrue(faIsHigh(protectedHighMod));
assertTrue(faIsModified(protectedHighMod));
assertFalse(faIsNumeric(protectedHighMod));
assertFalse(faIsSkip(protectedHighMod));
int skipField = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC;
assertTrue(faIsSkip(skipField));
assertTrue(faIsProtected(skipField));
assertTrue(faIsNumeric(skipField));
}
}
@@ -0,0 +1,64 @@
package org.lib3270j.screen;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class ScreenBufferTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
@Test
public void testScreenInitialization() {
assertEquals(24, screen.getRows());
assertEquals(80, screen.getCols());
assertEquals(43, screen.getMaxRows());
assertEquals(80, screen.getMaxCols());
assertFalse(screen.isFormatted());
}
@Test
public void testScreenEraseAlternate() {
screen.erase(true);
assertTrue(screen.isScreenAlt());
assertEquals(43, screen.getRows());
assertEquals(80, screen.getCols());
screen.erase(false);
assertFalse(screen.isScreenAlt());
assertEquals(24, screen.getRows());
assertEquals(80, screen.getCols());
}
@Test
public void testFieldNavigationAndFindAttribute() {
screen.erase(false);
// Place field attribute at position 0 (unprotected)
screen.setCellFA(0, (byte) FA_PRINTABLE);
// Place field attribute at position 20 (protected)
screen.setCellFA(20, (byte) (FA_PRINTABLE | FA_PROTECT));
assertTrue(screen.isFormatted());
assertEquals(0, screen.findFieldAttribute(5));
assertEquals(20, screen.findFieldAttribute(25));
// findNextUnprotected
int nextUnprot = screen.findNextUnprotected(0);
assertEquals(1, nextUnprot);
}
@Test
public void testGraphicEscapeTranslation() {
screen.erase(false);
// Set cell at position 10 to horizontal line (0xA2) with CS_GE (0x04)
ExtendedAttribute ea = screen.getCell(10);
ea.ec = (byte) 0xA2;
ea.cs = (byte) CS_GE;
screen.translateToUnicode();
assertEquals('\u2500', ea.ucs4); // '─'
}
}
+9 -38
View File
@@ -3,43 +3,14 @@
# Usage: ./run.sh [host] [port] [model] # Usage: ./run.sh [host] [port] [model]
# Example: ./run.sh mainframe.example.com 23 4 # Example: ./run.sh mainframe.example.com 23 4
JAVA_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$JAVA_DIR/build" BUILD_DIR="$SCRIPT_DIR/build"
# Always rebuild to pick up changes # Build if jar is missing or rebuild requested
echo "Building lib3270j..." if [ ! -f "$BUILD_DIR/j3270.jar" ]; then
rm -rf "$BUILD_DIR" "$SCRIPT_DIR/build_all.sh"
mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270" fi
# Compile libraries exec java -Dapple.laf.useScreenMenuBar=true \
find "$JAVA_DIR/lib3270j/src" -name "*.java" -print0 | xargs -0 javac -d "$BUILD_DIR/lib3270j" -Dapple.awt.application.name=j3270 \
-jar "$BUILD_DIR/j3270.jar" "$@"
echo "Building j3270..."
find "$JAVA_DIR/j3270/src" -name "*.java" -print0 | xargs -0 javac -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270"
echo "Build complete."
# ---------------------------------------------------------
# NEW: Package the shareable JAR
# ---------------------------------------------------------
echo "Packaging j3270.jar..."
# 1. Create the Manifest file indicating the entry point
# Note: The manifest file must end with a new line or carriage return
echo "Main-Class: org.pubvm.j3270.J3270App" > "$BUILD_DIR/MANIFEST.MF"
echo "" >> "$BUILD_DIR/MANIFEST.MF"
# 2. Build the JAR using both compiled directories
jar cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \
-C "$BUILD_DIR/j3270" .
echo "Executable JAR created at: $BUILD_DIR/j3270.jar"
# ---------------------------------------------------------
# Run the newly packaged JAR
# ---------------------------------------------------------
#exec java -Dapple.laf.useScreenMenuBar=true \
# -Dapple.awt.application.name=j3270 \
# -jar "$BUILD_DIR/j3270.jar" "$@"
nohup java -jar build/j3270.jar &
-1
View File
@@ -2,4 +2,3 @@ rootProject.name = 'j3270-project'
include 'lib3270j' include 'lib3270j'
include 'j3270' include 'j3270'
include 'a3270'