diff --git a/.gitignore b/.gitignore index 18a179c..01b729c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store *.jar build +.gradle diff --git a/.gradle/8.5/checksums/checksums.lock b/.gradle/8.5/checksums/checksums.lock deleted file mode 100644 index a20b0af..0000000 Binary files a/.gradle/8.5/checksums/checksums.lock and /dev/null differ diff --git a/.gradle/8.5/checksums/md5-checksums.bin b/.gradle/8.5/checksums/md5-checksums.bin deleted file mode 100644 index 1a70dfd..0000000 Binary files a/.gradle/8.5/checksums/md5-checksums.bin and /dev/null differ diff --git a/.gradle/8.5/checksums/sha1-checksums.bin b/.gradle/8.5/checksums/sha1-checksums.bin deleted file mode 100644 index f1e86c1..0000000 Binary files a/.gradle/8.5/checksums/sha1-checksums.bin and /dev/null differ diff --git a/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock b/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock deleted file mode 100644 index 79fd8ed..0000000 Binary files a/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock and /dev/null differ diff --git a/.gradle/8.5/fileChanges/last-build.bin b/.gradle/8.5/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/.gradle/8.5/fileChanges/last-build.bin and /dev/null differ diff --git a/.gradle/8.5/fileHashes/fileHashes.lock b/.gradle/8.5/fileHashes/fileHashes.lock deleted file mode 100644 index 8be93d8..0000000 Binary files a/.gradle/8.5/fileHashes/fileHashes.lock and /dev/null differ diff --git a/.gradle/8.5/gc.properties b/.gradle/8.5/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/.gradle/9.2.0/checksums/checksums.lock b/.gradle/9.2.0/checksums/checksums.lock deleted file mode 100644 index d391b44..0000000 Binary files a/.gradle/9.2.0/checksums/checksums.lock and /dev/null differ diff --git a/.gradle/9.2.0/checksums/md5-checksums.bin b/.gradle/9.2.0/checksums/md5-checksums.bin deleted file mode 100644 index 80d68af..0000000 Binary files a/.gradle/9.2.0/checksums/md5-checksums.bin and /dev/null differ diff --git a/.gradle/9.2.0/checksums/sha1-checksums.bin b/.gradle/9.2.0/checksums/sha1-checksums.bin deleted file mode 100644 index 77e51ad..0000000 Binary files a/.gradle/9.2.0/checksums/sha1-checksums.bin and /dev/null differ diff --git a/.gradle/9.2.0/fileChanges/last-build.bin b/.gradle/9.2.0/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/.gradle/9.2.0/fileChanges/last-build.bin and /dev/null differ diff --git a/.gradle/9.2.0/fileHashes/fileHashes.bin b/.gradle/9.2.0/fileHashes/fileHashes.bin deleted file mode 100644 index 7412165..0000000 Binary files a/.gradle/9.2.0/fileHashes/fileHashes.bin and /dev/null differ diff --git a/.gradle/9.2.0/fileHashes/fileHashes.lock b/.gradle/9.2.0/fileHashes/fileHashes.lock deleted file mode 100644 index 2303a76..0000000 Binary files a/.gradle/9.2.0/fileHashes/fileHashes.lock and /dev/null differ diff --git a/.gradle/9.2.0/gc.properties b/.gradle/9.2.0/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index b6cc982..0000000 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index 2a7ea30..0000000 --- a/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Wed Mar 18 14:12:38 EDT 2026 -gradle.version=8.5 diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/bugs.txt b/bugs.txt index e69de29..58bd518 100644 --- a/bugs.txt +++ b/bugs.txt @@ -0,0 +1 @@ +IND$FILE no longer works in CMS or TSO diff --git a/build.gradle b/build.gradle index df820f0..2cbf342 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,12 @@ -plugins { - id 'java' +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 { @@ -7,23 +14,26 @@ allprojects { version = '0.1.0' repositories { + google() mavenCentral() } } subprojects { - apply plugin: 'java' + if (name != 'a3270') { + apply plugin: 'java' - java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } + java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } - test { - useJUnitPlatform() - } + test { + useJUnitPlatform() + } - dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + } } } diff --git a/build/j3270/org/pubvm/j3270/J3270App$1.class b/build/j3270/org/pubvm/j3270/J3270App$1.class index 8ca98af..2aab021 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App$1.class and b/build/j3270/org/pubvm/j3270/J3270App$1.class differ diff --git a/build/j3270/org/pubvm/j3270/J3270App$2.class b/build/j3270/org/pubvm/j3270/J3270App$2.class index 5113d3f..554b90f 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App$2.class and b/build/j3270/org/pubvm/j3270/J3270App$2.class differ diff --git a/build/j3270/org/pubvm/j3270/J3270App.class b/build/j3270/org/pubvm/j3270/J3270App.class index fe40d61..d7ec860 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App.class and b/build/j3270/org/pubvm/j3270/J3270App.class differ diff --git a/build/j3270/org/pubvm/j3270/config/Settings$StartupBehavior.class b/build/j3270/org/pubvm/j3270/config/Settings$StartupBehavior.class index 6839a76..936d123 100644 Binary files a/build/j3270/org/pubvm/j3270/config/Settings$StartupBehavior.class and b/build/j3270/org/pubvm/j3270/config/Settings$StartupBehavior.class differ diff --git a/build/j3270/org/pubvm/j3270/config/Settings.class b/build/j3270/org/pubvm/j3270/config/Settings.class index 0614591..9c8e953 100644 Binary files a/build/j3270/org/pubvm/j3270/config/Settings.class and b/build/j3270/org/pubvm/j3270/config/Settings.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class b/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class index e8ba19b..53ed31e 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class and b/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class index 145519e..edfecf8 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class index 7e3d3e9..d336d30 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class index f5efe87..f7a00f1 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class index 3d9b553..e9e7d8a 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class index d925b0d..c9bb757 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class b/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class index c4929f9..25ecac0 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class and b/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/StatusBar.class b/build/j3270/org/pubvm/j3270/ui/StatusBar.class index e157413..c7a9935 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/StatusBar.class and b/build/j3270/org/pubvm/j3270/ui/StatusBar.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class index c0bbf60..7cb704d 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class index c26fa2c..371789c 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class index a8bfd05..7cd527f 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class differ diff --git a/build/lib3270j/org/lib3270j/ConnectionConfig.class b/build/lib3270j/org/lib3270j/ConnectionConfig.class index c724b82..0cc933d 100644 Binary files a/build/lib3270j/org/lib3270j/ConnectionConfig.class and b/build/lib3270j/org/lib3270j/ConnectionConfig.class differ diff --git a/build/lib3270j/org/lib3270j/ConnectionState.class b/build/lib3270j/org/lib3270j/ConnectionState.class index 6b99c5d..73e6adb 100644 Binary files a/build/lib3270j/org/lib3270j/ConnectionState.class and b/build/lib3270j/org/lib3270j/ConnectionState.class differ diff --git a/build/lib3270j/org/lib3270j/DiagnosticClient$1.class b/build/lib3270j/org/lib3270j/DiagnosticClient$1.class index f97f102..47eb1aa 100644 Binary files a/build/lib3270j/org/lib3270j/DiagnosticClient$1.class and b/build/lib3270j/org/lib3270j/DiagnosticClient$1.class differ diff --git a/build/lib3270j/org/lib3270j/DiagnosticClient$2.class b/build/lib3270j/org/lib3270j/DiagnosticClient$2.class index 8ba4e60..495f49b 100644 Binary files a/build/lib3270j/org/lib3270j/DiagnosticClient$2.class and b/build/lib3270j/org/lib3270j/DiagnosticClient$2.class differ diff --git a/build/lib3270j/org/lib3270j/DiagnosticClient.class b/build/lib3270j/org/lib3270j/DiagnosticClient.class index f4c643d..86dc20e 100644 Binary files a/build/lib3270j/org/lib3270j/DiagnosticClient.class and b/build/lib3270j/org/lib3270j/DiagnosticClient.class differ diff --git a/build/lib3270j/org/lib3270j/Telnet3270Client.class b/build/lib3270j/org/lib3270j/Telnet3270Client.class index 27c9753..8ada131 100644 Binary files a/build/lib3270j/org/lib3270j/Telnet3270Client.class and b/build/lib3270j/org/lib3270j/Telnet3270Client.class differ diff --git a/build/lib3270j/org/lib3270j/TerminalModel.class b/build/lib3270j/org/lib3270j/TerminalModel.class index bf03ca1..837e0f3 100644 Binary files a/build/lib3270j/org/lib3270j/TerminalModel.class and b/build/lib3270j/org/lib3270j/TerminalModel.class differ diff --git a/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class b/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class index 2469a25..e214353 100644 Binary files a/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class and b/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor$OutputSender.class b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor$OutputSender.class index cfce635..b5b120b 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor$OutputSender.class and b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor$OutputSender.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class index 725d46d..da1d3aa 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class and b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class index 00aab4e..cbe03a7 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class and b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class differ diff --git a/build/lib3270j/org/lib3270j/input/InputProcessor.class b/build/lib3270j/org/lib3270j/input/InputProcessor.class index 0158a88..6ec5520 100644 Binary files a/build/lib3270j/org/lib3270j/input/InputProcessor.class and b/build/lib3270j/org/lib3270j/input/InputProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/listener/ConnectionListener.class b/build/lib3270j/org/lib3270j/listener/ConnectionListener.class index e556307..515815c 100644 Binary files a/build/lib3270j/org/lib3270j/listener/ConnectionListener.class and b/build/lib3270j/org/lib3270j/listener/ConnectionListener.class differ diff --git a/build/lib3270j/org/lib3270j/listener/ScreenUpdateListener.class b/build/lib3270j/org/lib3270j/listener/ScreenUpdateListener.class index 706455b..d589c15 100644 Binary files a/build/lib3270j/org/lib3270j/listener/ScreenUpdateListener.class and b/build/lib3270j/org/lib3270j/listener/ScreenUpdateListener.class differ diff --git a/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class b/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class index 1fdbea2..6280c87 100644 Binary files a/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class and b/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class differ diff --git a/build/lib3270j/org/lib3270j/protocol/TN3270EConstants.class b/build/lib3270j/org/lib3270j/protocol/TN3270EConstants.class index 0de4f69..2386640 100644 Binary files a/build/lib3270j/org/lib3270j/protocol/TN3270EConstants.class and b/build/lib3270j/org/lib3270j/protocol/TN3270EConstants.class differ diff --git a/build/lib3270j/org/lib3270j/protocol/TelnetConstants.class b/build/lib3270j/org/lib3270j/protocol/TelnetConstants.class index f95779f..e9ecbdf 100644 Binary files a/build/lib3270j/org/lib3270j/protocol/TelnetConstants.class and b/build/lib3270j/org/lib3270j/protocol/TelnetConstants.class differ diff --git a/build/lib3270j/org/lib3270j/screen/ExtendedAttribute.class b/build/lib3270j/org/lib3270j/screen/ExtendedAttribute.class index 63a37cf..eac570f 100644 Binary files a/build/lib3270j/org/lib3270j/screen/ExtendedAttribute.class and b/build/lib3270j/org/lib3270j/screen/ExtendedAttribute.class differ diff --git a/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class b/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class index 2dbf12c..aaf779b 100644 Binary files a/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class and b/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class index 3671d43..735db9f 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class and b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class b/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class index 89f0eb3..241800d 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class and b/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class b/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class index 58aed75..4a3210d 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class and b/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class differ diff --git a/build_all.sh b/build_all.sh new file mode 100755 index 0000000..f11c32f --- /dev/null +++ b/build_all.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +echo "=== Building j3270 (Desktop JAR) and a3270 (Android APK) ===" + +export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem" +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" +BUILD_DIR="$(pwd)/build" + +mkdir -p "$BUILD_DIR" + +echo "Building Android APK (a3270)..." +"$GRADLE_BIN" :a3270:assembleDebug + +echo "Building Desktop App JAR (j3270)..." +mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270" +"$JAVA_HOME/bin/javac" -d "$BUILD_DIR/lib3270j" $(find lib3270j/src -name "*.java") +"$JAVA_HOME/bin/javac" -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" $(find j3270/src -name "*.java") + +echo "Main-Class: org.pubvm.j3270.J3270App" > "$BUILD_DIR/MANIFEST.MF" +echo "" >> "$BUILD_DIR/MANIFEST.MF" + +"$JAVA_HOME/bin/jar" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \ + -C "$BUILD_DIR/lib3270j" . \ + -C "$BUILD_DIR/j3270" . + +# Copy APK to build/ +if [ -f "a3270/build/outputs/apk/debug/a3270-debug.apk" ]; then + cp "a3270/build/outputs/apk/debug/a3270-debug.apk" "$BUILD_DIR/a3270.apk" + echo "Copied a3270-debug.apk to $BUILD_DIR/a3270.apk" +fi + +echo "=== Build Complete ===" +echo "Artifacts in $BUILD_DIR:" +ls -lh "$BUILD_DIR"/*.jar "$BUILD_DIR"/*.apk 2>/dev/null || true diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..50234ab --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m diff --git a/j3270/src/main/java/org/pubvm/j3270/J3270App.java b/j3270/src/main/java/org/pubvm/j3270/J3270App.java index d9e3345..829ae1f 100644 --- a/j3270/src/main/java/org/pubvm/j3270/J3270App.java +++ b/j3270/src/main/java/org/pubvm/j3270/J3270App.java @@ -350,12 +350,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate @Override public void onScreenUpdated() { SwingUtilities.invokeLater(() -> { - if (client != null) { - client.getInputProcessor().setKeyboardLocked(false); - } + // During an active file transfer, let the CUT/DFT handler drive + // keyboard state. In x3270, ft_cut_data() runs before WCC + // keyboard-restore is applied — the keyboard stays locked for the + // entire CUT transfer. if (fileTransfer != null) { fileTransfer.onScreenUpdated(); } + if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) { + client.getInputProcessor().setKeyboardLocked(false); + } terminalPanel.repaint(); statusBar.updateStatus(); }); diff --git a/j3270/src/main/java/org/pubvm/j3270/ft/FileTransfer.java b/j3270/src/main/java/org/pubvm/j3270/ft/FileTransfer.java index 02cbb33..affed6f 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ft/FileTransfer.java +++ b/j3270/src/main/java/org/pubvm/j3270/ft/FileTransfer.java @@ -22,6 +22,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { private static final Logger log = Logger.getLogger(FileTransfer.class.getName()); + public enum FTMode { + UNKNOWN, CUT, DFT + } + public interface FileTransferCallback { void onTransferStarted(); void onTransferRunning(); @@ -36,6 +40,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { private FTConfig currentConfig; private File localFile; private FTState state = FTState.NONE; + private FTMode activeMode = FTMode.UNKNOWN; private FTCut cutHandler; private FTDft dftHandler; @@ -64,6 +69,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { this.currentConfig = config; this.localFile = new File(config.getLocalFilename()); + this.activeMode = FTMode.UNKNOWN; // Check overwrite if (config.isReceive() && !config.isAppend() && !config.isOverwrite()) { @@ -83,14 +89,6 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { client.getDataStreamProcessor().setFTDft(dftHandler); } - try { - cutHandler.initTransfer(localFile); - dftHandler.initTransfer(localFile); - } catch (IOException e) { - cleanupHandlers(false); - return "Failed to open local file: " + e.getMessage(); - } - // Build and type the IND$FILE command String command = config.buildCommand(); log.info("Starting IND$FILE transfer with command: " + command); @@ -130,9 +128,19 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { } } + /** Check whether a file transfer is currently active. */ + public boolean isTransferActive() { + return state != FTState.NONE; + } + + public FTMode getActiveMode() { + return activeMode; + } + /** Must be called after every screen update to drive CUT mode. */ public void onScreenUpdated() { - if (state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT) { + if ((activeMode == FTMode.CUT || activeMode == FTMode.UNKNOWN) && + (state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT)) { cutHandler.processScreenUpdate(); } } @@ -179,6 +187,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { boolean success = (errorMessage == null); cleanupHandlers(success); setState(FTState.NONE); + activeMode = FTMode.UNKNOWN; currentConfig = null; } @@ -186,6 +195,32 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { @Override public void onTransferRunning() { + // Mode detection check + StackTraceElement[] st = Thread.currentThread().getStackTrace(); + boolean isDft = false; + for (StackTraceElement elem : st) { + if (elem.getClassName().contains("FTDft")) { + isDft = true; + break; + } + } + + if (activeMode == FTMode.UNKNOWN) { + activeMode = isDft ? FTMode.DFT : FTMode.CUT; + log.info("FT mode established: " + activeMode); + try { + if (activeMode == FTMode.DFT) { + dftHandler.initTransfer(localFile); + } else { + cutHandler.initTransfer(localFile); + } + } catch (IOException e) { + log.warning("Failed to open local file for " + activeMode + ": " + e.getMessage()); + onTransferAborted("Failed to open local file: " + e.getMessage()); + return; + } + } + cancelTimeout(); setState(FTState.RUNNING); SwingUtilities.invokeLater(callback::onTransferRunning); @@ -193,7 +228,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener { @Override public void onTransferComplete(String errorMessage) { - completeTransfer(null); + completeTransfer(errorMessage); SwingUtilities.invokeLater(() -> { if (errorMessage == null) { callback.onTransferComplete("Transfer complete."); diff --git a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java index 06d879d..06119f2 100644 --- a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java +++ b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java @@ -51,6 +51,7 @@ public class Telnet3270Client { // Wire up the output sender: DSProcessor -> FSM -> TelnetConnection dsProcessor.setOutputSender(fsm::send3270Data); + dsProcessor.setInputProcessor(inputProcessor); } /** diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java index b2ecec3..8acac23 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -34,6 +34,9 @@ public class DataStreamProcessor { // File Transfer DFT handler private org.lib3270j.ft.FTDft ftDft; + // Input processor reference to manage keyboard locking state + private org.lib3270j.input.InputProcessor inputProcessor; + /** Functional interface for sending output back through the telnet stack. */ @FunctionalInterface public interface OutputSender { @@ -56,6 +59,10 @@ public class DataStreamProcessor { this.ftDft = ftDft; } + public void setInputProcessor(org.lib3270j.input.InputProcessor inputProcessor) { + this.inputProcessor = inputProcessor; + } + public void addScreenUpdateListener(ScreenUpdateListener l) { screenListeners.add(l); } @@ -231,6 +238,12 @@ public class DataStreamProcessor { log.fine("WCC: " + String.format("0x%02x", wcc) + " reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt); + if (kbdRestore || inputProcessor != null) { + if (inputProcessor != null) { + inputProcessor.setKeyboardLocked(false); + } + } + if (resetMdt) { resetAllMDT(); } @@ -471,6 +484,10 @@ public class DataStreamProcessor { if (!faIsProtected(faVal & 0xFF)) { ea.ec = 0; ea.ucs4 = 0; + ea.fg = 0; + ea.bg = 0; + ea.gr = 0; + ea.cs = 0; } } baddr = (baddr + 1) % size; @@ -614,6 +631,10 @@ public class DataStreamProcessor { // ========== Read Modified ========== private void processReadModified(boolean all) { + if (ftDft != null) { + ftDft.readModified(); + } + outputPos = 0; int aid = AID_NO; // Last AID diff --git a/lib3270j/src/main/java/org/lib3270j/ft/FTConstants.java b/lib3270j/src/main/java/org/lib3270j/ft/FTConstants.java index 62d12ec..a10a3e3 100644 --- a/lib3270j/src/main/java/org/lib3270j/ft/FTConstants.java +++ b/lib3270j/src/main/java/org/lib3270j/ft/FTConstants.java @@ -183,6 +183,12 @@ public final class FTConstants { * Decode a CUT-mode base-64 encoded integer from EBCDIC. * Converts a table6-encoded EBCDIC character to its 6-bit value. */ + public static int from6(int ebcdicByte, org.lib3270j.charset.EbcdicTranslator translator) { + char ascii = translator.ebcdicToUnicode(ebcdicByte & 0xFF); + int idx = TABLE6.indexOf(ascii); + return idx >= 0 ? idx : 0; + } + public static int from6(int ebcdicByte) { // First convert EBCDIC to ASCII via IND$FILE's table int ascii = FT2ASC[ebcdicByte & 0xFF]; @@ -193,6 +199,12 @@ public final class FTConstants { /** * Encode a 6-bit value into a table6-encoded EBCDIC character. */ + public static int to6(int value, org.lib3270j.charset.EbcdicTranslator translator) { + char ascii = TABLE6.charAt(value & 0x3F); + int ebc = translator.unicodeToEbcdic(ascii); + return ebc >= 0 ? ebc : ASC2FT[ascii & 0xFF]; + } + public static int to6(int value) { char ascii = TABLE6.charAt(value & 0x3F); return ASC2FT[ascii & 0xFF]; diff --git a/lib3270j/src/main/java/org/lib3270j/ft/FTCut.java b/lib3270j/src/main/java/org/lib3270j/ft/FTCut.java index 8733608..2001f12 100644 --- a/lib3270j/src/main/java/org/lib3270j/ft/FTCut.java +++ b/lib3270j/src/main/java/org/lib3270j/ft/FTCut.java @@ -7,6 +7,7 @@ import static org.lib3270j.ft.FTConstants.*; import static org.lib3270j.protocol.DS3270Constants.*; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.logging.Logger; /** @@ -49,10 +50,10 @@ public class FTCut { private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?"; - private static final int SELECTOR_0 = 0x5E; // ';' - private static final int SELECTOR_1 = 0x7E; // '=' - private static final int SELECTOR_2 = 0x5C; // '*' - private static final int SELECTOR_3 = 0x7D; // '\'' + private static final int SELECTOR_0 = 0x5E; // ';' (EBCDIC) + private static final int SELECTOR_1 = 0x7E; // '=' (EBCDIC) + private static final int SELECTOR_2 = 0x5C; // '*' (EBCDIC) + private static final int SELECTOR_3 = 0x7D; // '\'' (EBCDIC) private static final int[] XLATE_0 = { 0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2, @@ -110,6 +111,55 @@ public class FTCut { this.listener = listener; } + /** + * Initialize CUT mode with active streams. + */ + public void initTransfer(InputStream in, OutputStream out) { + this.inputStream = in; + this.outputStream = out; + resetState(); + } + + /** + * Initialize CUT mode for a new transfer. + */ + public void initTransfer(File localFile) throws IOException { + FTConfig config = listener.getConfig(); + resetState(); + + if (config.isReceive()) { + boolean append = config.isAppend(); + outputStream = new FileOutputStream(localFile, append); + inputStream = null; + } else { + inputStream = new FileInputStream(localFile); + outputStream = null; + } + } + + private void resetState() { + xferInProgress = false; + expandedLength = 0; + quadrant = -1; + xlateBuffered = 0; + xlateBufIx = 0; + cutEof = false; + lastCr = false; + } + + /** + * Clean up CUT mode resources. + */ + public void cleanup() { + xferInProgress = false; + try { + if (inputStream != null) { inputStream.close(); inputStream = null; } + if (outputStream != null) { outputStream.close(); outputStream = null; } + } catch (IOException e) { + log.warning("Error closing file: " + e.getMessage()); + } + } + /** * Process a CUT-mode screen update. * Called by the transfer coordinator when the screen changes during a transfer. @@ -120,7 +170,12 @@ public class FTCut { // CUT frames MUST have a skip field attribute at O_SF (1919) byte sfAttr = screen.getCellFAByte(O_SF); if (sfAttr == 0 || !isSkip(sfAttr)) { - // Not a CUT frame (likely local echo or intermediate screen), ignore + // Not a CUT frame (likely local echo, menu return, or intermediate screen) + if (xferInProgress) { + log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted."); + xferInProgress = false; + listener.onTransferAborted("Host aborted transfer (returned to menu)"); + } return; } @@ -149,42 +204,6 @@ public class FTCut { return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0; } - /** - * Initialize CUT mode for a new transfer. - */ - public void initTransfer(File localFile) throws IOException { - FTConfig config = listener.getConfig(); - xferInProgress = false; - expandedLength = 0; - quadrant = -1; - xlateBuffered = 0; - xlateBufIx = 0; - cutEof = false; - lastCr = false; - - if (config.isReceive()) { - boolean append = config.isAppend(); - outputStream = new FileOutputStream(localFile, append); - inputStream = null; - } else { - inputStream = new FileInputStream(localFile); - outputStream = null; - } - } - - /** - * Clean up CUT mode resources. - */ - public void cleanup() { - xferInProgress = false; - try { - if (inputStream != null) { inputStream.close(); inputStream = null; } - if (outputStream != null) { outputStream.close(); outputStream = null; } - } catch (IOException e) { - log.warning("Error closing file: " + e.getMessage()); - } - } - // ========== Control Code Processing ========== private void cutControlCode() { @@ -218,7 +237,6 @@ public class FTCut { xferInProgress = false; cutAck(); - // Extract error message from the host (positions 4-83) String msg = extractHostMessage(); listener.onTransferAborted(msg); break; @@ -242,7 +260,6 @@ public class FTCut { char ch = translator.ebcdicToUnicode(ebc); sb.append(ch); } - // Trim trailing spaces and '$' String msg = sb.toString().stripTrailing(); if (msg.endsWith("$")) { msg = msg.substring(0, msg.length() - 1).stripTrailing(); @@ -252,12 +269,10 @@ public class FTCut { // ========== Data Request (Upload) ========== - /** - * Process a data request from the host (upload: send data to host). - */ private void cutDataRequest() { - int seq = screen.getCellEC(O_DR_FRAME_SEQ); - log.fine("CUT: DATA_REQUEST seq=" + from6(seq)); + int seqEbc = screen.getCellEC(O_DR_FRAME_SEQ); + int seq = FTConstants.from6(seqEbc, translator); + log.fine("CUT: DATA_REQUEST seq=" + seq); if (listener.getCurrentState() == FTState.ABORT_WAIT) { cutAbort("Transfer cancelled by user", SC_ABORT_FILE); @@ -266,7 +281,6 @@ public class FTCut { FTConfig config = listener.getConfig(); - // Read data from local file into screen buffer int count = 0; try { while (count < O_UP_MAX && !cutEof) { @@ -284,32 +298,28 @@ public class FTCut { return; } - // EOF with no data → send EOF marker if (count == 0 && cutEof) { screen.setCell(O_UP_DATA, EOF_DATA1); screen.setCell(O_UP_DATA + 1, EOF_DATA2); count = 2; } - // Compute frame fields - screen.setCell(O_UP_FRAME_SEQ, seq); + screen.setCell(O_UP_FRAME_SEQ, seqEbc); - // Checksum int cs = 0; for (int i = 0; i < count; i++) { cs ^= screen.getCellEC(O_UP_DATA + i); } - screen.setCell(O_UP_CSUM, to6(cs)); - screen.setCell(O_UP_LEN, to6((count >> 6) & 0x3F)); - screen.setCell(O_UP_LEN + 1, to6(count & 0x3F)); + screen.setCell(O_UP_CSUM, FTConstants.to6(cs & 0x3F, translator)); + screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator)); + screen.setCell(O_UP_LEN + 1, FTConstants.to6(count & 0x3F, translator)); - // Hide the data field (change SF attribute to zero intensity) + // Hide data field by setting zero intensity on field attribute byte attr = screen.getCellFAByte(O_DR_SF); - attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL); + attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL | FA_MODIFY); screen.setCellFA(O_DR_SF, attr); - // Send it - log.fine("CUT: > DATA seq=" + from6(seq) + " len=" + count); + log.fine("CUT: > DATA seq=" + seq + " len=" + count); expandedLength += count; listener.onBytesTransferred(expandedLength); input.sendAidForFT(AID_ENTER); @@ -317,9 +327,6 @@ public class FTCut { // ========== Data (Download) ========== - /** - * Process data from the host (download: receive data from host). - */ private void cutData() { log.fine("CUT: DATA"); @@ -330,9 +337,8 @@ public class FTCut { FTConfig config = listener.getConfig(); - // Extract raw data - int rawLength = (from6(screen.getCellEC(O_DT_LEN)) << 6) | - from6(screen.getCellEC(O_DT_LEN + 1)); + int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) | + FTConstants.from6(screen.getCellEC(O_DT_LEN + 1), translator); if (rawLength > O_RESPONSE - O_DT_DATA) { cutAbort("Oversized CUT data frame", SC_ABORT_XMIT); @@ -344,7 +350,6 @@ public class FTCut { rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i); } - // Check for EOF marker if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 && (rawData[1] & 0xFF) == EOF_DATA2) { log.fine("CUT: EOF marker received"); @@ -352,7 +357,6 @@ public class FTCut { return; } - // Convert and write to local file try { byte[] converted = convertDownload(rawData, rawLength, config); if (outputStream != null) { @@ -386,31 +390,20 @@ public class FTCut { private void cutAbort(String message, int reason) { log.warning("CUT: ABORT — " + message); - // Write abort frame to the response area screen.setCell(RO_FRAME_TYPE, RFT_CONTROL_CODE); screen.setCell(RO_FRAME_SEQ, screen.getCellEC(O_DT_FRAME_SEQ)); screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF); screen.setCell(RO_REASON_CODE + 1, reason & 0xFF); - // Send PF2 to signal abort input.sendAidForFT(AID_PF2); - listener.onTransferAborted(message); } // ========== Character Translation ========== /** - * Convert received EBCDIC data to local format. - * Handles ASCII mode (with optional CR processing and remapping) - * and binary mode (raw passthrough). - * - * The quadrant tables decode wire bytes into IND$FILE's "pseudo-ASCII" - * representation. For the remap path, control codes (< 0x20 and - * 0x80-0x9E) are treated as direct Unicode codepoints, while printable - * characters are re-mapped through ASC2FT (inverting IND$FILE's built-in - * EBCDIC→ASCII table) back to real EBCDIC, then to Unicode. - * This matches x3270's upload_convert() logic. + * Convert received CUT EBCDIC data to local format. + * Matching x3270 upload_convert logic. */ private byte[] convertDownload(byte[] rawData, int length, FTConfig config) throws IOException { @@ -421,81 +414,59 @@ public class FTCut { while (true) { if (quadrant < 0) { - // Find the quadrant for (quadrant = 0; quadrant < 4; quadrant++) { if (c == SELECTORS[quadrant]) break; } if (quadrant >= 4) { throw new IOException("CUT conversion error (quadrant selector not found)"); } - break; // continue outer loop + break; } if (c < 0x40 || c > 0xF9) { throw new IOException("CUT conversion error (data out of bounds)"); } - char asciiChar = translator.ebcdicToUnicode(c); + char asciiChar = (char) FT2ASC[c & 0xFF]; int ix = ALPHAS.indexOf(asciiChar); if (ix < 0) { - // Try a different quadrant quadrant = -1; - continue; // retry loop + continue; } - if (quadrant != 2 && c != 0xC1 && QUADS[quadrant][ix] == 0) { - // Try a different quadrant + if (!(quadrant == 2 && c == 0xC1) && QUADS[quadrant][ix] == 0) { quadrant = -1; - continue; // retry + continue; } - // Map the character — this produces IND$FILE's pseudo-ASCII byte int decoded = QUADS[quadrant][ix]; if (config.isAscii() && config.isCrFlag() && (decoded == 0x0D || decoded == 0x1A)) { - break; // Ignore bare CR and EOF markers in ASCII stream + break; } if (!config.isAscii() || !config.isRemapFlag()) { - // Binary or ASCII-no-remap: emit the decoded byte as-is out.write(decoded); break; } - /* - * ASCII with remap: invert IND$FILE's EBCDIC→ASCII translation. - * - * Control codes (< 0x20) and high control range (0x80-0x9E, - * except 0x9F) are treated as direct Unicode codepoints. - * IND$FILE maps EBCDIC 0xE1 to pseudo-ASCII 0x9F, so - * 0xFF is the special marker that maps back to U+009F. - * Everything else is a printable character that needs - * the ASC2FT→ebcdicToUnicode remap chain. - */ if (decoded < 0x20 || (decoded >= 0x80 && decoded < 0xA0 && decoded != 0x9F)) { - // Control code — emit as its Unicode codepoint directly - out.write(String.valueOf((char) decoded).getBytes( - java.nio.charset.StandardCharsets.UTF_8)); + out.write(String.valueOf((char) decoded).getBytes(StandardCharsets.UTF_8)); } else if (decoded == 0xFF) { - // Special case: 0xFF → U+009F - out.write(String.valueOf((char) 0x9F).getBytes( - java.nio.charset.StandardCharsets.UTF_8)); + out.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8)); } else { - // Displayable character: invert IND$FILE's table - int ebc = FTConstants.ASC2FT[decoded & 0xFF]; + int ebc = ASC2FT[decoded & 0xFF]; char unicodeChar = translator.ebcdicToUnicode(ebc); - out.write(String.valueOf(unicodeChar).getBytes( - java.nio.charset.StandardCharsets.UTF_8)); + out.write(String.valueOf(unicodeChar).getBytes(StandardCharsets.UTF_8)); } break; - } // end while(true) chunk + } } return out.toByteArray(); } private int xlateGetc(FTConfig config) throws IOException { - // Return buffered data first if (xlateBuffered > 0) { int r = xlateBuf[xlateBufIx++]; xlateBuffered--; @@ -509,7 +480,7 @@ public class FTCut { int localByte = c & 0xFF; int nc = 0; - int[] cbuf = new int[4]; // max 4 bytes (2 for \r + 2 for \n if quadrant encoded) + int[] cbuf = new int[4]; if (config.isAscii()) { if (config.isCrFlag() && !lastCr && localByte == '\n') { @@ -532,7 +503,6 @@ public class FTCut { private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) { int ebc; if (localByte == 0) { - // Nulls are special in the 'OTHER_2' quadrant if (quadrant != 2) { quadrant = 2; // OTHER_2 cbuf[offset] = SELECTORS[quadrant]; @@ -545,25 +515,25 @@ public class FTCut { } if (!config.isAscii() || !config.isRemapFlag()) { - // Binary or Ascii without remap: treat byte directly ebc = localByte & 0xFF; } else { - // Ascii with remap: locally read ASCII translates to actual EBCDIC, then maps to IND$FILE's pseudo-ASCII int standardEbc = translator.unicodeToEbcdic((char) localByte); if (standardEbc < 0) { - standardEbc = 0x40; // Space as fallback + standardEbc = 0x40; } - ebc = org.lib3270j.ft.FTConstants.FT2ASC[standardEbc & 0xFF]; + ebc = FTConstants.FT2ASC[standardEbc & 0xFF]; } return storeUpload(ebc, cbuf, offset); } - private int storeUpload(int ebc, int[] obBuf, int offset) { + private int storeUpload(int pseudoAsciiByte, int[] obBuf, int offset) { if (quadrant >= 0) { for (int i = 0; i < 77; i++) { - if (QUADS[quadrant][i] == ebc) { - obBuf[offset] = translator.unicodeToEbcdic(ALPHAS.charAt(i)); + if (QUADS[quadrant][i] == pseudoAsciiByte) { + char ch = ALPHAS.charAt(i); + int ebc = translator.unicodeToEbcdic(ch); + obBuf[offset] = ebc >= 0 ? ebc : 0x40; return 1; } } @@ -572,16 +542,18 @@ public class FTCut { for (quadrant = 0; quadrant < 4; quadrant++) { if (quadrant == oq) continue; for (int i = 0; i < 77; i++) { - if (QUADS[quadrant][i] == ebc) { + if (QUADS[quadrant][i] == pseudoAsciiByte) { + char ch = ALPHAS.charAt(i); + int ebc = translator.unicodeToEbcdic(ch); obBuf[offset] = SELECTORS[quadrant]; - obBuf[offset + 1] = translator.unicodeToEbcdic(ALPHAS.charAt(i)); + obBuf[offset + 1] = ebc >= 0 ? ebc : 0x40; return 2; } } } quadrant = -1; - // Fallback safety measure - obBuf[offset] = translator.unicodeToEbcdic('?'); + int questionEbc = translator.unicodeToEbcdic('?'); + obBuf[offset] = questionEbc >= 0 ? questionEbc : 0x6F; return 1; } } diff --git a/lib3270j/src/main/java/org/lib3270j/ft/FTDft.java b/lib3270j/src/main/java/org/lib3270j/ft/FTDft.java index 9aa5c6a..bc1079e 100644 --- a/lib3270j/src/main/java/org/lib3270j/ft/FTDft.java +++ b/lib3270j/src/main/java/org/lib3270j/ft/FTDft.java @@ -5,6 +5,7 @@ import org.lib3270j.charset.EbcdicTranslator; import static org.lib3270j.ft.FTConstants.*; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.logging.Logger; /** @@ -17,6 +18,9 @@ public class FTDft { private static final Logger log = Logger.getLogger(FTDft.class.getName()); + private static final String OPEN_MSG = "FT:MSG"; + private static final String END_TRANSFER = "TRANS03"; + /** Callback for transfer events (shared interface with CUT) */ public interface FTDftListener { void onTransferRunning(); @@ -34,7 +38,7 @@ public class FTDft { private final FTDftListener listener; // DFT state - private long recnum = 0; + private long recnum = 1; private boolean dftEof = false; private boolean messageFlag = false; private long bytesTransferred = 0; @@ -56,17 +60,20 @@ public class FTDft { } /** - * Initialize DFT mode for a new transfer. + * Initialize DFT mode with active streams. + */ + public void initTransfer(InputStream in, OutputStream out) { + this.inputStream = in; + this.outputStream = out; + resetState(); + } + + /** + * Initialize DFT mode for a new transfer from file. */ public void initTransfer(File localFile) throws IOException { FTConfig config = listener.getConfig(); - recnum = 0; - dftEof = false; - messageFlag = false; - bytesTransferred = 0; - dftSaveBuf = null; - dftSaveBufLen = 0; - lastCr = false; + resetState(); if (config.isReceive()) { outputStream = new FileOutputStream(localFile, config.isAppend()); @@ -77,6 +84,16 @@ public class FTDft { } } + private void resetState() { + recnum = 1; + dftEof = false; + messageFlag = false; + bytesTransferred = 0; + dftSaveBuf = null; + dftSaveBufLen = 0; + lastCr = false; + } + /** * Clean up DFT mode resources. */ @@ -115,7 +132,7 @@ public class FTDft { switch (requestCode) { case TR_OPEN_REQ: - dftOpenRequest(); + dftOpenRequest(data, offset, length); break; case TR_INSERT_REQ: dftInsertRequest(data, payloadStart, offset + length - payloadStart); @@ -142,11 +159,53 @@ public class FTDft { // ========== Open Request ========== - private void dftOpenRequest() { - log.fine("DFT: Open"); - listener.onTransferRunning(); - // Send acknowledgement - dftDataAck(); + private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) { + log.fine("DFT: Open request"); + + // Parse open request payload matching x3270 + // sfLength is the 2-byte length value at sfOffset + int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF); + String nameBuf = ""; + + if (sfLenVal == 0x23 && sfOffset + 3 + 25 <= data.length) { + nameBuf = extractName(data, sfOffset + 3 + 25, 7); + } else if (sfLenVal == 0x29 && sfOffset + 3 + 31 <= data.length) { + nameBuf = extractName(data, sfOffset + 3 + 31, 7); + } + + if (OPEN_MSG.equalsIgnoreCase(nameBuf)) { + messageFlag = true; + log.info("DFT: Open request for message stream"); + } else { + messageFlag = false; + listener.onTransferRunning(); + } + + dftEof = false; + recnum = 1; + + // Acknowledge Open matching x3270 (SF_TRANSFER_DATA + 0x0009) + dftOpenAck(); + } + + private String extractName(byte[] data, int start, int maxLen) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < maxLen && (start + i) < data.length; i++) { + int b = data[start + i] & 0xFF; + if (b == 0) break; + char ch = translator.ebcdicToUnicode(b); + sb.append(ch); + } + return sb.toString().trim(); + } + + private void dftOpenAck() { + ByteArrayOutputStream out = new ByteArrayOutputStream(6); + out.write(AID_SF); + out.write(0); out.write(5); // SF length + out.write(SF_TRANSFER_DATA); + out.write(0); out.write(9); // OpenAck response code 0x0009 + input.sendStructuredFieldData(out.toByteArray()); } // ========== Insert Request (host sending data for download) ========== @@ -159,12 +218,11 @@ public class FTDft { private void dftDataInsert(byte[] data, int offset, int length) { FTConfig config = listener.getConfig(); - if (listener.getCurrentState() == FTState.ABORT_WAIT) { + if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) { dftAbort("Transfer cancelled by user", TR_DATA_INSERT); return; } - // Parse the SF payload to find data // Skip the 2-byte request code int pos = offset + 2; int end = offset + length; @@ -174,13 +232,19 @@ public class FTDft { int headerCode = data[pos] & 0xFF; if (headerCode == TR_BEGIN_DATA) { - // Next 2 bytes are data length (including the 3-byte header) if (pos + 3 > end) break; int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF); - int actualDataLen = dataLen - 3; // subtract header + int actualDataLen = dataLen - 3; pos += 3; if (actualDataLen > 0 && pos + actualDataLen <= end) { + if (messageFlag) { + // Handle message payload from host + dftDataAck(); + handleHostMessage(data, pos, actualDataLen); + return; + } + try { writeDownloadData(data, pos, actualDataLen, config); bytesTransferred += actualDataLen; @@ -192,74 +256,84 @@ public class FTDft { } pos += Math.max(0, actualDataLen); } else if (pos + 1 < end) { - // Skip other headers (2-byte code + length-based skip) - // Most headers are 4-6 bytes, but we just skip based on known patterns int hdrCode16 = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF); if (hdrCode16 == TR_RECNUM_HDR) { - pos += 6; // 2-byte code + 4-byte record number + pos += 6; } else if (hdrCode16 == TR_NOT_COMPRESSED) { pos += 2; } else { - pos += 2; // skip unknown 2-byte header + pos += 2; } } else { pos++; } } - // Send acknowledgement + // Send acknowledgement for file data dftDataAck(); } + private void handleHostMessage(byte[] data, int offset, int length) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + int b = data[offset + i] & 0xFF; + if (b == 0 || b == '$') break; + char ch = translator.ebcdicToUnicode(b); + sb.append(ch); + } + String msg = sb.toString().trim(); + log.info("DFT message: " + msg); + + if (msg.startsWith(END_TRANSFER)) { + listener.onTransferComplete(null); + } else if (listener.getCurrentState() == FTState.ABORT_SENT) { + listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg); + } else { + listener.onTransferComplete(msg); + } + } + /** * Write download data to the local file, handling ASCII conversion. + * Matching x3270 upload_convert logic. */ private void writeDownloadData(byte[] data, int offset, int length, FTConfig config) throws IOException { if (outputStream == null) return; if (!config.isAscii()) { - // Binary: write raw outputStream.write(data, offset, length); return; } - // ASCII mode with optional CR stripping and remapping for (int i = 0; i < length; i++) { int b = data[offset + i] & 0xFF; - if (config.isRemapFlag()) { - // Use IND$FILE's EBCDIC→ASCII table - int ascii = FT2ASC[b]; + if (config.isCrFlag() && (b == '\r' || b == 0x1A)) { + continue; // Strip CR and EOF ^Z + } - if (config.isCrFlag()) { - if (ascii == '\r') { - lastCr = true; - continue; - } - if (lastCr) { - lastCr = false; - if (ascii == '\n') { - outputStream.write('\n'); - continue; - } - outputStream.write('\r'); - } - } - outputStream.write(ascii); + if (!config.isRemapFlag()) { + outputStream.write(b); + continue; + } + + /* + * ASCII mode with remap: + * Host IND$FILE sends pseudo-ASCII byte b. + * Map pseudo-ASCII b to EBCDIC byte via ASC2FT[b], + * then convert EBCDIC byte to Unicode UTF-8 character. + */ + if (b < 0x20 || (b >= 0x80 && b < 0xA0 && b != 0x9F)) { + // Control code — write as Unicode directly + outputStream.write(String.valueOf((char) b).getBytes(StandardCharsets.UTF_8)); + } else if (b == 0xFF) { + // Special 0xFF -> U+009F + outputStream.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8)); } else { - // Standard EBCDIC→Unicode - char ch = translator.ebcdicToUnicode(b); - if (config.isCrFlag()) { - if (ch == '\r') { lastCr = true; continue; } - if (lastCr) { - lastCr = false; - if (ch == '\n') { outputStream.write('\n'); continue; } - outputStream.write('\r'); - } - } - outputStream.write(String.valueOf(ch).getBytes( - java.nio.charset.StandardCharsets.UTF_8)); + int ebc = ASC2FT[b & 0xFF]; + char ch = translator.ebcdicToUnicode(ebc); + outputStream.write(String.valueOf(ch).getBytes(StandardCharsets.UTF_8)); } } } @@ -276,7 +350,7 @@ public class FTDft { } int bufferSize = config.getDftBufferSize(); - int numbytes = bufferSize - 27; // reserve space for headers + int numbytes = bufferSize - 27; byte[] readBuf = new byte[numbytes]; int totalRead = 0; @@ -290,7 +364,6 @@ public class FTDft { } readBuf[totalRead++] = (byte) b; } else { - // Binary read if (inputStream == null) { dftEof = true; break; } int n = inputStream.read(readBuf, totalRead, numbytes - totalRead); if (n <= 0) { @@ -305,25 +378,20 @@ public class FTDft { return; } - // Build SF response ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize); + out.write(AID_SF); - out.write(AID_SF); // AID byte - - // Placeholder for SF length (2 bytes, filled in later) int sfLenPos = out.size(); out.write(0); out.write(0); - out.write(SF_TRANSFER_DATA); // SF type + out.write(SF_TRANSFER_DATA); if (totalRead > 0) { log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes"); - // TR_GET_REPLY out.write((TR_GET_REPLY >> 8) & 0xFF); out.write(TR_GET_REPLY & 0xFF); - // Record number header out.write((TR_RECNUM_HDR >> 8) & 0xFF); out.write(TR_RECNUM_HDR & 0xFF); out.write((int) ((recnum >> 24) & 0xFF)); @@ -332,56 +400,47 @@ public class FTDft { out.write((int) (recnum & 0xFF)); recnum++; - // Not compressed out.write((TR_NOT_COMPRESSED >> 8) & 0xFF); out.write(TR_NOT_COMPRESSED & 0xFF); - // Begin data out.write(TR_BEGIN_DATA); int dataFieldLen = totalRead + 5; out.write((dataFieldLen >> 8) & 0xFF); out.write(dataFieldLen & 0xFF); - // The actual data out.write(readBuf, 0, totalRead); bytesTransferred += totalRead; } else { log.fine("DFT: > GetReply EOF"); - // EOF reply out.write((TR_GET_REQ >> 8) & 0xFF); out.write(TR_ERROR_REPLY & 0xFF); - // Error header out.write((TR_ERROR_HDR >> 8) & 0xFF); out.write(TR_ERROR_HDR & 0xFF); - // EOF error code out.write((TR_ERR_EOF >> 8) & 0xFF); out.write(TR_ERR_EOF & 0xFF); dftEof = true; } - // Set the SF length byte[] result = out.toByteArray(); - int sfLen = result.length - 1; // exclude AID byte + int sfLen = result.length - 1; result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF); result[sfLenPos + 1] = (byte) (sfLen & 0xFF); - // Save for potential Read Modified retransmit dftSaveBuf = result.clone(); dftSaveBufLen = result.length; - // Send it input.sendStructuredFieldData(result); listener.onBytesTransferred(bytesTransferred); } /** - * Read a byte from the local file in ASCII mode with CR expansion and remapping. - * Returns -1 for EOF. + * Read a byte from local file for upload, handling ASCII conversion and remapping. + * Matching x3270 dft_ascii_read logic. */ private int dftAsciiRead(FTConfig config) throws IOException { if (inputStream == null) return -1; @@ -389,30 +448,35 @@ public class FTDft { int c = inputStream.read(); if (c == -1) return -1; - if (config.isRemapFlag()) { - // CR expansion: insert \r before \n - if (config.isCrFlag() && !lastCr && c == '\n') { - // We need to return \r now and buffer \n - lastCr = false; - // Can't easily buffer here, so return \r and push \n back - // Actually, let's handle this inline - // Return the CR equivalent, then handle NL next call - return ASC2FT['\r' & 0xFF]; - // Note: the \n will be read on next call naturally - } - lastCr = (c == '\r'); - return ASC2FT[c & 0xFF]; - } else { - // No remap — convert to standard EBCDIC - if (config.isCrFlag() && !lastCr && c == '\n') { - lastCr = false; - int ebc = translator.unicodeToEbcdic('\r'); - return ebc >= 0 ? ebc : 0x0D; - } - lastCr = (c == '\r'); + if (config.isCrFlag() && !lastCr && c == '\n') { + lastCr = false; + // Expand \n to \r\n: return \r byte now + int rEbc = translator.unicodeToEbcdic('\r'); + if (rEbc < 0) rEbc = 0x0D; + return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc; + } + lastCr = (c == '\r'); + + if (!config.isRemapFlag()) { int ebc = translator.unicodeToEbcdic((char) c); return ebc >= 0 ? ebc : 0x40; } + + /* + * ASCII mode with remap: + * Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc]. + */ + int ebc; + if (c < 0x20 || (c >= 0x80 && c < 0x9F)) { + ebc = ASC2FT[c & 0xFF]; + } else if (c == 0x9F) { + ebc = 0xFF; + } else { + ebc = translator.unicodeToEbcdic((char) c); + } + if (ebc < 0) ebc = 0x40; + + return FT2ASC[ebc & 0xFF]; } // ========== Close Request ========== @@ -420,16 +484,10 @@ public class FTDft { private void dftCloseRequest() { log.fine("DFT: Close"); - // Send close acknowledgement ByteArrayOutputStream out = new ByteArrayOutputStream(6); out.write(AID_SF); - - // SF length out.write(0); out.write(5); - out.write(SF_TRANSFER_DATA); - - // TR_CLOSE_REPLY out.write((TR_CLOSE_REPLY >> 8) & 0xFF); out.write(TR_CLOSE_REPLY & 0xFF); @@ -441,13 +499,8 @@ public class FTDft { private void dftDataAck() { ByteArrayOutputStream out = new ByteArrayOutputStream(6); out.write(AID_SF); - - // SF length out.write(0); out.write(5); - out.write(SF_TRANSFER_DATA); - - // TR_NORMAL_REPLY out.write((TR_NORMAL_REPLY >> 8) & 0xFF); out.write(TR_NORMAL_REPLY & 0xFF); @@ -461,21 +514,12 @@ public class FTDft { ByteArrayOutputStream out = new ByteArrayOutputStream(10); out.write(AID_SF); - - // SF length out.write(0); out.write(9); - out.write(SF_TRANSFER_DATA); - - // Error reply code out.write((code >> 8) & 0xFF); out.write(TR_ERROR_REPLY & 0xFF); - - // Error header out.write((TR_ERROR_HDR >> 8) & 0xFF); out.write(TR_ERROR_HDR & 0xFF); - - // Command failed out.write((TR_ERR_CMDFAIL >> 8) & 0xFF); out.write(TR_ERR_CMDFAIL & 0xFF); diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java index 0d13427..25f2777 100644 --- a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -30,8 +30,46 @@ public class InputProcessor { this.fsm = fsm; } + public enum OiaStatus { + NOT_CONNECTED("OFFLINE"), + X_SYSTEM("X SYSTEM"), + X_PROT("X PROT"), + READY("READY"); + + private final String label; + OiaStatus(String label) { this.label = label; } + public String getLabel() { return label; } + } + + public OiaStatus getOiaStatus() { + if (fsm == null || fsm.getConnectionState() == null || !fsm.getConnectionState().isConnected()) { + return OiaStatus.NOT_CONNECTED; + } + if (keyboardLocked) { + return OiaStatus.X_SYSTEM; + } + return OiaStatus.READY; + } + + public interface LockStateListener { + void onLockStateChanged(boolean locked); + } + + private LockStateListener lockStateListener; + + public void setLockStateListener(LockStateListener listener) { + this.lockStateListener = listener; + } + public boolean isKeyboardLocked() { return keyboardLocked; } - public void setKeyboardLocked(boolean locked) { this.keyboardLocked = locked; } + public void setKeyboardLocked(boolean locked) { + if (this.keyboardLocked != locked) { + this.keyboardLocked = locked; + if (lockStateListener != null) { + lockStateListener.onLockStateChanged(locked); + } + } + } public boolean isInsertMode() { return insertMode; } public void setInsertMode(boolean insert) { this.insertMode = insert; } @@ -115,7 +153,7 @@ public class InputProcessor { if (keyboardLocked && aidCode != AID_CLEAR) return; lastAid = aidCode; - keyboardLocked = true; + setKeyboardLocked(true); if (aidCode == AID_CLEAR) { screen.clear(); @@ -149,8 +187,6 @@ public class InputProcessor { for (int i = 0; i < size; i++) { ExtendedAttribute ea = screen.getCell(i); if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) { - if (faIsProtected(ea.fa & 0xFF)) continue; - int fieldStart = (i + 1) % size; // First, collect field data and find last non-null byte @@ -169,14 +205,14 @@ public class InputProcessor { if (pos == fieldStart) break; } - // Only send if there's actual data (strip trailing nulls) - if (lastNonNull >= 0) { - out.write(ORDER_SBA); - byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); - out.write(addr[0] & 0xFF); - out.write(addr[1] & 0xFF); + // Always send SBA and address + out.write(ORDER_SBA); + byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); + out.write(addr[0] & 0xFF); + out.write(addr[1] & 0xFF); - // Write only up to the last non-null byte + // Only send data if there is any (strip trailing nulls) + if (lastNonNull >= 0) { byte[] allData = fieldData.toByteArray(); out.write(allData, 0, lastNonNull + 1); } @@ -260,6 +296,17 @@ public class InputProcessor { } public void eraseEof() { + if (!screen.isFormatted()) { + int addr = screen.getCursorAddress(); + int size = screen.getRows() * screen.getCols(); + for (int i = addr; i < size; i++) { + ExtendedAttribute ea = screen.getCell(i); + ea.ec = 0; + ea.ucs4 = 0; + } + screen.markAllChanged(); + return; + } int addr = screen.getCursorAddress(); int size = screen.getRows() * screen.getCols(); byte faVal = screen.getFieldAttributeAt(addr); @@ -282,6 +329,7 @@ public class InputProcessor { } public void deleteChar() { + if (!screen.isFormatted()) return; int addr = screen.getCursorAddress(); byte faVal = screen.getFieldAttributeAt(addr); if (faIsProtected(faVal & 0xFF)) return; @@ -316,7 +364,7 @@ public class InputProcessor { /** Reset (unlock keyboard, cancel insert mode). */ public void reset() { - keyboardLocked = false; + setKeyboardLocked(false); insertMode = false; } diff --git a/.gradle/8.5/dependencies-accessors/gc.properties b/nohup.out similarity index 100% rename from .gradle/8.5/dependencies-accessors/gc.properties rename to nohup.out diff --git a/settings.gradle b/settings.gradle index 27331d1..0d6bc4d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,4 @@ rootProject.name = 'j3270-project' include 'lib3270j' include 'j3270' +include 'a3270'