Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
d8acd713af
|
|||
|
dde5918da8
|
|||
|
03d5ec0fae
|
|||
|
f8d078a23b
|
|||
|
cf0c0999bf
|
|||
|
fac6ac8bfb
|
|||
|
daac11472d
|
|||
|
8d5672c708
|
|||
|
9918c823c2
|
|||
|
0201c7c277
|
|||
|
b61702a57c
|
|||
|
bd4c1f3040
|
|||
|
0d6ab31f0a
|
|||
|
1f0e36c561
|
|||
|
8286af19e6
|
|||
|
3d3a939ece
|
|||
|
d05f1a4c45
|
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Make scripts executable
|
||||
run: |
|
||||
chmod +x ./build_all.sh ./test_all.sh ./run.sh
|
||||
chmod +x ./setup.sh ./test_all.sh
|
||||
|
||||
- name: Build & Run Unit Tests
|
||||
run: |
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Make scripts executable
|
||||
run: |
|
||||
chmod +x ./build_all.sh ./test_all.sh ./run.sh
|
||||
chmod +x ./setup.sh ./test_all.sh
|
||||
|
||||
- name: Build & Run Unit Tests
|
||||
run: |
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Make scripts executable
|
||||
run: |
|
||||
chmod +x ./build_all.sh ./test_all.sh ./run.sh
|
||||
chmod +x ./setup.sh ./test_all.sh
|
||||
|
||||
- name: Build & Run Unit Tests
|
||||
run: |
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Make scripts executable
|
||||
run: |
|
||||
chmod +x ./build_all.sh ./test_all.sh ./run.sh
|
||||
chmod +x ./setup.sh ./test_all.sh
|
||||
|
||||
- name: Build & Run Unit Tests
|
||||
run: |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||
[-blue.svg)](https://adoptium.net)
|
||||
[]()
|
||||
[]()
|
||||
[](LICENSE)
|
||||
|
||||
An **x3270-aligned and IBM Host On-Demand (HoD) compatible IBM 3270 mainframe terminal emulator & protocol library** written in pure Java with zero third-party runtime dependencies. Designed for high fidelity and compatibility with IBM z/OS, z/VM, and 3270-based systems over TN3270 and TN3270E.
|
||||
@@ -12,10 +12,11 @@ An **x3270-aligned and IBM Host On-Demand (HoD) compatible IBM 3270 mainframe te
|
||||
|
||||
## 🚀 Quick Download & Run
|
||||
|
||||
### Latest Standalone JARs
|
||||
Ready-to-run executable JARs are built and tested across all three Java LTS baselines (**Java 11, Java 17, and Java 21**) by Gitea Actions:
|
||||
### Latest Standalone JARs & Maven Artifacts
|
||||
Ready-to-run executable JARs and standalone library packages are built and tested across all three Java LTS baselines (**Java 11, Java 17, and Java 21**) by Gitea Actions:
|
||||
|
||||
- **[Download Latest Tagged Release (`j3270.jar`)](https://git.hugfreevikings.wtf/rudi/j3270/releases)**
|
||||
- **Standalone Protocol Library (`lib3270j.jar`)**: Built by setup/build scripts and published to Maven (`haus.nightmare.j3270:lib3270j:1.2.4`).
|
||||
- **[Latest Nightly CI Artifacts](https://git.hugfreevikings.wtf/rudi/j3270/actions)**: Download standalone JARs tailored for each LTS runtime under the latest workflow run:
|
||||
- `j3270-java11-jar` (Java 11 LTS)
|
||||
- `j3270-java17-jar` (Java 17 LTS — primary Gitea runner baseline)
|
||||
@@ -61,8 +62,8 @@ java -jar j3270.jar -c config.ini
|
||||
# Enable protocol and diagnostic debug logging to j3270.log
|
||||
java -jar j3270.jar -d mainframe.example.com
|
||||
|
||||
# Launch directly from source runner script
|
||||
./run.sh
|
||||
# Launch directly from source setup script
|
||||
./setup.sh --run
|
||||
```
|
||||
|
||||
---
|
||||
@@ -105,7 +106,7 @@ java -jar j3270.jar -d mainframe.example.com
|
||||
- **Host Catalog Browser**: Interactive directory dialog to query, parse, and browse CMS and TSO host dataset catalogs.
|
||||
|
||||
### 🌍 Internationalization & Character Sets
|
||||
- **22+ Single-Byte EBCDIC Codepages**: US/Canada (037), Open Systems / z/OS Unix (1047), International (500, 1148 Euro), UK (285), Germany/Austria (273, 1141 Euro), France (297), Italy (280), Spain (284), Scandinavia (277, 278), Greece (875), Turkey (1026, 1155, 905), Eastern Europe (870), Iceland (871), Cyrillic (1025, 1123, 1154, 880), Hebrew (424, 803), Arabic (420), Thai (838, 1160).
|
||||
- **40+ EBCDIC Codepages & Euro Variants**: US/Canada (037), Open Systems / z/OS Unix (1047), International (500, 1148 Euro), UK (285), Germany/Austria (273, 1141 Euro), France (297), Italy (280), Spain (284), Scandinavia (277, 278), Greece (875), Turkey (1026, 1155, 905), Eastern Europe (870), Iceland (871), Cyrillic (1025, 1123, 1154, 880), Hebrew (424, 803), Arabic (420), Thai (838, 1160), Japanese SBCS (290, 1027), and 10 full Euro variants (1140–1149).
|
||||
- **Mixed DBCS Support**: Japanese Katakana (930), Japanese Latin (939), Simplified Chinese (935, 1388), Traditional Chinese (937, 1371), and Korean (933) with Shift-In (`0x0E`) / Shift-Out (`0x0F`) state handling.
|
||||
- **APL & Special Graphics**: APL keyboard mode, box-drawing characters, and Graphic Escape (`GE`) symbol translations.
|
||||
|
||||
@@ -125,7 +126,7 @@ java -jar j3270.jar -d mainframe.example.com
|
||||
- **Persistent Profiles**: Configuration profiles and session preferences saved in INI files (`j3270.ini`).
|
||||
|
||||
### 📦 Modular Zero-Dependency Core
|
||||
- **`lib3270j`**: Standalone Java protocol and terminal engine with **zero third-party dependencies** (`java.base`, `java.desktop` only).
|
||||
- **`lib3270j`**: Standalone Java protocol and terminal engine with **zero third-party dependencies** (`java.base`, `java.desktop` only). Distributed as standalone JAR (`build/lib3270j.jar`) and published Maven coordinate `haus.nightmare.j3270:lib3270j:1.2.4`.
|
||||
- **`j3270`**: Standalone desktop Swing terminal application.
|
||||
|
||||
---
|
||||
@@ -159,16 +160,12 @@ Self-contained setup scripts automatically locate an installed Java 11+ JDK (sea
|
||||
# Clean build and connect to host:
|
||||
.\setup.ps1 -Clean -Run -- mainframe.example.com 23 4
|
||||
```
|
||||
*(Also available as `.\setup.ps`)*
|
||||
|
||||
### Standalone Build (Advanced / CI)
|
||||
The project also provides low-level scripts and optional Gradle support:
|
||||
### Standalone Testing & CI
|
||||
Run automated tests directly via the standalone test runner:
|
||||
|
||||
```bash
|
||||
# Direct compile and package without diagnostics:
|
||||
sh ./build_all.sh
|
||||
|
||||
# Run all 539+ automated unit tests (~7s):
|
||||
# Run all 851 automated unit tests (~25s):
|
||||
sh ./test_all.sh
|
||||
```
|
||||
|
||||
@@ -186,6 +183,7 @@ All pushes and pull requests are verified across **Java 11, 17, and 21 LTS** run
|
||||
## 📜 Acknowledgements
|
||||
|
||||
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
||||
- IBM Bob — Architectural evaluation, Host On-Demand parity audit, and efficiency analysis
|
||||
- [Antigravity](https://antigravity.google/)
|
||||
- Claude Opus 4.6
|
||||
- Gemini 3.1 Pro
|
||||
|
||||
+6
-1
@@ -1,6 +1,6 @@
|
||||
allprojects {
|
||||
group = 'haus.nightmare.j3270'
|
||||
version = '0.1.0'
|
||||
version = '1.2.4'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -23,6 +23,11 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Javadoc).configureEach {
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Building j3270 Terminal Emulator ==="
|
||||
|
||||
# 1. Locate Java toolchain
|
||||
JAVAC_BIN=""
|
||||
JAR_BIN=""
|
||||
|
||||
if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/jar" ]; then
|
||||
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||
JAR_BIN="$JAVA_HOME/bin/jar"
|
||||
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
|
||||
if javac -version >/dev/null 2>&1; then
|
||||
JAVAC_BIN="$(command -v javac)"
|
||||
JAR_BIN="$(command -v jar)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
|
||||
if [ -x /usr/libexec/java_home ]; then
|
||||
MAC_JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null || true)"
|
||||
if [ -n "$MAC_JAVA_HOME" ] && [ -x "$MAC_JAVA_HOME/bin/javac" ] && [ -x "$MAC_JAVA_HOME/bin/jar" ]; then
|
||||
export JAVA_HOME="$MAC_JAVA_HOME"
|
||||
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||
JAR_BIN="$JAVA_HOME/bin/jar"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
|
||||
# Check standard system JDK installation locations
|
||||
for h in /usr/lib/jvm/default-java \
|
||||
/usr/lib/jvm/java-21-openjdk* \
|
||||
/usr/lib/jvm/java-17-openjdk* \
|
||||
/usr/lib/jvm/java-11-openjdk* \
|
||||
/Library/Java/JavaVirtualMachines/*/Contents/Home \
|
||||
/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home \
|
||||
/opt/homebrew/opt/openjdk; do
|
||||
if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then
|
||||
export JAVA_HOME="$h"
|
||||
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||
JAR_BIN="$JAVA_HOME/bin/jar"
|
||||
break
|
||||
fi
|
||||
done
|
||||
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..."
|
||||
find "$SCRIPT_DIR/lib3270j/src/main/java" -name "*.java" > "$BUILD_DIR/lib_sources.txt"
|
||||
if [ ! -s "$BUILD_DIR/lib_sources.txt" ]; then
|
||||
echo "ERROR: No sources found in lib3270j/src/main/java"
|
||||
exit 1
|
||||
fi
|
||||
"$JAVAC_BIN" -d "$BUILD_DIR/lib3270j" @"$BUILD_DIR/lib_sources.txt"
|
||||
|
||||
# 3. Compile j3270
|
||||
echo "Compiling j3270 application..."
|
||||
find "$SCRIPT_DIR/j3270/src/main/java" -name "*.java" > "$BUILD_DIR/app_sources.txt"
|
||||
if [ ! -s "$BUILD_DIR/app_sources.txt" ]; then
|
||||
echo "ERROR: No sources found in j3270/src/main/java"
|
||||
exit 1
|
||||
fi
|
||||
"$JAVAC_BIN" -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" @"$BUILD_DIR/app_sources.txt"
|
||||
|
||||
# Clean up temporary source lists
|
||||
rm -f "$BUILD_DIR/lib_sources.txt" "$BUILD_DIR/app_sources.txt"
|
||||
|
||||
# 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..."
|
||||
printf "Manifest-Version: 1.0\nMain-Class: haus.nightmare.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
|
||||
|
||||
"$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 "Executable JAR created: $BUILD_DIR/j3270.jar"
|
||||
ls -lh "$BUILD_DIR/j3270.jar"
|
||||
|
||||
# 5. Compile and run tests if requested
|
||||
if [ "$1" = "test" ] || [ "$1" = "check" ]; then
|
||||
echo "=== Running Tests ==="
|
||||
TEST_BUILD_DIR="$BUILD_DIR/test-classes"
|
||||
mkdir -p "$TEST_BUILD_DIR"
|
||||
JUNIT_CP=$(find "$HOME/.gradle/caches/modules-2/files-2.1/" -name "*.jar" 2>/dev/null | grep -E "junit|opentest|apiguardian" | tr "\n" ":" || true)
|
||||
FULL_CP="$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_CP"
|
||||
|
||||
find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
|
||||
if [ -d "$SCRIPT_DIR/j3270/src/test/java" ]; then
|
||||
find "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" >> "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -s "$BUILD_DIR/test_sources.txt" ] && [ -n "$JUNIT_CP" ]; then
|
||||
echo "Compiling tests..."
|
||||
"$JAVAC_BIN" -cp "$FULL_CP" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
|
||||
rm -f "$BUILD_DIR/test_sources.txt"
|
||||
|
||||
echo "Executing tests with JUnit Platform..."
|
||||
cat << 'EOF' > "$BUILD_DIR/TestRunner.java"
|
||||
import org.junit.platform.launcher.*;
|
||||
import org.junit.platform.launcher.core.*;
|
||||
import org.junit.platform.launcher.listeners.*;
|
||||
import org.junit.platform.engine.discovery.*;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public class TestRunner {
|
||||
public static void main(String[] args) {
|
||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
|
||||
.selectors(
|
||||
selectPackage("haus.nightmare")
|
||||
)
|
||||
.build();
|
||||
|
||||
Launcher launcher = LauncherFactory.create();
|
||||
SummaryGeneratingListener listener = new SummaryGeneratingListener();
|
||||
launcher.registerTestExecutionListeners(listener);
|
||||
launcher.execute(request);
|
||||
|
||||
TestExecutionSummary summary = listener.getSummary();
|
||||
summary.printTo(new PrintWriter(System.out));
|
||||
summary.printFailuresTo(new PrintWriter(System.err));
|
||||
|
||||
System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
"$JAVAC_BIN" -cp "$FULL_CP:$TEST_BUILD_DIR" -d "$BUILD_DIR" "$BUILD_DIR/TestRunner.java"
|
||||
JAVA_BIN_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/java}"
|
||||
JAVA_BIN_EXEC="${JAVA_BIN_EXEC:-java}"
|
||||
"$JAVA_BIN_EXEC" -cp "$FULL_CP:$TEST_BUILD_DIR:$BUILD_DIR" TestRunner
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This repository includes Gitea Actions workflows in `.gitea/workflows/build.yaml` to automatically:
|
||||
1. Compile and run the JUnit 5 test suite across the 3 LTS Java versions (Java 11, 17, and 21) via `test_all.sh`.
|
||||
2. Build standalone executables for each LTS version via `build_all.sh`.
|
||||
2. Build standalone executables for each LTS version via `setup.sh`.
|
||||
3. Upload `j3270-java11.jar`, `j3270-java17.jar`, and `j3270-java21.jar` as downloadable artifacts.
|
||||
4. Report test results and diagnostics per runtime.
|
||||
|
||||
@@ -98,12 +98,12 @@ This means Docker's bridge network inside the runner container cannot resolve th
|
||||
You can verify the entire build and test suite locally at any time without external tools:
|
||||
|
||||
```bash
|
||||
# Compile and run all 539 unit tests (~7s):
|
||||
# Compile and run all 851 unit tests (~25s):
|
||||
sh ./test_all.sh
|
||||
|
||||
# Build executable standalone JAR:
|
||||
sh ./build_all.sh
|
||||
./setup.sh
|
||||
|
||||
# Run the emulator:
|
||||
sh ./run.sh
|
||||
java -jar build/j3270.jar
|
||||
```
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,10 +15,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -27,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -112,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -170,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -203,15 +203,14 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+24
-34
@@ -13,16 +13,18 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
; j3270 Configuration File
|
||||
; Load with: java -jar j3270.jar -c j3270.ini
|
||||
; or: ./run.sh -c j3270.ini
|
||||
|
||||
[appearance]
|
||||
; UI font settings (menus, dialogs, status bar)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
/**
|
||||
* Version information for the j3270 application.
|
||||
*/
|
||||
public final class Version {
|
||||
|
||||
/**
|
||||
* Hardcoded fallback release version matching build.gradle and pom coordinates.
|
||||
*/
|
||||
public static final String VERSION = "1.2.4";
|
||||
|
||||
/**
|
||||
* Canonical application name.
|
||||
*/
|
||||
public static final String NAME = "j3270";
|
||||
|
||||
private Version() {}
|
||||
|
||||
/**
|
||||
* Retrieves the runtime version string of the j3270 application.
|
||||
* Inspects the package implementation version first (populated when loaded from a JAR),
|
||||
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
|
||||
*
|
||||
* @return the version string (e.g. "1.2.4")
|
||||
*/
|
||||
public static String getVersion() {
|
||||
Package pkg = Version.class.getPackage();
|
||||
if (pkg != null) {
|
||||
String implVer = pkg.getImplementationVersion();
|
||||
if (implVer != null && !implVer.isBlank()) {
|
||||
return implVer;
|
||||
}
|
||||
}
|
||||
return VERSION;
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,15 @@ public class Settings {
|
||||
return getAutoConnectAutoReconnect();
|
||||
}
|
||||
|
||||
public static String getDefaultHostType() {
|
||||
return prefs.get("defaultHostType", "TSO");
|
||||
}
|
||||
|
||||
public static void setDefaultHostType(String hostType) {
|
||||
prefs.put("defaultHostType", (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static void setAutoReconnect(boolean autoReconnect) {
|
||||
setAutoConnectAutoReconnect(autoReconnect);
|
||||
}
|
||||
@@ -738,6 +747,11 @@ public class Settings {
|
||||
case "dynamic_cols":
|
||||
setDynamicCols(Integer.parseInt(value));
|
||||
break;
|
||||
case "hostType":
|
||||
case "hosttype":
|
||||
case "host_type":
|
||||
setDefaultHostType(value);
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
|
||||
case "autoReconnect":
|
||||
@@ -945,6 +959,7 @@ public class Settings {
|
||||
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
||||
}
|
||||
w.println("codePage = " + getCodePage());
|
||||
w.println("hostType = " + getDefaultHostType());
|
||||
w.println("dynamicRows = " + getDynamicRows());
|
||||
w.println("dynamicCols = " + getDynamicCols());
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
|
||||
@@ -5,6 +5,7 @@ import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
@@ -28,6 +29,8 @@ public class FileTransferDialog extends JDialog {
|
||||
public String lrecl = "";
|
||||
public String blksize = "";
|
||||
public String space = "";
|
||||
public FTConfig.AllocationUnit units = FTConfig.AllocationUnit.TRACKS;
|
||||
public String dirBlocks = "";
|
||||
public String options = "";
|
||||
}
|
||||
|
||||
@@ -82,6 +85,8 @@ public class FileTransferDialog extends JDialog {
|
||||
private JTextField lreclField;
|
||||
private JTextField blksizeField;
|
||||
private JTextField spaceField;
|
||||
private JComboBox<FTConfig.AllocationUnit> unitsCombo;
|
||||
private JTextField dirField;
|
||||
|
||||
// VM specific
|
||||
private JTextField optionsField;
|
||||
@@ -220,7 +225,7 @@ public class FileTransferDialog extends JDialog {
|
||||
formPanel.add(createRow("File Options:", optionsPanel2));
|
||||
|
||||
// Host-specific options panel (TSO dataset allocation)
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(3, 4, 5, 5));
|
||||
hostOptsPanel.setOpaque(false);
|
||||
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
|
||||
|
||||
@@ -236,10 +241,24 @@ public class FileTransferDialog extends JDialog {
|
||||
blksizeField = new JTextField(5);
|
||||
hostOptsPanel.add(blksizeField);
|
||||
|
||||
hostOptsPanel.add(new JLabel("SPACE:"));
|
||||
hostOptsPanel.add(new JLabel("Units:"));
|
||||
unitsCombo = new JComboBox<>(new FTConfig.AllocationUnit[]{
|
||||
FTConfig.AllocationUnit.TRACKS,
|
||||
FTConfig.AllocationUnit.CYLINDERS,
|
||||
FTConfig.AllocationUnit.AVBLOCK,
|
||||
FTConfig.AllocationUnit.DEFAULT
|
||||
});
|
||||
unitsCombo.setSelectedItem(FTConfig.AllocationUnit.TRACKS);
|
||||
hostOptsPanel.add(unitsCombo);
|
||||
|
||||
hostOptsPanel.add(new JLabel("SPACE (Pri,Sec):"));
|
||||
spaceField = new JTextField(15);
|
||||
hostOptsPanel.add(spaceField);
|
||||
|
||||
hostOptsPanel.add(new JLabel("Directory (PDS):"));
|
||||
dirField = new JTextField(5);
|
||||
hostOptsPanel.add(dirField);
|
||||
|
||||
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
formPanel.add(hostOptsPanel);
|
||||
|
||||
@@ -306,9 +325,25 @@ public class FileTransferDialog extends JDialog {
|
||||
if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm);
|
||||
if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl);
|
||||
if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize);
|
||||
if (lastTransferState.units != null) unitsCombo.setSelectedItem(lastTransferState.units);
|
||||
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
|
||||
if (lastTransferState.dirBlocks != null) dirField.setText(lastTransferState.dirBlocks);
|
||||
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
|
||||
}
|
||||
|
||||
// When using a saved connection (or when no previous transfer state exists for this connection),
|
||||
// use the configured host type (CMS/TSO/CICS) for File Transfers
|
||||
if (coordinator != null && coordinator.getClient() != null) {
|
||||
haus.nightmare.lib3270j.ConnectionConfig cfg = coordinator.getClient().getConfig();
|
||||
if (cfg != null) {
|
||||
FTConfig.HostType configuredType = cfg.getFtHostType();
|
||||
if (configuredType != null) {
|
||||
if (cfg.isSavedConnection() || lastTransferState == null) {
|
||||
hostTypeCombo.setSelectedItem(configuredType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOptionStates() {
|
||||
@@ -327,7 +362,9 @@ public class FileTransferDialog extends JDialog {
|
||||
recfmField.setEnabled(isSend && isTso);
|
||||
lreclField.setEnabled(isSend && isTso);
|
||||
blksizeField.setEnabled(isSend && isTso);
|
||||
unitsCombo.setEnabled(isSend && isTso);
|
||||
spaceField.setEnabled(isSend && isTso);
|
||||
dirField.setEnabled(isSend && isTso);
|
||||
optionsField.setEnabled(isCms);
|
||||
}
|
||||
|
||||
@@ -359,11 +396,26 @@ public class FileTransferDialog extends JDialog {
|
||||
private void browseHostDirectory() {
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
String initialQuery = hostFileField.getText().trim();
|
||||
if (hostType == FTConfig.HostType.CMS && (initialQuery.isEmpty() || initialQuery.equals("* * *") || initialQuery.equals("*"))) {
|
||||
initialQuery = "* * A";
|
||||
}
|
||||
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||
hostFileField.setText(dialog.getSelectedHostFile());
|
||||
HostDirectoryEntry entry = dialog.getSelectedEntry();
|
||||
if (entry != null) {
|
||||
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
|
||||
recfmField.setText(entry.getRecfm().trim());
|
||||
}
|
||||
if (entry.getLrecl() > 0) {
|
||||
lreclField.setText(String.valueOf(entry.getLrecl()));
|
||||
}
|
||||
if (entry.getBlksize() > 0) {
|
||||
blksizeField.setText(String.valueOf(entry.getBlksize()));
|
||||
}
|
||||
}
|
||||
boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
|
||||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
|
||||
if (shouldAutofill) {
|
||||
@@ -410,7 +462,10 @@ public class FileTransferDialog extends JDialog {
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setBlksize(blksizeField.getText().trim());
|
||||
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
|
||||
config.setSpace(spaceField.getText().trim());
|
||||
config.setDirectoryBlocks(dirField.getText().trim());
|
||||
config.setOptions(optionsField.getText().trim());
|
||||
|
||||
recordSessionTransfer();
|
||||
@@ -439,7 +494,9 @@ public class FileTransferDialog extends JDialog {
|
||||
state.recfm = recfmField.getText().trim();
|
||||
state.lrecl = lreclField.getText().trim();
|
||||
state.blksize = blksizeField.getText().trim();
|
||||
state.units = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
state.space = spaceField.getText().trim();
|
||||
state.dirBlocks = dirField.getText().trim();
|
||||
state.options = optionsField.getText().trim();
|
||||
lastTransferState = state;
|
||||
}
|
||||
@@ -459,7 +516,9 @@ public class FileTransferDialog extends JDialog {
|
||||
public JTextField getRecfmField() { return recfmField; }
|
||||
public JTextField getLreclField() { return lreclField; }
|
||||
public JTextField getBlksizeField() { return blksizeField; }
|
||||
public JComboBox<FTConfig.AllocationUnit> getUnitsCombo() { return unitsCombo; }
|
||||
public JTextField getSpaceField() { return spaceField; }
|
||||
public JTextField getDirField() { return dirField; }
|
||||
public JTextField getOptionsField() { return optionsField; }
|
||||
|
||||
private void applyTheme(Container container) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
@@ -109,6 +110,267 @@ public class HostStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// ================= INI File Export / Import =================
|
||||
|
||||
/**
|
||||
* Export all currently saved host connections to an INI file.
|
||||
*/
|
||||
public static synchronized void exportToIniFile(File file) throws IOException {
|
||||
List<SavedHost> hosts = getSavedHosts();
|
||||
exportToIniFile(file, hosts);
|
||||
}
|
||||
|
||||
public static synchronized void exportToIniFile(String path) throws IOException {
|
||||
exportToIniFile(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a specified list of host connections to an INI file.
|
||||
*/
|
||||
public static void exportToIniFile(File file, List<SavedHost> hosts) throws IOException {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("Target file cannot be null");
|
||||
}
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
|
||||
w.println("; j3270 Saved Connections Configuration File");
|
||||
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
|
||||
w.println("; Connections count: " + (hosts != null ? hosts.size() : 0));
|
||||
w.println();
|
||||
|
||||
if (hosts != null) {
|
||||
for (SavedHost h : hosts) {
|
||||
String section = (h.getName() != null && !h.getName().trim().isEmpty())
|
||||
? h.getName().trim()
|
||||
: (h.getHost() != null && !h.getHost().trim().isEmpty() ? h.getHost().trim() : "Connection");
|
||||
w.println("[" + section + "]");
|
||||
if (h.getId() != null && !h.getId().trim().isEmpty()) {
|
||||
w.println("id = " + h.getId());
|
||||
}
|
||||
w.println("name = " + (h.getName() != null ? h.getName() : ""));
|
||||
w.println("host = " + (h.getHost() != null ? h.getHost() : ""));
|
||||
w.println("port = " + h.getPort());
|
||||
w.println("model = " + h.getModel());
|
||||
w.println("dynamicRows = " + h.getDynamicRows());
|
||||
w.println("dynamicCols = " + h.getDynamicCols());
|
||||
w.println("luName = " + (h.getLuName() != null ? h.getLuName() : ""));
|
||||
w.println("autoConnect = " + h.isAutoConnect());
|
||||
w.println("hostType = " + (h.getHostType() != null ? h.getHostType() : "TSO"));
|
||||
w.println("useTls = " + h.isUseTls());
|
||||
w.println("tlsVerifyCert = " + h.isTlsVerifyCert());
|
||||
w.println("tn3270e = " + h.isTn3270e());
|
||||
w.println("graphicsMode = " + (h.getGraphicsMode() != null ? h.getGraphicsMode() : "BOTH"));
|
||||
w.println("codePage = " + (h.getCodePage() != null ? h.getCodePage() : "037"));
|
||||
w.println("proxyType = " + (h.getProxyType() != null ? h.getProxyType() : "NONE"));
|
||||
w.println("proxyHost = " + (h.getProxyHost() != null ? h.getProxyHost() : ""));
|
||||
w.println("proxyPort = " + h.getProxyPort());
|
||||
w.println("proxyUsername = " + (h.getProxyUsername() != null ? h.getProxyUsername() : ""));
|
||||
w.println("proxyPassword = " + (h.getProxyPassword() != null ? h.getProxyPassword() : ""));
|
||||
w.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Saved connections exported to: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load connections from an INI file without saving them to persistent storage.
|
||||
*/
|
||||
public static List<SavedHost> loadFromIniFile(File file) throws IOException {
|
||||
if (file == null || !file.exists()) {
|
||||
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
|
||||
}
|
||||
List<SavedHost> hosts = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
SavedHost current = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
if (current != null) {
|
||||
hosts.add(current);
|
||||
}
|
||||
String section = line.substring(1, line.length() - 1).trim();
|
||||
if (section.toLowerCase().startsWith("connection:")) {
|
||||
section = section.substring("connection:".length()).trim();
|
||||
} else if (section.toLowerCase().startsWith("host:")) {
|
||||
section = section.substring("host:".length()).trim();
|
||||
}
|
||||
current = new SavedHost();
|
||||
current.setName(section);
|
||||
continue;
|
||||
}
|
||||
int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
String key = line.substring(0, eq).trim();
|
||||
String value = line.substring(eq + 1).trim();
|
||||
if (current == null) {
|
||||
current = new SavedHost();
|
||||
current.setName("Default");
|
||||
}
|
||||
applyIniEntry(current, key, value);
|
||||
}
|
||||
if (current != null) {
|
||||
hosts.add(current);
|
||||
}
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
public static List<SavedHost> loadFromIniFile(String path) throws IOException {
|
||||
return loadFromIniFile(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Import connections from an INI file and merge them into persistent storage.
|
||||
* Updates existing profiles matching by ID or Name, and appends new profiles.
|
||||
*/
|
||||
public static synchronized List<SavedHost> importFromIniFile(File file) throws IOException {
|
||||
List<SavedHost> imported = loadFromIniFile(file);
|
||||
if (imported == null || imported.isEmpty()) {
|
||||
return getSavedHosts();
|
||||
}
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
for (SavedHost host : imported) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
SavedHost existing = current.get(i);
|
||||
if ((host.getId() != null && !host.getId().trim().isEmpty() && Objects.equals(existing.getId(), host.getId())) ||
|
||||
(host.getName() != null && !host.getName().trim().isEmpty() &&
|
||||
host.getName().equalsIgnoreCase(existing.getName()))) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (host.isAutoConnect()) {
|
||||
for (SavedHost c : current) {
|
||||
c.setAutoConnect(false);
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
if (host.getId() == null || host.getId().trim().isEmpty()) {
|
||||
host.setId(current.get(index).getId());
|
||||
}
|
||||
current.set(index, host);
|
||||
} else {
|
||||
current.add(host);
|
||||
}
|
||||
}
|
||||
saveAll(current);
|
||||
return current;
|
||||
}
|
||||
|
||||
public static synchronized List<SavedHost> importFromIniFile(String path) throws IOException {
|
||||
return importFromIniFile(new File(path));
|
||||
}
|
||||
|
||||
private static void applyIniEntry(SavedHost h, String key, String value) {
|
||||
String lowerKey = key.toLowerCase();
|
||||
switch (lowerKey) {
|
||||
case "id":
|
||||
if (!value.isEmpty()) h.setId(value);
|
||||
break;
|
||||
case "name":
|
||||
case "profilename":
|
||||
case "profile":
|
||||
if (!value.isEmpty()) h.setName(value);
|
||||
break;
|
||||
case "host":
|
||||
case "hostname":
|
||||
case "address":
|
||||
h.setHost(value);
|
||||
break;
|
||||
case "port":
|
||||
h.setPort(parseInt(value, 23));
|
||||
break;
|
||||
case "model":
|
||||
case "terminalmodel":
|
||||
h.setModel(parseInt(value, 4));
|
||||
break;
|
||||
case "dynamicrows":
|
||||
case "dynamic_rows":
|
||||
case "rows":
|
||||
h.setDynamicRows(parseInt(value, 62));
|
||||
break;
|
||||
case "dynamiccols":
|
||||
case "dynamic_cols":
|
||||
case "cols":
|
||||
h.setDynamicCols(parseInt(value, 160));
|
||||
break;
|
||||
case "luname":
|
||||
case "lu_name":
|
||||
case "lu":
|
||||
h.setLuName(value);
|
||||
break;
|
||||
case "autoconnect":
|
||||
case "auto_connect":
|
||||
h.setAutoConnect(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "hosttype":
|
||||
case "host_type":
|
||||
case "type":
|
||||
h.setHostType(value.toUpperCase());
|
||||
break;
|
||||
case "usetls":
|
||||
case "use_tls":
|
||||
case "tls":
|
||||
case "ssl":
|
||||
h.setUseTls(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "tlsverifycert":
|
||||
case "verifycert":
|
||||
case "tls_verify_cert":
|
||||
h.setTlsVerifyCert(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "tn3270e":
|
||||
case "enabletn3270e":
|
||||
h.setTn3270e(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "graphicsmode":
|
||||
case "graphics_mode":
|
||||
case "graphics":
|
||||
h.setGraphicsMode(value.toUpperCase());
|
||||
break;
|
||||
case "codepage":
|
||||
case "code_page":
|
||||
case "charset":
|
||||
h.setCodePage(value);
|
||||
break;
|
||||
case "proxytype":
|
||||
case "proxy_type":
|
||||
h.setProxyType(value.toUpperCase());
|
||||
break;
|
||||
case "proxyhost":
|
||||
case "proxy_host":
|
||||
h.setProxyHost(value);
|
||||
break;
|
||||
case "proxyport":
|
||||
case "proxy_port":
|
||||
h.setProxyPort(parseInt(value, 0));
|
||||
break;
|
||||
case "proxyusername":
|
||||
case "proxy_username":
|
||||
case "proxyuser":
|
||||
h.setProxyUsername(value);
|
||||
break;
|
||||
case "proxypassword":
|
||||
case "proxy_password":
|
||||
case "proxypass":
|
||||
h.setProxyPassword(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================= JSON Serialization =================
|
||||
|
||||
public static String serializeHostsJson(List<SavedHost> hosts) {
|
||||
|
||||
@@ -108,6 +108,10 @@ public class SavedHost {
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId(this.id);
|
||||
config.setSavedProfileName(this.name);
|
||||
config.setHostType(this.hostType != null ? this.hostType : "TSO");
|
||||
if (model == 0 || tm.isDynamic()) {
|
||||
config.setDynamicDimensions(dynamicRows, dynamicCols);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
/**
|
||||
* Dialogue shown when a connection is initiated while j3270 is already connected.
|
||||
* Asks the user whether the connection should be opened in a new window or
|
||||
* if the current connection should be aborted.
|
||||
*/
|
||||
public class ConnectChoiceDialog extends JDialog {
|
||||
|
||||
public enum Choice {
|
||||
NEW_WINDOW,
|
||||
ABORT_CURRENT,
|
||||
CANCEL
|
||||
}
|
||||
|
||||
private Choice result = Choice.CANCEL;
|
||||
private final JButton newWindowBtn;
|
||||
private final JButton abortBtn;
|
||||
private final JButton cancelBtn;
|
||||
private final JLabel titleLabel;
|
||||
private final JLabel line1;
|
||||
private final JLabel line2;
|
||||
|
||||
public ConnectChoiceDialog(Window owner, String currentTarget) {
|
||||
super(owner, "Active Connection", ModalityType.APPLICATION_MODAL);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
setResizable(false);
|
||||
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(16, 16));
|
||||
mainPanel.setBorder(new EmptyBorder(20, 24, 20, 24));
|
||||
|
||||
// Message Panel
|
||||
JPanel messagePanel = new JPanel();
|
||||
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
|
||||
messagePanel.setOpaque(false);
|
||||
|
||||
titleLabel = new JLabel("Connection Already Active");
|
||||
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 2));
|
||||
messagePanel.add(titleLabel);
|
||||
messagePanel.add(Box.createVerticalStrut(10));
|
||||
|
||||
String targetText = (currentTarget != null && !currentTarget.trim().isEmpty())
|
||||
? currentTarget.trim()
|
||||
: "the host";
|
||||
line1 = new JLabel("j3270 is currently connected to " + targetText + ".");
|
||||
line1.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
messagePanel.add(line1);
|
||||
|
||||
messagePanel.add(Box.createVerticalStrut(6));
|
||||
line2 = new JLabel("Would you like to open the connection in a new window or abort the current connection?");
|
||||
line2.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
messagePanel.add(line2);
|
||||
|
||||
mainPanel.add(messagePanel, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
cancelBtn = new JButton("Cancel");
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
cancelBtn.addActionListener(e -> {
|
||||
result = Choice.CANCEL;
|
||||
dispose();
|
||||
});
|
||||
|
||||
abortBtn = new JButton("Abort Current Connection");
|
||||
ThemeManager.styleButton(abortBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
abortBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
abortBtn.addActionListener(e -> {
|
||||
result = Choice.ABORT_CURRENT;
|
||||
dispose();
|
||||
});
|
||||
|
||||
newWindowBtn = new JButton("New Window");
|
||||
ThemeManager.styleButton(newWindowBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
newWindowBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
newWindowBtn.addActionListener(e -> {
|
||||
result = Choice.NEW_WINDOW;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(abortBtn);
|
||||
buttonPanel.add(newWindowBtn);
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// ESC key cancels
|
||||
getRootPane().registerKeyboardAction(e -> {
|
||||
result = Choice.CANCEL;
|
||||
dispose();
|
||||
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
|
||||
getRootPane().setDefaultButton(newWindowBtn);
|
||||
|
||||
pack();
|
||||
if (owner != null) {
|
||||
setLocationRelativeTo(owner);
|
||||
} else {
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
}
|
||||
|
||||
public Choice getChoice() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public JButton getNewWindowButton() {
|
||||
return newWindowBtn;
|
||||
}
|
||||
|
||||
public JButton getAbortButton() {
|
||||
return abortBtn;
|
||||
}
|
||||
|
||||
public JButton getCancelButton() {
|
||||
return cancelBtn;
|
||||
}
|
||||
|
||||
public JLabel getTitleLabel() {
|
||||
return titleLabel;
|
||||
}
|
||||
|
||||
public JLabel getLine1() {
|
||||
return line1;
|
||||
}
|
||||
|
||||
public JLabel getLine2() {
|
||||
return line2;
|
||||
}
|
||||
|
||||
public static Choice showPrompt(Window owner, String currentTarget) {
|
||||
ConnectChoiceDialog dialog = new ConnectChoiceDialog(owner, currentTarget);
|
||||
dialog.setVisible(true);
|
||||
return dialog.getChoice();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.CodePageRegistry;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
@@ -36,6 +37,7 @@ public class ConnectDialog extends JDialog {
|
||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
||||
private JComboBox<String> codePageCombo;
|
||||
private JTextField luField;
|
||||
private JComboBox<String> hostTypeCombo;
|
||||
private JCheckBox tlsCheckBox;
|
||||
private JCheckBox verifyCertCheckBox;
|
||||
private JCheckBox tn3270eCheckBox;
|
||||
@@ -214,6 +216,25 @@ public class ConnectDialog extends JDialog {
|
||||
|
||||
gridy++;
|
||||
|
||||
// Host Type (CMS/TSO/CICS)
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel hostTypeLabel = new JLabel("Host Type:");
|
||||
hostTypeLabel.setFont(labelFont);
|
||||
contentPanel.add(hostTypeLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
String[] hostTypes = {"TSO", "CMS", "CICS"};
|
||||
hostTypeCombo = new JComboBox<>(hostTypes);
|
||||
hostTypeCombo.setFont(ThemeManager.getUiFont());
|
||||
ThemeManager.styleComboBox(hostTypeCombo);
|
||||
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
|
||||
contentPanel.add(hostTypeCombo, gbc);
|
||||
|
||||
gridy++;
|
||||
|
||||
// 6. Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = gridy;
|
||||
@@ -258,51 +279,7 @@ public class ConnectDialog extends JDialog {
|
||||
contentPanel.add(cpLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
String[] commonCodePages = {
|
||||
"037 - US / Canada / Brazil",
|
||||
"1047 - IBM Open Systems / z/OS Unix",
|
||||
"500 - International Latin-1",
|
||||
"273 - Germany / Austria",
|
||||
"277 - Denmark / Norway",
|
||||
"278 - Sweden / Finland",
|
||||
"280 - Italy",
|
||||
"284 - Spain / Latin America",
|
||||
"285 - United Kingdom",
|
||||
"297 - France",
|
||||
"870 - Eastern Europe / Latin-2",
|
||||
"871 - Iceland",
|
||||
"875 - Greece (Greek)",
|
||||
"1026 - Turkey (Turkish Latin-5)",
|
||||
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
|
||||
"905 - Turkey (Turkish Latin-3)",
|
||||
"420 - Arabic Bilingual",
|
||||
"424 - Hebrew (with Latin)",
|
||||
"803 - Hebrew Character Set",
|
||||
"838 - Thai Extended",
|
||||
"1160 - Thai Euro \u20AC",
|
||||
"1025 - Cyrillic Multilingual",
|
||||
"1123 - Cyrillic Ukraine",
|
||||
"1154 - Cyrillic Euro \u20AC",
|
||||
"880 - Cyrillic (Russian)",
|
||||
"1140 - US / Canada (Euro \u20AC)",
|
||||
"1141 - Germany / Austria (Euro \u20AC)",
|
||||
"1142 - Denmark / Norway (Euro \u20AC)",
|
||||
"1143 - Sweden / Finland (Euro \u20AC)",
|
||||
"1144 - Italy (Euro \u20AC)",
|
||||
"1145 - Spain / Latin America (Euro \u20AC)",
|
||||
"1146 - United Kingdom (Euro \u20AC)",
|
||||
"1147 - France (Euro \u20AC)",
|
||||
"1148 - International (Euro \u20AC)",
|
||||
"1149 - Iceland (Euro \u20AC)",
|
||||
"930 - Japanese Katakana Mixed DBCS",
|
||||
"939 - Japanese Latin Mixed DBCS",
|
||||
"935 - Simplified Chinese Mixed DBCS",
|
||||
"937 - Traditional Chinese Mixed DBCS",
|
||||
"1388 - Simplified Chinese Extended DBCS",
|
||||
"1371 - Traditional Chinese Extended DBCS",
|
||||
"933 - Korean Mixed DBCS"
|
||||
};
|
||||
codePageCombo = new JComboBox<>(commonCodePages);
|
||||
codePageCombo = new JComboBox<>(CodePageRegistry.COMMON_CODE_PAGES);
|
||||
codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleComboBox(codePageCombo);
|
||||
setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
||||
@@ -565,6 +542,7 @@ public class ConnectDialog extends JDialog {
|
||||
} else {
|
||||
selectedProfileId = null;
|
||||
deleteProfileBtn.setEnabled(false);
|
||||
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,6 +568,7 @@ public class ConnectDialog extends JDialog {
|
||||
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
|
||||
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
|
||||
luField.setText(h.getLuName() != null ? h.getLuName() : "");
|
||||
setInitialHostType(h.getHostType() != null ? h.getHostType() : "TSO");
|
||||
tlsCheckBox.setSelected(h.isUseTls());
|
||||
verifyCertCheckBox.setEnabled(h.isUseTls());
|
||||
verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
|
||||
@@ -677,6 +656,7 @@ public class ConnectDialog extends JDialog {
|
||||
int dRows = (Integer) dynamicRowsSpinner.getValue();
|
||||
int dCols = (Integer) dynamicColsSpinner.getValue();
|
||||
String lu = luField.getText().trim();
|
||||
String hostType = getSelectedHostType();
|
||||
boolean tls = tlsCheckBox.isSelected();
|
||||
boolean verify = verifyCertCheckBox.isSelected();
|
||||
boolean tn3270e = tn3270eCheckBox.isSelected();
|
||||
@@ -705,7 +685,7 @@ public class ConnectDialog extends JDialog {
|
||||
String pPass = new String(proxyPassField.getPassword());
|
||||
|
||||
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
|
||||
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
|
||||
hostType, tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
|
||||
}
|
||||
|
||||
// ================= Connect Action =================
|
||||
@@ -797,10 +777,14 @@ public class ConnectDialog extends JDialog {
|
||||
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
|
||||
}
|
||||
|
||||
result.setHostType(getSelectedHostType());
|
||||
if (selectedProfileId != null) {
|
||||
result.setSavedConnection(true);
|
||||
result.setSavedProfileId(selectedProfileId);
|
||||
SavedHost existing = HostStorage.findById(selectedProfileId);
|
||||
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
|
||||
? existing.getName() : host;
|
||||
result.setSavedProfileName(pName);
|
||||
SavedHost updated = buildCurrentSavedHost(pName);
|
||||
HostStorage.saveHost(updated);
|
||||
}
|
||||
@@ -902,4 +886,44 @@ public class ConnectDialog extends JDialog {
|
||||
public String getProxyPassword() {
|
||||
return new String(proxyPassField.getPassword());
|
||||
}
|
||||
|
||||
public void setInitialHostType(String hostType) {
|
||||
if (hostType != null && hostTypeCombo != null) {
|
||||
for (int i = 0; i < hostTypeCombo.getItemCount(); i++) {
|
||||
if (hostTypeCombo.getItemAt(i).equalsIgnoreCase(hostType.trim())) {
|
||||
hostTypeCombo.setSelectedIndex(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setInitialHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType hostType) {
|
||||
if (hostType != null) {
|
||||
setInitialHostType(hostType.name());
|
||||
}
|
||||
}
|
||||
|
||||
public String getSelectedHostType() {
|
||||
if (hostTypeCombo != null && hostTypeCombo.getSelectedItem() != null) {
|
||||
return hostTypeCombo.getSelectedItem().toString().trim().toUpperCase();
|
||||
}
|
||||
return "TSO";
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ft.FTConfig.HostType getSelectedFtHostType() {
|
||||
try {
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(getSelectedHostType());
|
||||
} catch (Exception e) {
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
|
||||
}
|
||||
}
|
||||
|
||||
public JComboBox<String> getHostTypeCombo() {
|
||||
return hostTypeCombo;
|
||||
}
|
||||
|
||||
public String getSelectedProfileId() {
|
||||
return selectedProfileId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.List;
|
||||
*/
|
||||
public class FieldInspectorDialog extends JDialog implements ScreenUpdateListener {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private DefaultTableModel tableModel;
|
||||
@@ -46,13 +46,39 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
if (client != null) {
|
||||
// Screen update listener removed on close
|
||||
if (FieldInspectorDialog.this.client != null) {
|
||||
FieldInspectorDialog.this.client.removeScreenUpdateListener(FieldInspectorDialog.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client newClient) {
|
||||
if (this.client != null) {
|
||||
this.client.removeScreenUpdateListener(this);
|
||||
}
|
||||
this.client = newClient;
|
||||
if (this.client != null) {
|
||||
this.client.addScreenUpdateListener(this);
|
||||
refreshFields();
|
||||
} else if (tableModel != null && countLabel != null) {
|
||||
tableModel.setRowCount(0);
|
||||
countLabel.setText("Not connected");
|
||||
}
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (client != null) {
|
||||
client.removeScreenUpdateListener(this);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
|
||||
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.awt.event.KeyEvent;
|
||||
*/
|
||||
public class FindDialog extends JDialog {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextField searchField;
|
||||
@@ -40,6 +40,24 @@ public class FindDialog extends JDialog {
|
||||
setResizable(false);
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public void setSearchText(String text) {
|
||||
if (searchField != null) {
|
||||
searchField.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public String getSearchText() {
|
||||
return searchField != null ? searchField.getText() : "";
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
@@ -3,15 +3,19 @@ package haus.nightmare.j3270.ui;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryParser;
|
||||
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
@@ -19,6 +23,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
|
||||
* Aligned with IBM Host On-Demand presentation space queries and clipboard parsing.
|
||||
*/
|
||||
public class HostDirectoryDialog extends JDialog {
|
||||
|
||||
@@ -27,13 +32,16 @@ public class HostDirectoryDialog extends JDialog {
|
||||
|
||||
private JComboBox<FTConfig.HostType> hostTypeCombo;
|
||||
private JTextField queryField;
|
||||
private JButton queryHostBtn;
|
||||
private JTable table;
|
||||
private DefaultTableModel tableModel;
|
||||
private JLabel statusLabel;
|
||||
|
||||
private String selectedHostFile = null;
|
||||
private HostDirectoryEntry selectedEntry = null;
|
||||
private boolean confirmed = false;
|
||||
|
||||
private final List<HostDirectoryEntry> allEntries = new ArrayList<>();
|
||||
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
|
||||
|
||||
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
|
||||
@@ -43,7 +51,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI(initialQuery);
|
||||
setSize(700, 420);
|
||||
setSize(840, 480);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
@@ -69,33 +77,68 @@ public class HostDirectoryDialog extends JDialog {
|
||||
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
|
||||
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
|
||||
ThemeManager.styleComboBox(hostTypeCombo);
|
||||
hostTypeCombo.addActionListener(e -> {
|
||||
setupTableColumns();
|
||||
FTConfig.HostType ht = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
if (ht == FTConfig.HostType.CMS) {
|
||||
String cur = queryField.getText().trim();
|
||||
if (cur.isEmpty() || cur.equals("* * *") || cur.equals("*")) {
|
||||
queryField.setText("* * A");
|
||||
}
|
||||
} else if (ht == FTConfig.HostType.TSO) {
|
||||
String cur = queryField.getText().trim();
|
||||
if (cur.equals("* * A") || cur.equals("* * *")) {
|
||||
queryField.setText("");
|
||||
}
|
||||
}
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
queryHost();
|
||||
} else {
|
||||
parseCurrentScreen();
|
||||
}
|
||||
});
|
||||
topPanel.add(hostTypeCombo, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
JLabel queryLabel = new JLabel("Query Pattern / Text:");
|
||||
JLabel queryLabel = new JLabel("Filter / Pattern:");
|
||||
queryLabel.setFont(font);
|
||||
topPanel.add(queryLabel, gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.weightx = 1.0;
|
||||
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
|
||||
String defaultQuery = resolveDefaultQuery(initialHostType, initialQuery);
|
||||
queryField = new JTextField(defaultQuery, 16);
|
||||
queryField.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextField(queryField);
|
||||
queryField.addActionListener(e -> onFilterOrParse());
|
||||
topPanel.add(queryField, gbc);
|
||||
|
||||
gbc.gridx = 4;
|
||||
gbc.weightx = 0;
|
||||
JButton parseBtn = new JButton("Query / Parse");
|
||||
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
parseBtn.setFont(font);
|
||||
parseBtn.addActionListener(e -> runQuery());
|
||||
topPanel.add(parseBtn, gbc);
|
||||
JButton filterBtn = new JButton("Filter / Parse");
|
||||
ThemeManager.styleButton(filterBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
filterBtn.setFont(font);
|
||||
filterBtn.addActionListener(e -> onFilterOrParse());
|
||||
topPanel.add(filterBtn, gbc);
|
||||
|
||||
gbc.gridx = 5;
|
||||
queryHostBtn = new JButton("Query Host");
|
||||
ThemeManager.styleButton(queryHostBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
queryHostBtn.setFont(font);
|
||||
queryHostBtn.setToolTipText("Send LISTCAT or LISTFILE query to active host session");
|
||||
queryHostBtn.addActionListener(e -> queryHost());
|
||||
topPanel.add(queryHostBtn, gbc);
|
||||
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Table
|
||||
tableModel = new DefaultTableModel();
|
||||
tableModel = new DefaultTableModel() {
|
||||
@Override
|
||||
public boolean isCellEditable(int row, int column) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
table = new JTable(tableModel);
|
||||
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(ThemeManager.getMonospacedUiFont());
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
@@ -114,26 +157,42 @@ public class HostDirectoryDialog extends JDialog {
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
|
||||
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 6));
|
||||
bottomPanel.setOpaque(false);
|
||||
bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 4, 4, 4));
|
||||
|
||||
statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(font);
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
bottomPanel.add(statusLabel, BorderLayout.NORTH);
|
||||
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
btnPanel.setOpaque(false);
|
||||
|
||||
JButton pasteScreenBtn = new JButton("Parse Current Screen");
|
||||
JButton inspectBtn = new JButton("Inspect (LISTDS)");
|
||||
ThemeManager.styleButton(inspectBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
inspectBtn.setFont(font);
|
||||
inspectBtn.setToolTipText("Retrieve VTOC attributes (RECFM, LRECL, BLKSIZE) for the selected dataset via TSO LISTDS");
|
||||
inspectBtn.addActionListener(e -> inspectSelectedDataset());
|
||||
|
||||
JButton pasteScreenBtn = new JButton("Parse Screen");
|
||||
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pasteScreenBtn.setFont(font);
|
||||
pasteScreenBtn.setToolTipText("Parse items currently visible on the 3270 presentation space");
|
||||
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
|
||||
|
||||
JButton pasteClipBtn = new JButton("Paste Clipboard");
|
||||
ThemeManager.styleButton(pasteClipBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pasteClipBtn.setFont(font);
|
||||
pasteClipBtn.setToolTipText("Parse host filenames copied to the system clipboard (HoD compatible)");
|
||||
pasteClipBtn.addActionListener(e -> parseClipboard());
|
||||
|
||||
JButton selectBtn = new JButton("Select Dataset");
|
||||
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
selectBtn.setFont(font);
|
||||
@@ -147,23 +206,28 @@ public class HostDirectoryDialog extends JDialog {
|
||||
dispose();
|
||||
});
|
||||
|
||||
btnPanel.add(inspectBtn);
|
||||
btnPanel.add(pasteScreenBtn);
|
||||
btnPanel.add(pasteClipBtn);
|
||||
btnPanel.add(Box.createHorizontalStrut(10));
|
||||
btnPanel.add(selectBtn);
|
||||
btnPanel.add(cancelBtn);
|
||||
|
||||
bottomPanel.add(btnPanel, BorderLayout.EAST);
|
||||
bottomPanel.add(btnPanel, BorderLayout.SOUTH);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Initial setup
|
||||
// Initial setup: columns and screen parse or host query
|
||||
setupTableColumns();
|
||||
if (initialQuery != null && !initialQuery.isEmpty()) {
|
||||
runQuery();
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
queryHost();
|
||||
} else {
|
||||
parseCurrentScreen();
|
||||
if (!queryField.getText().trim().isEmpty()) {
|
||||
applyFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,76 +238,665 @@ public class HostDirectoryDialog extends JDialog {
|
||||
tableModel.setColumnIdentifiers(new Object[]{
|
||||
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
|
||||
});
|
||||
int[] widths = {120, 100, 80, 70, 70, 90, 170};
|
||||
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
|
||||
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
|
||||
}
|
||||
} else {
|
||||
tableModel.setColumnIdentifiers(new Object[]{
|
||||
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents"
|
||||
"Dataset Name", "Volume"
|
||||
});
|
||||
int[] widths = {620, 160};
|
||||
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
|
||||
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
|
||||
if (i == 0) {
|
||||
table.getColumnModel().getColumn(0).setMinWidth(250);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void runQuery() {
|
||||
setupTableColumns();
|
||||
currentEntries.clear();
|
||||
private void onFilterOrParse() {
|
||||
String query = queryField.getText().trim();
|
||||
// If text contains multiple lines, treat it as pasted raw text to parse
|
||||
if (query.contains("\n") || query.contains("\r") || query.toUpperCase().contains("NONVSAM")
|
||||
|| query.toUpperCase().contains("IN-CAT") || query.toUpperCase().contains("FILENAME")) {
|
||||
parseText(query, "query text");
|
||||
} else if (allEntries.isEmpty() && client != null && client.getConnectionState().isFullSession()) {
|
||||
queryHost();
|
||||
} else {
|
||||
applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void queryHost() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
statusLabel.setText("No active session to query.");
|
||||
return;
|
||||
}
|
||||
|
||||
String pattern = queryField.getText().trim();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
statusLabel.setText("Querying host (" + hostType + ")...");
|
||||
if (queryHostBtn != null) queryHostBtn.setEnabled(false);
|
||||
|
||||
SwingWorker<String, Void> worker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected String doInBackground() throws Exception {
|
||||
if (client == null || client.getPS() == null) return "";
|
||||
|
||||
StringBuilder allPages = new StringBuilder();
|
||||
int maxPages = 500;
|
||||
|
||||
if (client != null && client.getXfer() != null) {
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
|
||||
currentEntries.addAll(cmsEntries);
|
||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||
});
|
||||
// CMS Query:
|
||||
// 1. Clear presentation space first to ensure clean state
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
|
||||
// 2. Format CMS LISTFILE query command (default to minidisk A unless specified)
|
||||
String cmsFilter = formatCMSFilter(pattern);
|
||||
client.getPS().SendKeys("LISTFILE " + cmsFilter + " ( nohead alloc date[enter]");
|
||||
Thread.sleep(100);
|
||||
|
||||
// 3. Multi-page collection loop:
|
||||
// CMS pagination displays MORE... or HOLDING when a screen is full.
|
||||
// When advanced with [clear], CMS flashes RUNNING while generating the next screen.
|
||||
// We poll up to 10 seconds per page for MORE... / HOLDING or Ready;
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
long startWait = System.currentTimeMillis();
|
||||
long timeoutMs = 10000; // 10 seconds per page
|
||||
String screen = "";
|
||||
boolean isMore = false;
|
||||
|
||||
while (System.currentTimeMillis() - startWait < timeoutMs) {
|
||||
waitForKeyboardUnlock(client, 500);
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
screen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
|
||||
isMore = true;
|
||||
break;
|
||||
}
|
||||
if (upper.contains("READY;") || upper.contains("READY(")) {
|
||||
break;
|
||||
}
|
||||
// Still RUNNING or transitioning, wait and re-check
|
||||
Thread.sleep(150);
|
||||
}
|
||||
|
||||
allPages.append(screen).append("\n");
|
||||
|
||||
if (isMore) {
|
||||
// In CMS, sending [clear] advances to the next page of output
|
||||
client.getPS().SendKeys("[clear]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
} else {
|
||||
// Ready; encountered or timed out -> listing complete
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically clear the listing from the terminal presentation space
|
||||
// so the terminal screen is left clean for the user while keeping the dialog populated
|
||||
if (client.getPS() != null) {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
}
|
||||
|
||||
} else {
|
||||
// TSO Query:
|
||||
// Check if current screen is in ISPF (formatted screen or containing ===> or ISPF)
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
|
||||
boolean hasCmdArrow = initialScreen.contains("===>");
|
||||
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
|
||||
|
||||
String tsoCmd = "TSO LISTCAT ALL";
|
||||
if (!pattern.isEmpty()) {
|
||||
String dsnPattern = pattern.trim();
|
||||
if (dsnPattern.startsWith("'") && dsnPattern.endsWith("'") && dsnPattern.length() > 2) {
|
||||
dsnPattern = dsnPattern.substring(1, dsnPattern.length() - 1);
|
||||
}
|
||||
int paren = dsnPattern.indexOf('(');
|
||||
if (paren > 0) {
|
||||
dsnPattern = dsnPattern.substring(0, paren);
|
||||
}
|
||||
if (dsnPattern.equals("*") || dsnPattern.equals("* * *") || dsnPattern.equals("*.*")) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
String level = dsnPattern;
|
||||
if (level.endsWith(".*")) {
|
||||
level = level.substring(0, level.length() - 2);
|
||||
} else if (level.endsWith("*")) {
|
||||
level = level.substring(0, level.length() - 1);
|
||||
}
|
||||
if (level.isEmpty()) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isIspf) {
|
||||
// In ISPF, do NOT send [clear] because [clear] destroys the ISPF panel.
|
||||
// Locate the command/option line (indicated by ===>) or home to first input field.
|
||||
int arrowPos = initialScreen.indexOf("===>");
|
||||
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
|
||||
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
|
||||
if (inputAddr > 0) {
|
||||
client.getScreenBuffer().setCursorAddress(inputAddr);
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
} else {
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
} else {
|
||||
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
|
||||
currentEntries.addAll(tsoEntries);
|
||||
for (TSODirectoryEntry entry : tsoEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||
});
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
client.getPS().SendKeys("[erase_eof]");
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
} else {
|
||||
// In native TSO (ISPF closed, line mode READY prompt)
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
}
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
// Multi-page collection loop for TSO:
|
||||
// 1. Wait for output to start appearing after sending command
|
||||
long startCmd = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||
waitForKeyboardUnlock(client, 200);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String u = s.toUpperCase();
|
||||
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|
||||
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|
||||
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|
||||
|| u.contains("NO ENTRY") || s.contains(" --- ")
|
||||
|| u.contains("--RECFM-") || u.contains("--VOLUMES--")) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(80);
|
||||
}
|
||||
|
||||
// 2. Multi-page collection
|
||||
String prevScreen = "";
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
waitForKeyboardUnlock(client, 300);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
prevScreen = screen;
|
||||
allPages.append(screen).append("\n");
|
||||
|
||||
if (upper.contains("***")) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|
||||
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")
|
||||
|| upper.contains("--RECFM-") || upper.contains("--VOLUMES--")) {
|
||||
// If screen shows dataset data without ***, send [enter] once to see if *** or more output is triggered
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 1000);
|
||||
|
||||
int r2 = client.getPS().getRows();
|
||||
int c2 = client.getPS().getCols();
|
||||
String s2 = client.getPS().copyString(0, 0, r2 - 1, c2 - 1);
|
||||
if (s2.toUpperCase().contains("***")) {
|
||||
prevScreen = s2;
|
||||
allPages.append(s2).append("\n");
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
continue;
|
||||
} else {
|
||||
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
|
||||
allPages.append(s2).append("\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Terminal clean-up: clear the list from the terminal presentation space
|
||||
if (client.getPS() != null) {
|
||||
if (isIspf) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(150);
|
||||
waitForKeyboardUnlock(client, 1500);
|
||||
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|
||||
|| currentScr.contains(" --- ") || currentScr.toUpperCase().contains("--RECFM-")
|
||||
|| (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
} else {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusLabel.setText(currentEntries.size() + " entries found.");
|
||||
return allPages.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
if (queryHostBtn != null) queryHostBtn.setEnabled(true);
|
||||
try {
|
||||
String screenText = get();
|
||||
parseText(screenText, "host query");
|
||||
if (!pattern.isEmpty() && !allEntries.isEmpty()) {
|
||||
applyFilter();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
statusLabel.setText("Host query failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
}
|
||||
|
||||
private void inspectSelectedDataset() {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0 || row >= currentEntries.size()) {
|
||||
JOptionPane.showMessageDialog(this, "Please select an entry from the table to inspect.",
|
||||
"No Selection", JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
HostDirectoryEntry entry = currentEntries.get(row);
|
||||
if (!(entry instanceof TSODirectoryEntry)) {
|
||||
JOptionPane.showMessageDialog(this, "Inspection is only supported for TSO datasets.",
|
||||
"Not Supported", JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
JOptionPane.showMessageDialog(this, "No active full 3270 session to query host.",
|
||||
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
|
||||
String dsn = tso.getDatasetName();
|
||||
if (dsn == null || dsn.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusLabel.setText("Inspecting " + dsn + " via TSO LISTDS...");
|
||||
|
||||
SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
|
||||
@Override
|
||||
protected String doInBackground() throws Exception {
|
||||
StringBuilder output = new StringBuilder();
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
|
||||
boolean hasCmdArrow = initialScreen.contains("===>");
|
||||
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
|
||||
|
||||
String tsoCmd = "TSO LISTDS '" + dsn + "' STATUS";
|
||||
|
||||
if (isIspf) {
|
||||
int arrowPos = initialScreen.indexOf("===>");
|
||||
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
|
||||
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
|
||||
if (inputAddr > 0) {
|
||||
client.getScreenBuffer().setCursorAddress(inputAddr);
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
} else {
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
} else {
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
client.getPS().SendKeys("[erase_eof]");
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
} else {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
}
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
// Wait for output to start appearing
|
||||
long startCmd = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||
waitForKeyboardUnlock(client, 200);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String u = s.toUpperCase();
|
||||
if (u.contains("--RECFM-") || u.contains("--VOLUMES--") || u.contains("***")
|
||||
|| u.contains("READY") || u.contains("NOT IN CATALOG") || u.contains("NOT FOUND")) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(80);
|
||||
}
|
||||
|
||||
String prevScreen = "";
|
||||
for (int page = 0; page < 5; page++) {
|
||||
waitForKeyboardUnlock(client, 300);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
prevScreen = screen;
|
||||
output.append(screen).append("\n");
|
||||
|
||||
if (upper.contains("***")) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal cleanup
|
||||
if (client.getPS() != null) {
|
||||
if (isIspf) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(150);
|
||||
waitForKeyboardUnlock(client, 1500);
|
||||
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
if (currentScr.toUpperCase().contains("--RECFM-") || (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
} else {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
try {
|
||||
String outText = get();
|
||||
List<TSODirectoryEntry> parsed = TSODirectoryParser.parse(outText);
|
||||
if (!parsed.isEmpty()) {
|
||||
TSODirectoryEntry updated = parsed.get(0);
|
||||
if (updated.getRecfm() != null) tso.setRecfm(updated.getRecfm());
|
||||
if (updated.getLrecl() > 0) tso.setLrecl(updated.getLrecl());
|
||||
if (updated.getBlksize() > 0) tso.setBlksize(updated.getBlksize());
|
||||
if (updated.getDsorg() != null) tso.setDsorg(updated.getDsorg());
|
||||
if (updated.getVolume() != null && (tso.getVolume() == null || tso.getVolume().equals("******") || tso.getVolume().isEmpty())) {
|
||||
tso.setVolume(updated.getVolume());
|
||||
}
|
||||
|
||||
// Update table row in view
|
||||
int modelRow = table.convertRowIndexToModel(row);
|
||||
if (modelRow >= 0 && modelRow < tableModel.getRowCount()) {
|
||||
tableModel.setValueAt(tso.getVolume() != null ? tso.getVolume() : "", modelRow, 1);
|
||||
}
|
||||
|
||||
statusLabel.setText("Attributes for " + dsn + ": RECFM=" + tso.getRecfm() + ", LRECL=" + tso.getLrecl() + ", BLKSIZE=" + tso.getBlksize() + ", VOL=" + tso.getVolume());
|
||||
|
||||
StringBuilder msg = new StringBuilder();
|
||||
msg.append("Dataset: ").append(dsn).append("\n\n");
|
||||
msg.append("Volume: ").append(tso.getVolume() != null ? tso.getVolume() : "N/A").append("\n");
|
||||
msg.append("Record Format (RECFM): ").append(tso.getRecfm() != null ? tso.getRecfm() : "N/A").append("\n");
|
||||
msg.append("Logical Record Length (LRECL): ").append(tso.getLrecl() > 0 ? String.valueOf(tso.getLrecl()) : "N/A").append("\n");
|
||||
msg.append("Block Size (BLKSIZE): ").append(tso.getBlksize() > 0 ? String.valueOf(tso.getBlksize()) : "N/A").append("\n");
|
||||
msg.append("Dataset Organization (DSORG): ").append(tso.getDsorg() != null ? tso.getDsorg() : "N/A").append("\n");
|
||||
if (tso.getTracksAllocated() > 0) {
|
||||
msg.append("Allocated Tracks: ").append(tso.getTracksAllocated()).append("\n");
|
||||
}
|
||||
if (tso.getExtents() > 0) {
|
||||
msg.append("Extents: ").append(tso.getExtents()).append("\n");
|
||||
}
|
||||
JOptionPane.showMessageDialog(HostDirectoryDialog.this, msg.toString(), "Dataset Attributes (LISTDS)", JOptionPane.INFORMATION_MESSAGE);
|
||||
} else {
|
||||
statusLabel.setText("No VTOC attributes found for " + dsn);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
statusLabel.setText("Inspection failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
}
|
||||
|
||||
private static boolean waitForKeyboardUnlock(Telnet3270Client client, long timeoutMs) {
|
||||
if (client == null) return false;
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
boolean locked = false;
|
||||
if (client.getInputProcessor() != null && client.getInputProcessor().isKeyboardLocked()) {
|
||||
locked = true;
|
||||
}
|
||||
if (client.getOIA() != null && (client.getOIA().isKeyLocked() || client.getOIA().isXSystem())) {
|
||||
locked = true;
|
||||
}
|
||||
if (!locked) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(40);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void parseCurrentScreen() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
if (client == null || !client.getConnectionState().isFullSession() || client.getPS() == null) {
|
||||
statusLabel.setText("No active session to parse.");
|
||||
return;
|
||||
}
|
||||
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
String screenText = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
parseText(screenText, "presentation space");
|
||||
}
|
||||
|
||||
private void parseClipboard() {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
Transferable contents = clipboard.getContents(null);
|
||||
if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
|
||||
String clipText = (String) contents.getTransferData(DataFlavor.stringFlavor);
|
||||
if (clipText != null && !clipText.trim().isEmpty()) {
|
||||
parseText(clipText, "clipboard");
|
||||
return;
|
||||
}
|
||||
}
|
||||
statusLabel.setText("Clipboard contains no text.");
|
||||
} catch (Exception ex) {
|
||||
statusLabel.setText("Error reading clipboard: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void parseText(String text, String source) {
|
||||
setupTableColumns();
|
||||
currentEntries.clear();
|
||||
allEntries.clear();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
String screenText = client.getPS().getString(0, client.getPS().getSize());
|
||||
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText);
|
||||
currentEntries.addAll(cmsEntries);
|
||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||
});
|
||||
}
|
||||
List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(text);
|
||||
allEntries.addAll(cmsEntries);
|
||||
} else {
|
||||
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText);
|
||||
currentEntries.addAll(tsoEntries);
|
||||
for (TSODirectoryEntry entry : tsoEntries) {
|
||||
List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(text);
|
||||
allEntries.addAll(tsoEntries);
|
||||
}
|
||||
|
||||
applyFilter();
|
||||
if (allEntries.isEmpty()) {
|
||||
statusLabel.setText("0 entries found from " + source + ". Use 'Query Host' or 'Paste Clipboard'.");
|
||||
} else {
|
||||
statusLabel.setText("Parsed " + allEntries.size() + " entries from " + source + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFilter() {
|
||||
tableModel.setRowCount(0);
|
||||
currentEntries.clear();
|
||||
String filter = queryField.getText().trim();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
|
||||
for (HostDirectoryEntry entry : allEntries) {
|
||||
if (!matchesFilter(entry, filter)) {
|
||||
continue;
|
||||
}
|
||||
currentEntries.add(entry);
|
||||
if (hostType == FTConfig.HostType.CMS && entry instanceof CMSDirectoryEntry) {
|
||||
CMSDirectoryEntry cms = (CMSDirectoryEntry) entry;
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||
cms.getFilename() != null ? cms.getFilename() : "",
|
||||
cms.getFiletype() != null ? cms.getFiletype() : "",
|
||||
cms.getFilemode() != null ? cms.getFilemode() : "",
|
||||
cms.getRecfm() != null ? cms.getRecfm() : "",
|
||||
cms.getLrecl() > 0 ? String.valueOf(cms.getLrecl()) : "",
|
||||
cms.getNumRecords() > 0 ? String.valueOf(cms.getNumRecords()) : "",
|
||||
(cms.getDate() != null ? cms.getDate() : "") + " " + (cms.getTime() != null ? cms.getTime() : "")
|
||||
});
|
||||
} else if (entry instanceof TSODirectoryEntry) {
|
||||
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
|
||||
tableModel.addRow(new Object[]{
|
||||
tso.getDatasetName() != null ? tso.getDatasetName() : "",
|
||||
tso.getVolume() != null ? tso.getVolume() : ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space.");
|
||||
if (!allEntries.isEmpty()) {
|
||||
statusLabel.setText(currentEntries.size() + " entries shown (" + allEntries.size() + " total).");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesFilter(HostDirectoryEntry entry, String filter) {
|
||||
if (filter == null || filter.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String cleanFilter = filter.replace("'", "").replace("\"", "").trim();
|
||||
if (cleanFilter.isEmpty() || cleanFilter.equals("*") || cleanFilter.equals("* * *") || cleanFilter.equals("*.*")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry instanceof CMSDirectoryEntry) {
|
||||
return matchesCmsFilter((CMSDirectoryEntry) entry, cleanFilter);
|
||||
} else if (entry instanceof TSODirectoryEntry) {
|
||||
return matchesTsoFilter((TSODirectoryEntry) entry, cleanFilter);
|
||||
} else {
|
||||
return matchesWildcard(entry.getName(), cleanFilter);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesCmsFilter(CMSDirectoryEntry cms, String filter) {
|
||||
String fn = cms.getFilename() != null ? cms.getFilename().trim() : "";
|
||||
String ft = cms.getFiletype() != null ? cms.getFiletype().trim() : "";
|
||||
String fm = cms.getFilemode() != null ? cms.getFilemode().trim() : "";
|
||||
|
||||
String[] tokens = filter.split("\\s+");
|
||||
if (tokens.length >= 3) {
|
||||
String fnPat = tokens[0];
|
||||
String ftPat = tokens[1];
|
||||
String fmPat = tokens[2];
|
||||
return matchesWildcard(fn, fnPat)
|
||||
&& matchesWildcard(ft, ftPat)
|
||||
&& matchesCmsFilemode(fm, fmPat);
|
||||
} else if (tokens.length == 2) {
|
||||
String fnPat = tokens[0];
|
||||
String ftPat = tokens[1];
|
||||
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
|
||||
} else {
|
||||
String tok = tokens[0];
|
||||
if (tok.contains(".")) {
|
||||
int dot = tok.indexOf('.');
|
||||
String fnPat = tok.substring(0, dot);
|
||||
String ftPat = tok.substring(dot + 1);
|
||||
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
|
||||
}
|
||||
if (matchesWildcard(fn, tok) || matchesWildcard(ft, tok) || matchesCmsFilemode(fm, tok)) {
|
||||
return true;
|
||||
}
|
||||
return matchesWildcard(cms.getName(), tok);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesCmsFilemode(String actualFm, String fmPattern) {
|
||||
if (fmPattern == null || fmPattern.isEmpty() || fmPattern.equals("*")) {
|
||||
return true;
|
||||
}
|
||||
if (actualFm == null || actualFm.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (fmPattern.length() == 1 && Character.isLetter(fmPattern.charAt(0))) {
|
||||
return actualFm.toUpperCase().startsWith(fmPattern.toUpperCase());
|
||||
}
|
||||
return matchesWildcard(actualFm, fmPattern);
|
||||
}
|
||||
|
||||
public static boolean matchesTsoFilter(TSODirectoryEntry tso, String filter) {
|
||||
String dsn = tso.getDatasetName() != null ? tso.getDatasetName().trim() : "";
|
||||
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
|
||||
return true;
|
||||
}
|
||||
if (filter.contains("*") || filter.contains("?")) {
|
||||
return matchesWildcard(dsn, filter);
|
||||
}
|
||||
return dsn.toUpperCase().contains(filter.toUpperCase());
|
||||
}
|
||||
|
||||
public static boolean matchesWildcard(String text, String pattern) {
|
||||
if (text == null) text = "";
|
||||
if (pattern == null || pattern.isEmpty() || pattern.equals("*")) return true;
|
||||
|
||||
StringBuilder regex = new StringBuilder("^");
|
||||
for (int i = 0; i < pattern.length(); i++) {
|
||||
char c = pattern.charAt(i);
|
||||
if (c == '*') {
|
||||
regex.append(".*");
|
||||
} else if (c == '?') {
|
||||
regex.append(".");
|
||||
} else if ("\\.[]{}()+^$|".indexOf(c) >= 0) {
|
||||
regex.append("\\").append(c);
|
||||
} else {
|
||||
regex.append(Character.toUpperCase(c));
|
||||
}
|
||||
}
|
||||
regex.append("$");
|
||||
return text.toUpperCase().matches(regex.toString());
|
||||
}
|
||||
|
||||
private void onConfirmSelection() {
|
||||
@@ -256,6 +909,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
|
||||
HostDirectoryEntry entry = currentEntries.get(row);
|
||||
selectedHostFile = entry.getName();
|
||||
selectedEntry = entry;
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
@@ -267,4 +921,50 @@ public class HostDirectoryDialog extends JDialog {
|
||||
public String getSelectedHostFile() {
|
||||
return selectedHostFile;
|
||||
}
|
||||
|
||||
public HostDirectoryEntry getSelectedEntry() {
|
||||
return selectedEntry;
|
||||
}
|
||||
|
||||
public List<HostDirectoryEntry> getAllEntries() {
|
||||
return allEntries;
|
||||
}
|
||||
|
||||
public List<HostDirectoryEntry> getCurrentEntries() {
|
||||
return currentEntries;
|
||||
}
|
||||
|
||||
public JTextField getQueryField() {
|
||||
return queryField;
|
||||
}
|
||||
|
||||
public static String resolveDefaultQuery(FTConfig.HostType hostType, String initialQuery) {
|
||||
String defaultQuery = initialQuery != null ? initialQuery.trim() : "";
|
||||
if (hostType == FTConfig.HostType.CMS && (defaultQuery.isEmpty() || defaultQuery.equals("* * *") || defaultQuery.equals("*"))) {
|
||||
defaultQuery = "* * A";
|
||||
}
|
||||
return defaultQuery;
|
||||
}
|
||||
|
||||
public static String formatCMSFilter(String pattern) {
|
||||
if (pattern == null || pattern.trim().isEmpty()) {
|
||||
return "* * A";
|
||||
}
|
||||
String clean = pattern.replace("'", "").replace("\"", "").trim();
|
||||
if (clean.equals("* * *") || clean.equals("*")) {
|
||||
return "* * A";
|
||||
} else if (clean.contains(".")) {
|
||||
int dot = clean.indexOf('.');
|
||||
return clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
|
||||
} else {
|
||||
String[] parts = clean.split("\\s+");
|
||||
if (parts.length == 1) {
|
||||
return parts[0] + " * A";
|
||||
} else if (parts.length == 2) {
|
||||
return parts[0] + " " + parts[1] + " A";
|
||||
} else {
|
||||
return parts[0] + " " + parts[1] + " " + parts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public class HotspotEngine {
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5"
|
||||
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5", ISPF (" 0 Settings", " 1 View", "10 SCLM", " X Exit")
|
||||
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
|
||||
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
|
||||
);
|
||||
@@ -138,11 +138,18 @@ public class HotspotEngine {
|
||||
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
||||
);
|
||||
|
||||
private static final Pattern ISPF_MENU_PATTERN = Pattern.compile(
|
||||
"(?:^\\s*|(?<=\\s{3,}))(?:(\\d{1,2}(?:\\.\\d{1,2})?)(?:\\s*[-–—]\\s*|\\s+)|([A-Za-z])(?:\\s*[-–—]\\s*|\\s{2,}))([A-Za-z][A-Za-z0-9/_-]*)"
|
||||
);
|
||||
|
||||
// Caching state to avoid rescanning unchanged screens
|
||||
private String cachedScreenText = "";
|
||||
private int cachedRows = 0;
|
||||
private int cachedCols = 0;
|
||||
private List<Hotspot> cachedHotspots = Collections.emptyList();
|
||||
private ScreenBuffer lastScreenBuffer;
|
||||
private long lastSnapshotVersion = -1;
|
||||
private long scanCount = 0;
|
||||
|
||||
public HotspotEngine() {
|
||||
}
|
||||
@@ -174,9 +181,19 @@ public class HotspotEngine {
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public void invalidateCache() {
|
||||
public synchronized void invalidateCache() {
|
||||
cachedScreenText = "";
|
||||
cachedHotspots = Collections.emptyList();
|
||||
lastScreenBuffer = null;
|
||||
lastSnapshotVersion = -1;
|
||||
}
|
||||
|
||||
public synchronized void markDirty() {
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public synchronized long getScanCount() {
|
||||
return scanCount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,6 +203,13 @@ public class HotspotEngine {
|
||||
if (sb == null) return Collections.emptyList();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
long version = sb.getSnapshotVersion();
|
||||
|
||||
if (sb == lastScreenBuffer && version == lastSnapshotVersion && rows == cachedRows && cols == cachedCols) {
|
||||
return cachedHotspots;
|
||||
}
|
||||
|
||||
scanCount++;
|
||||
|
||||
// Build plain text representation
|
||||
StringBuilder fullText = new StringBuilder(rows * (cols + 1));
|
||||
@@ -204,7 +228,10 @@ public class HotspotEngine {
|
||||
}
|
||||
}
|
||||
|
||||
return scanText(fullText.toString(), rows, cols);
|
||||
List<Hotspot> results = scanText(fullText.toString(), rows, cols);
|
||||
lastScreenBuffer = sb;
|
||||
lastSnapshotVersion = version;
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,6 +373,28 @@ public class HotspotEngine {
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
|
||||
// Pattern 3c: ISPF menu choices (e.g. " 1 View", " 2 Edit", "10 SCLM", " X Exit", " M More", " 1 - View", " 3.1 Library")
|
||||
Matcher mIspf = ISPF_MENU_PATTERN.matcher(line);
|
||||
while (mIspf.find()) {
|
||||
String choiceStr = mIspf.group(1) != null ? mIspf.group(1) : mIspf.group(2).toUpperCase();
|
||||
String labelStr = mIspf.group(3);
|
||||
int startCol = mIspf.group(1) != null ? mIspf.start(1) : mIspf.start(2);
|
||||
int endCol = mIspf.end(3);
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String action = choiceStr + "[enter]";
|
||||
String tooltip = "Select option " + choiceStr + " (" + labelStr + ")";
|
||||
String display = line.substring(startCol, endCol);
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.MENU_CHOICE,
|
||||
r, startCol, r, endCol - 1,
|
||||
display, action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.nio.charset.StandardCharsets;
|
||||
*/
|
||||
public class ScriptDialog extends JDialog {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextArea scriptArea;
|
||||
@@ -32,6 +32,24 @@ public class ScriptDialog extends JDialog {
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public void setScriptText(String text) {
|
||||
if (scriptArea != null) {
|
||||
scriptArea.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public String getScriptText() {
|
||||
return scriptArea != null ? scriptArea.getText() : "";
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
@@ -11,12 +11,16 @@ import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
|
||||
public class SettingsDialog extends JDialog {
|
||||
|
||||
@@ -58,8 +62,7 @@ public class SettingsDialog extends JDialog {
|
||||
// Connection tab
|
||||
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
||||
private JPanel autoConnectPanel;
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JComboBox<SavedHost> savedHostBox;
|
||||
private JCheckBox autoReconnectCheck;
|
||||
private JSpinner dynamicRowsSpinner;
|
||||
private JSpinner dynamicColsSpinner;
|
||||
@@ -83,6 +86,8 @@ public class SettingsDialog extends JDialog {
|
||||
// Bottom action buttons
|
||||
private JButton btnImport;
|
||||
private JButton btnExport;
|
||||
private JButton btnImportConnections;
|
||||
private JButton btnExportConnections;
|
||||
|
||||
// Colors tab component tracking
|
||||
private final JPanel[] hostColorSwatches = new JPanel[16];
|
||||
@@ -94,7 +99,7 @@ public class SettingsDialog extends JDialog {
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
initComponents();
|
||||
setSize(660, 540);
|
||||
setSize(680, 650);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
@@ -107,13 +112,18 @@ public class SettingsDialog extends JDialog {
|
||||
tabbedPane.addTab("Connection", createConnectionPanel());
|
||||
tabbedPane.addTab("Entry Assist", createEntryAssistPanel());
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
|
||||
btnImport = new JButton("Import Config...");
|
||||
ThemeManager.styleButton(btnImport, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
btnExport = new JButton("Export Config...");
|
||||
ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
btnImportConnections = new JButton("Import Connections...");
|
||||
ThemeManager.styleButton(btnImportConnections, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
btnExportConnections = new JButton("Export Connections...");
|
||||
ThemeManager.styleButton(btnExportConnections, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
JButton btnApply = new JButton("Apply");
|
||||
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
@@ -131,6 +141,14 @@ public class SettingsDialog extends JDialog {
|
||||
exportConfig();
|
||||
});
|
||||
|
||||
btnImportConnections.addActionListener((ActionEvent e) -> {
|
||||
importConnections();
|
||||
});
|
||||
|
||||
btnExportConnections.addActionListener((ActionEvent e) -> {
|
||||
exportConnections();
|
||||
});
|
||||
|
||||
btnOk.addActionListener((ActionEvent e) -> {
|
||||
boolean success = applySettings();
|
||||
if (success) {
|
||||
@@ -146,12 +164,28 @@ public class SettingsDialog extends JDialog {
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(btnImport);
|
||||
buttonPanel.add(btnExport);
|
||||
buttonPanel.add(Box.createHorizontalStrut(20));
|
||||
buttonPanel.add(btnApply);
|
||||
buttonPanel.add(btnCancel);
|
||||
buttonPanel.add(btnOk);
|
||||
JPanel buttonPanel = new JPanel(new GridLayout(3, 1, 0, 4));
|
||||
buttonPanel.setBorder(BorderFactory.createEmptyBorder(4, 8, 8, 8));
|
||||
|
||||
JPanel row1 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
|
||||
row1.setOpaque(false);
|
||||
row1.add(btnImport);
|
||||
row1.add(btnExport);
|
||||
|
||||
JPanel row2 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
|
||||
row2.setOpaque(false);
|
||||
row2.add(btnImportConnections);
|
||||
row2.add(btnExportConnections);
|
||||
|
||||
JPanel row3 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
|
||||
row3.setOpaque(false);
|
||||
row3.add(btnApply);
|
||||
row3.add(btnCancel);
|
||||
row3.add(btnOk);
|
||||
|
||||
buttonPanel.add(row1);
|
||||
buttonPanel.add(row2);
|
||||
buttonPanel.add(row3);
|
||||
|
||||
getContentPane().add(tabbedPane, BorderLayout.CENTER);
|
||||
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
|
||||
@@ -499,7 +533,7 @@ public class SettingsDialog extends JDialog {
|
||||
gbcH.gridy = 2;
|
||||
hotGroup.add(hotspotsPfCheck, gbcH);
|
||||
|
||||
hotspotsMenuCheck = new JCheckBox("Auto-detect Menu choices (\"1.\", \"2.\", \"===> 3\")", Settings.getHotspotsMenuChoicesEnabled());
|
||||
hotspotsMenuCheck = new JCheckBox("Auto-detect Menu choices (ISPF, \"1.\", \"===> 3\")", Settings.getHotspotsMenuChoicesEnabled());
|
||||
ThemeManager.styleCheckBox(hotspotsMenuCheck);
|
||||
gbcH.gridy = 3;
|
||||
hotGroup.add(hotspotsMenuCheck, gbcH);
|
||||
@@ -588,22 +622,33 @@ public class SettingsDialog extends JDialog {
|
||||
acGbc.insets = new Insets(5, 5, 5, 5);
|
||||
acGbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
JLabel savedConnLabel = new JLabel("Saved Connection:");
|
||||
acGbc.gridx = 0;
|
||||
acGbc.gridy = 0;
|
||||
autoConnectPanel.add(hostLabel, acGbc);
|
||||
autoConnectPanel.add(savedConnLabel, acGbc);
|
||||
|
||||
hostField = new JTextField(Settings.getAutoConnectHost(), 15);
|
||||
savedHostBox = new JComboBox<>();
|
||||
savedHostBox.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
if (value instanceof SavedHost) {
|
||||
SavedHost sh = (SavedHost) value;
|
||||
if (sh.getName() != null && !sh.getName().trim().isEmpty()) {
|
||||
setText(sh.getName() + " (" + sh.getHost() + ":" + sh.getPort() + ")");
|
||||
} else {
|
||||
setText(sh.getHost() + ":" + sh.getPort());
|
||||
}
|
||||
} else if (value == null) {
|
||||
setText("(No saved connections)");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
refreshSavedHostsBox();
|
||||
acGbc.gridx = 1;
|
||||
autoConnectPanel.add(hostField, acGbc);
|
||||
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
acGbc.gridx = 2;
|
||||
autoConnectPanel.add(portLabel, acGbc);
|
||||
|
||||
portField = new JTextField(String.valueOf(Settings.getAutoConnectPort()), 4);
|
||||
acGbc.gridx = 3;
|
||||
autoConnectPanel.add(portField, acGbc);
|
||||
acGbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
autoConnectPanel.add(savedHostBox, acGbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
@@ -658,6 +703,30 @@ public class SettingsDialog extends JDialog {
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void refreshSavedHostsBox() {
|
||||
if (savedHostBox == null) return;
|
||||
savedHostBox.removeAllItems();
|
||||
List<SavedHost> savedHosts = HostStorage.getSavedHosts();
|
||||
SavedHost currentAutoHost = HostStorage.getAutoConnectHost();
|
||||
SavedHost toSelect = null;
|
||||
for (SavedHost sh : savedHosts) {
|
||||
savedHostBox.addItem(sh);
|
||||
if (currentAutoHost != null && Objects.equals(sh.getId(), currentAutoHost.getId())) {
|
||||
toSelect = sh;
|
||||
}
|
||||
}
|
||||
if (toSelect != null) {
|
||||
savedHostBox.setSelectedItem(toSelect);
|
||||
savedHostBox.setEnabled(true);
|
||||
} else if (!savedHosts.isEmpty()) {
|
||||
savedHostBox.setSelectedIndex(0);
|
||||
savedHostBox.setEnabled(true);
|
||||
} else {
|
||||
savedHostBox.addItem(null);
|
||||
savedHostBox.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private JPanel createEntryAssistPanel() {
|
||||
JPanel panel = new JPanel();
|
||||
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
|
||||
@@ -1187,11 +1256,32 @@ public class SettingsDialog extends JDialog {
|
||||
if (behavior != null) {
|
||||
Settings.setStartupBehavior(behavior);
|
||||
}
|
||||
if (behavior == Settings.StartupBehavior.AUTO_CONNECT && hostField != null && portField != null) {
|
||||
Settings.setAutoConnectHost(hostField.getText().trim());
|
||||
try {
|
||||
Settings.setAutoConnectPort(Integer.parseInt(portField.getText().trim()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
if (behavior == Settings.StartupBehavior.AUTO_CONNECT && savedHostBox != null) {
|
||||
Object sel = savedHostBox.getSelectedItem();
|
||||
if (sel instanceof SavedHost) {
|
||||
SavedHost selected = (SavedHost) sel;
|
||||
List<SavedHost> hosts = HostStorage.getSavedHosts();
|
||||
for (SavedHost h : hosts) {
|
||||
h.setAutoConnect(Objects.equals(h.getId(), selected.getId()));
|
||||
}
|
||||
HostStorage.saveAll(hosts);
|
||||
Settings.setAutoConnectHost(selected.getHost());
|
||||
Settings.setAutoConnectPort(selected.getPort());
|
||||
Settings.setAutoConnectTls(selected.isUseTls());
|
||||
Settings.setAutoConnectVerifyCert(selected.isTlsVerifyCert());
|
||||
Settings.setAutoConnectTn3270e(selected.isTn3270e());
|
||||
}
|
||||
} else if (behavior != Settings.StartupBehavior.AUTO_CONNECT) {
|
||||
List<SavedHost> hosts = HostStorage.getSavedHosts();
|
||||
boolean modified = false;
|
||||
for (SavedHost h : hosts) {
|
||||
if (h.isAutoConnect()) {
|
||||
h.setAutoConnect(false);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
HostStorage.saveAll(hosts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1371,12 +1461,7 @@ public class SettingsDialog extends JDialog {
|
||||
autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT);
|
||||
}
|
||||
}
|
||||
if (hostField != null) {
|
||||
hostField.setText(Settings.getAutoConnectHost());
|
||||
}
|
||||
if (portField != null) {
|
||||
portField.setText(String.valueOf(Settings.getAutoConnectPort()));
|
||||
}
|
||||
refreshSavedHostsBox();
|
||||
if (autoReconnectCheck != null) {
|
||||
autoReconnectCheck.setSelected(Settings.getAutoConnectAutoReconnect());
|
||||
}
|
||||
@@ -1456,7 +1541,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// ========== Import Config ==========
|
||||
|
||||
void importConfigFile(File file) throws IOException {
|
||||
public void importConfigFile(File file) throws IOException {
|
||||
if (file == null || !file.exists()) {
|
||||
throw new FileNotFoundException("Config file not found: " + (file != null ? file.getAbsolutePath() : "null"));
|
||||
}
|
||||
@@ -1494,6 +1579,102 @@ public class SettingsDialog extends JDialog {
|
||||
return btnExport;
|
||||
}
|
||||
|
||||
public JButton getBtnImportConnections() {
|
||||
return btnImportConnections;
|
||||
}
|
||||
|
||||
public JButton getBtnExportConnections() {
|
||||
return btnExportConnections;
|
||||
}
|
||||
|
||||
public JComboBox<SavedHost> getSavedHostBox() {
|
||||
return savedHostBox;
|
||||
}
|
||||
|
||||
// ========== Import / Export Connections ==========
|
||||
|
||||
public void importConnectionsFile(File file) throws IOException {
|
||||
if (file == null || !file.exists()) {
|
||||
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
|
||||
}
|
||||
HostStorage.importFromIniFile(file);
|
||||
refreshSavedHostsBox();
|
||||
SavedHost autoHost = HostStorage.getAutoConnectHost();
|
||||
if (autoHost != null) {
|
||||
Settings.setAutoConnectHost(autoHost.getHost());
|
||||
Settings.setAutoConnectPort(autoHost.getPort());
|
||||
Settings.setAutoConnectTls(autoHost.isUseTls());
|
||||
Settings.setAutoConnectVerifyCert(autoHost.isTlsVerifyCert());
|
||||
Settings.setAutoConnectTn3270e(autoHost.isTn3270e());
|
||||
}
|
||||
refreshFromSettings();
|
||||
}
|
||||
|
||||
public void exportConnectionsFile(File file) throws IOException {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("Target file cannot be null");
|
||||
}
|
||||
HostStorage.exportToIniFile(file);
|
||||
}
|
||||
|
||||
private void importConnections() {
|
||||
JFileChooser fc = new JFileChooser();
|
||||
fc.setDialogTitle("Import Connections");
|
||||
fc.setSelectedFile(new java.io.File("connections.ini"));
|
||||
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini"));
|
||||
|
||||
int result = fc.showOpenDialog(this);
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
java.io.File file = fc.getSelectedFile();
|
||||
try {
|
||||
importConnectionsFile(file);
|
||||
int count = HostStorage.getSavedHosts().size();
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Connections imported successfully from:\n" + file.getAbsolutePath() + "\nTotal saved connections: " + count,
|
||||
"Import Successful", JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Import connections failed: " + ex.getMessage(),
|
||||
"Import Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void exportConnections() {
|
||||
JFileChooser fc = new JFileChooser();
|
||||
fc.setDialogTitle("Export Connections");
|
||||
fc.setSelectedFile(new java.io.File("connections.ini"));
|
||||
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini"));
|
||||
|
||||
int result = fc.showSaveDialog(this);
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
java.io.File file = fc.getSelectedFile();
|
||||
// Ensure .ini extension
|
||||
if (!file.getName().toLowerCase().endsWith(".ini")) {
|
||||
file = new java.io.File(file.getAbsolutePath() + ".ini");
|
||||
}
|
||||
|
||||
// Confirm overwrite
|
||||
if (file.exists()) {
|
||||
int confirm = JOptionPane.showConfirmDialog(this,
|
||||
"File already exists. Overwrite?", "Confirm Overwrite",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
}
|
||||
|
||||
try {
|
||||
exportConnectionsFile(file);
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Connections exported to:\n" + file.getAbsolutePath(),
|
||||
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Export connections failed: " + ex.getMessage(),
|
||||
"Export Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Export Config ==========
|
||||
|
||||
private void exportConfig() {
|
||||
|
||||
@@ -357,9 +357,12 @@ public class StatusBar extends JPanel {
|
||||
wordWrapStatus.setText("");
|
||||
}
|
||||
|
||||
// Field status: Numeric vs Alphanumeric
|
||||
// Field status: Numeric vs Alphanumeric vs DBCS
|
||||
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
|
||||
if (client.getOIA().isNumeric()) {
|
||||
if (client.getOIA().isDBCS()) {
|
||||
fieldTypeStatus.setText("DBCS");
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
} else if (client.getOIA().isNumeric()) {
|
||||
fieldTypeStatus.setText("NUM");
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,12 @@ import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.event.InputMethodEvent;
|
||||
import java.awt.event.InputMethodListener;
|
||||
import java.awt.font.TextHitInfo;
|
||||
import java.awt.im.InputMethodRequests;
|
||||
import java.text.AttributedCharacterIterator;
|
||||
import java.text.CharacterIterator;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
@@ -18,7 +24,8 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
* Supports colors, bold, underline, reverse, blink, GOCA vector graphics,
|
||||
* programmed symbols, search highlighting, and all 3278/3279 attributes.
|
||||
*/
|
||||
public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
public class TerminalPanel extends JPanel implements java.awt.print.Printable, InputMethodListener, InputMethodRequests {
|
||||
|
||||
|
||||
private Telnet3270Client client;
|
||||
|
||||
@@ -29,7 +36,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
int cellHeight;
|
||||
private int fontAscent;
|
||||
private int fontDescent;
|
||||
private int padding = 4;
|
||||
private int padding = 1;
|
||||
private int currentFontSize = 16;
|
||||
|
||||
// Cursor state
|
||||
@@ -57,6 +64,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
CHAR_STRINGS[i] = String.valueOf((char) i);
|
||||
}
|
||||
}
|
||||
// Reusable single-character buffer for drawing non-ASCII cells (Phase 27.4 / T1-A)
|
||||
private final char[] charScratch = new char[1];
|
||||
public enum CursorStyle {
|
||||
BLOCK,
|
||||
UNDERLINE
|
||||
@@ -96,15 +105,21 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private long lastGraphicsUpdateCount = -1;
|
||||
private java.awt.image.BufferedImage cachedGraphicsImage = null;
|
||||
private boolean resizeGuard = false;
|
||||
private Integer targetModelRows = null;
|
||||
private Integer targetModelCols = null;
|
||||
private StatusBar statusBar = null;
|
||||
|
||||
// ========== Phase 25: Hotspots & Mouse Wheel ==========
|
||||
private final HotspotEngine hotspotEngine = new HotspotEngine();
|
||||
private HotspotEngine.Hotspot hoveredHotspot = null;
|
||||
private static final Color HOTSPOT_HOVER_COLOR = new Color(0, 220, 255);
|
||||
private static final Stroke HOTSPOT_HOVER_STROKE = new BasicStroke(2.0f);
|
||||
private boolean hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled();
|
||||
private boolean mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled();
|
||||
private String mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey();
|
||||
private String mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey();
|
||||
private boolean hotspotsDirty = true;
|
||||
private boolean[] selectedCellsScratch = new boolean[0];
|
||||
|
||||
// ========== Mode and Listener State ==========
|
||||
private boolean insertMode = false;
|
||||
@@ -134,13 +149,21 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
/**
|
||||
* Compute the horizontal render offset to center the grid within the panel.
|
||||
* The left bound of the window is the left bound of the left cells when constrained.
|
||||
* Only if the window is too big is there blank space on the left, and it is equal
|
||||
* to or less than the space on the right at all times.
|
||||
*/
|
||||
int getRenderOffsetX() {
|
||||
int termCols = 80;
|
||||
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
|
||||
if (client != null && client.getScreenBuffer() != null) {
|
||||
termCols = client.getScreenBuffer().getDisplayCols();
|
||||
}
|
||||
int gridW = termCols * cellWidth;
|
||||
int extra = getWidth() - gridW;
|
||||
return Math.max(padding, extra / 2);
|
||||
if (extra > 0) {
|
||||
return extra / 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,10 +171,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
*/
|
||||
int getRenderOffsetY() {
|
||||
int termRows = 24;
|
||||
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
|
||||
if (client != null && client.getScreenBuffer() != null) {
|
||||
termRows = client.getScreenBuffer().getDisplayRows();
|
||||
}
|
||||
int gridH = termRows * cellHeight;
|
||||
int extra = getHeight() - gridH;
|
||||
return Math.max(padding, extra / 2);
|
||||
if (extra > 0) {
|
||||
return extra / 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Default Host color mapping aligned 1:1 with IBM Host On-Demand ColorRemapModel3270
|
||||
@@ -201,20 +229,28 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
|
||||
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
|
||||
|
||||
enableInputMethods(true);
|
||||
addInputMethodListener(this);
|
||||
|
||||
|
||||
// Handle mouse clicks to position cursor, selection, and grab focus
|
||||
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
Window win = SwingUtilities.getWindowAncestor(TerminalPanel.this);
|
||||
boolean wasWindowInactive = (win != null && (!win.isActive() || !win.isFocused()));
|
||||
boolean wasPanelUnfocused = !hasFocus();
|
||||
boolean wasPanelUnfocused = !java.awt.GraphicsEnvironment.isHeadless() && !hasFocus();
|
||||
isActivationClick = wasWindowInactive || wasPanelUnfocused;
|
||||
|
||||
if (wasWindowInactive) {
|
||||
if (win != null && wasWindowInactive) {
|
||||
win.toFront();
|
||||
win.requestFocus();
|
||||
}
|
||||
requestFocus();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (!hasFocus()) {
|
||||
requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
// Right-click context menu
|
||||
if (SwingUtilities.isRightMouseButton(e)) {
|
||||
@@ -370,7 +406,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
if (row >= 0 && row < rows && col >= 0 && col < cols) {
|
||||
if (hotspotsDirty) {
|
||||
hotspotEngine.scan(sb);
|
||||
hotspotsDirty = false;
|
||||
}
|
||||
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
|
||||
if (h != null) {
|
||||
if (hoveredHotspot != h) {
|
||||
@@ -407,6 +446,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (!hasFocus()) {
|
||||
requestFocus();
|
||||
}
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
if (activeDragHandle != DragHandle.NONE) {
|
||||
@@ -482,11 +524,27 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
// Phase 25.2: Interactive Hotspot click execution (URLs, PF keys, choices)
|
||||
if (hotspotsEnabled && client.getConnectionState().isFullSession()) {
|
||||
if (hotspotsDirty) {
|
||||
hotspotEngine.scan(sb);
|
||||
hotspotsDirty = false;
|
||||
}
|
||||
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
|
||||
if (h != null) {
|
||||
if (h.getType() == HotspotEngine.HotspotType.URL) {
|
||||
openUrl(h.getAction());
|
||||
return;
|
||||
} else if (h.getType() == HotspotEngine.HotspotType.MENU_CHOICE) {
|
||||
// In 3270 / ISPF, clicking on a menu choice hotspot positions the cursor
|
||||
// on a protected screen label. Typing directly into a protected cell triggers
|
||||
// INHIBIT_PROTECTED_FIELD keyboard lock. Menu choices must be submitted via
|
||||
// the command/option line (first unprotected field reachable via [home]).
|
||||
String action = h.getAction();
|
||||
if (!action.startsWith("[home]")) {
|
||||
action = "[home][eraseeof]" + action;
|
||||
}
|
||||
client.getPS().SendKeys(action);
|
||||
refreshScreen();
|
||||
return;
|
||||
} else {
|
||||
client.getPS().SendKeys(h.getAction());
|
||||
refreshScreen();
|
||||
@@ -510,13 +568,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
boolean isToggleField = (desEbcdic == 0x6F || desAscii == '?' || desAscii == '\uFF1F' ||
|
||||
desEbcdic == 0x6E || desAscii == '>' || desAscii == '\uFF1E');
|
||||
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
|
||||
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
|
||||
|
||||
boolean isDoubleClick = (e.getClickCount() >= 2);
|
||||
// For detectable fields (FA bits 4-5 = 01 or 11):
|
||||
// Double-clicking or invoking mnemonic [cursel] toggles the designator character (? -> >).
|
||||
// If field has immediate select attribute, immediately transmit Cursor Select AID (0x7E).
|
||||
if (lightPenMode || isDoubleClick || isToggleField || isEnterField || !isProtected) {
|
||||
// Automatic mouse selection applies to toggle fields (? -> >) and enter fields (&).
|
||||
// Standard text fields (space/null designator) must NOT trigger light-pen selection
|
||||
// on normal mouse clicks, so users can position cursor and edit text normally (HoD parity).
|
||||
// Hardware light-pen emulation on space designators only fires when lightPenMode is active.
|
||||
if (lightPenMode || isToggleField || isEnterField) {
|
||||
if (client.lightPenSelect(clickAddr)) {
|
||||
refreshScreen();
|
||||
return;
|
||||
@@ -700,6 +757,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void select(int startRow, int startCol, int endRow, int endCol) {
|
||||
selectionStartRow = startRow;
|
||||
selectionStartCol = startCol;
|
||||
selectionEndRow = endRow;
|
||||
selectionEndCol = endCol;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public boolean hasSelection() {
|
||||
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
|
||||
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
|
||||
@@ -725,13 +790,25 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
for (int c = c1; c <= c2; c++) {
|
||||
int baddr = r * cols + c;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
if (ea.isFieldAttribute()) {
|
||||
line.append(' ');
|
||||
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
|
||||
if (c > c1) {
|
||||
continue; // Character already added by DB_LEFT
|
||||
} else {
|
||||
line.append(' ');
|
||||
}
|
||||
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
|
||||
line.append(' ');
|
||||
} else {
|
||||
char ch = ea.ucs4;
|
||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||
if (ch <= 0x20 || ch == 0xFF) {
|
||||
line.append(' ');
|
||||
} else {
|
||||
line.append(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
|
||||
int end = line.length();
|
||||
while (end > 0 && line.charAt(end - 1) == ' ') {
|
||||
@@ -767,13 +844,25 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
prevRow = curRow;
|
||||
}
|
||||
ExtendedAttribute ea = sb.getCell(addr);
|
||||
if (ea.isFieldAttribute()) {
|
||||
line.append(' ');
|
||||
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
|
||||
if (addr > startAddr) {
|
||||
continue; // Character already added by DB_LEFT
|
||||
} else {
|
||||
line.append(' ');
|
||||
}
|
||||
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
|
||||
line.append(' ');
|
||||
} else {
|
||||
char ch = ea.ucs4;
|
||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||
if (ch <= 0x20 || ch == 0xFF) {
|
||||
line.append(' ');
|
||||
} else {
|
||||
line.append(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
int end = line.length();
|
||||
while (end > 0 && line.charAt(end - 1) == ' ') {
|
||||
end--;
|
||||
@@ -989,7 +1078,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
popup.show(this, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
private boolean isCellSelected(int row, int col) {
|
||||
public boolean isCellSelected(int row, int col) {
|
||||
if (!hasSelection()) return false;
|
||||
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
@@ -1001,7 +1090,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
||||
} else {
|
||||
int cols = 80;
|
||||
if (client != null) cols = client.getScreenBuffer().getCols();
|
||||
if (client != null && client.getScreenBuffer() != null) cols = client.getScreenBuffer().getDisplayCols();
|
||||
int addr = row * cols + col;
|
||||
int startAddr, endAddr;
|
||||
if (selectionStartRow < selectionEndRow ||
|
||||
@@ -1016,6 +1105,78 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
public void precomputeSelectionScratch(int rows, int cols) {
|
||||
int totalCells = rows * cols;
|
||||
if (selectedCellsScratch == null || selectedCellsScratch.length < totalCells) {
|
||||
selectedCellsScratch = new boolean[totalCells];
|
||||
}
|
||||
java.util.Arrays.fill(selectedCellsScratch, 0, totalCells, false);
|
||||
if (hasSelection()) {
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
if (blockSelectMode || trimRectSizingHandles) {
|
||||
int c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
int minR = Math.max(0, r1);
|
||||
int maxR = Math.min(rows - 1, r2);
|
||||
int minC = Math.max(0, c1);
|
||||
int maxC = Math.min(cols - 1, c2);
|
||||
if (minC <= maxC) {
|
||||
for (int r = minR; r <= maxR; r++) {
|
||||
int rowBase = r * cols;
|
||||
java.util.Arrays.fill(selectedCellsScratch, rowBase + minC, rowBase + maxC + 1, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int startAddr, endAddr;
|
||||
if (selectionStartRow < selectionEndRow ||
|
||||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||
startAddr = selectionStartRow * cols + selectionStartCol;
|
||||
endAddr = selectionEndRow * cols + selectionEndCol;
|
||||
} else {
|
||||
startAddr = selectionEndRow * cols + selectionEndCol;
|
||||
endAddr = selectionStartRow * cols + selectionStartCol;
|
||||
}
|
||||
int s = Math.max(0, startAddr);
|
||||
int e = Math.min(totalCells - 1, endAddr);
|
||||
if (s <= e) {
|
||||
java.util.Arrays.fill(selectedCellsScratch, s, e + 1, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean[] getSelectedCellsScratch() {
|
||||
return selectedCellsScratch;
|
||||
}
|
||||
|
||||
|
||||
public void packForModel(int modelRows, int modelCols) {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(() -> packForModel(modelRows, modelCols));
|
||||
return;
|
||||
}
|
||||
resizeGuard = true;
|
||||
updateCellSize();
|
||||
this.targetModelRows = modelRows;
|
||||
this.targetModelCols = modelCols;
|
||||
revalidate();
|
||||
Container top = getTopLevelAncestor();
|
||||
if (top instanceof java.awt.Window) {
|
||||
java.awt.Window win = (java.awt.Window) top;
|
||||
if (win instanceof java.awt.Frame) {
|
||||
if ((((java.awt.Frame) win).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) {
|
||||
win.pack();
|
||||
}
|
||||
} else {
|
||||
win.pack();
|
||||
}
|
||||
}
|
||||
this.targetModelRows = null;
|
||||
this.targetModelCols = null;
|
||||
repaint();
|
||||
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
||||
}
|
||||
|
||||
public void guardedPack() {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
@@ -1025,12 +1186,35 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
resizeGuard = true;
|
||||
revalidate();
|
||||
Container top = getTopLevelAncestor();
|
||||
if (top instanceof java.awt.Frame) {
|
||||
if ((((java.awt.Frame) top).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) {
|
||||
((java.awt.Window) top).pack();
|
||||
if (top instanceof java.awt.Window) {
|
||||
java.awt.Window win = (java.awt.Window) top;
|
||||
if (win.isShowing()) {
|
||||
int termCols = 80;
|
||||
int termRows = 24;
|
||||
if (client != null && client.getScreenBuffer() != null) {
|
||||
termCols = client.getScreenBuffer().getDisplayCols();
|
||||
termRows = client.getScreenBuffer().getDisplayRows();
|
||||
}
|
||||
int gridW = termCols * cellWidth;
|
||||
int gridH = termRows * cellHeight;
|
||||
Dimension winSize = win.getSize();
|
||||
Dimension winPref = win.getPreferredSize();
|
||||
boolean tooSmall = winSize.width < winPref.width || winSize.height < winPref.height
|
||||
|| (getWidth() > 0 && getWidth() < gridW)
|
||||
|| (getHeight() > 0 && getHeight() < gridH);
|
||||
if (tooSmall) {
|
||||
repaint();
|
||||
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (win instanceof java.awt.Frame) {
|
||||
if ((((java.awt.Frame) win).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) {
|
||||
win.pack();
|
||||
}
|
||||
} else {
|
||||
win.pack();
|
||||
}
|
||||
} else if (top instanceof java.awt.Window) {
|
||||
((java.awt.Window) top).pack();
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
||||
}
|
||||
@@ -1572,6 +1756,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
|
||||
public void applyModeSettings() {
|
||||
hotspotsDirty = true;
|
||||
if (client != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
if (sb != null) {
|
||||
@@ -1608,6 +1793,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
|
||||
public void refreshScreen() {
|
||||
hotspotsDirty = true;
|
||||
if (client != null) {
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
}
|
||||
@@ -1625,6 +1811,26 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
public void onScreenUpdated() {
|
||||
hotspotsDirty = true;
|
||||
}
|
||||
|
||||
public void onScreenSizeChanged() {
|
||||
hotspotsDirty = true;
|
||||
}
|
||||
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
hotspotsDirty = true;
|
||||
}
|
||||
|
||||
public boolean isHotspotsDirty() {
|
||||
return hotspotsDirty;
|
||||
}
|
||||
|
||||
public void setHotspotsDirty(boolean hotspotsDirty) {
|
||||
this.hotspotsDirty = hotspotsDirty;
|
||||
}
|
||||
|
||||
private void setupFont() {
|
||||
currentFontSize = haus.nightmare.j3270.config.Settings.getFontSize();
|
||||
String fontFamily = haus.nightmare.j3270.config.Settings.getFontFamily();
|
||||
@@ -1765,6 +1971,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
hotspotsDirty = true;
|
||||
if (client != null) {
|
||||
setupGraphicsPlaneRenderer();
|
||||
updateCellSize();
|
||||
@@ -1803,7 +2010,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
Container top = getTopLevelAncestor();
|
||||
if (top instanceof JFrame) {
|
||||
((JFrame) top).setTitle(title != null && !title.isEmpty() ? ("j3270 - " + title) : "j3270");
|
||||
((JFrame) top).setTitle(title != null && !title.isEmpty() ? ("j3270 — " + title) : "j3270 — Java TN3270 Terminal Emulator");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1873,7 +2080,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
if (client != null) {
|
||||
if (targetModelRows != null && targetModelCols != null) {
|
||||
return new Dimension(targetModelCols * cellWidth + padding * 2,
|
||||
targetModelRows * cellHeight + padding * 2);
|
||||
}
|
||||
if (client != null && client.getScreenBuffer() != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int displayCols = sb.getDisplayCols();
|
||||
int displayRows = sb.getDisplayRows();
|
||||
@@ -1957,6 +2168,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
g2.fillRect(cx, oy, cellWidth, gridH);
|
||||
}
|
||||
|
||||
// Phase 28.2 / T2-B: Precompute selection scratch lookup once per frame
|
||||
precomputeSelectionScratch(rows, cols);
|
||||
|
||||
// Layer 3: Character / Text Plane
|
||||
byte currentFA = 0;
|
||||
ExtendedAttribute currentFieldEa = null;
|
||||
@@ -1978,7 +2192,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (ea.isFieldAttribute()) {
|
||||
currentFA = ea.fa;
|
||||
currentFieldEa = ea;
|
||||
if (isCellSelected(row, col)) {
|
||||
if (selectedCellsScratch[baddr]) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
@@ -2023,20 +2237,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
// Field Outlining (0xC2 / TY_FOUTLINE)
|
||||
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
|
||||
|
||||
if (isCellSelected(row, col)) {
|
||||
if (selectedCellsScratch[baddr]) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
byte tr = ea.tr != 0 ? ea.tr : (currentFieldEa != null ? currentFieldEa.tr : 0);
|
||||
boolean isOpaque = ((tr & 0xFF) == XAT_OPAQUE);
|
||||
|
||||
if (reverse) {
|
||||
Color tmp = fgColor;
|
||||
fgColor = (rawBg == 0) ? this.bgColor : bgColor;
|
||||
bgColor = tmp;
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
} else if (bgIsExplicit || !bgColor.equals(this.bgColor)) {
|
||||
} else if (isOpaque || bgIsExplicit || !bgColor.equals(this.bgColor)) {
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
@@ -2051,7 +2268,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (cs >= 0x40 && client.getProgramSymbolManager() != null && !suppressGlyph) {
|
||||
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
||||
if (slot != null) {
|
||||
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
|
||||
int symBg = (bgIsExplicit || reverse || isOpaque) ? bgColor.getRGB() : 0;
|
||||
haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg);
|
||||
java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb);
|
||||
if (img != null) {
|
||||
@@ -2061,7 +2278,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawnAsPs && !suppressGlyph) {
|
||||
if (!drawnAsPs && !suppressGlyph && ea.db != ExtendedAttribute.DB_RIGHT
|
||||
&& ea.db != ExtendedAttribute.DB_SO && ea.db != ExtendedAttribute.DB_SI) {
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
@@ -2071,7 +2289,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (ch < 128) {
|
||||
g2.drawString(CHAR_STRINGS[ch], x, textY);
|
||||
} else {
|
||||
g2.drawString(String.valueOf(ch), x, textY);
|
||||
charScratch[0] = ch;
|
||||
g2.drawChars(charScratch, 0, 1, x, textY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2092,7 +2311,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
|
||||
// Draw selection highlight (40% alpha blend over cell)
|
||||
if (isCellSelected(row, col)) {
|
||||
if (selectedCellsScratch[baddr]) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
@@ -2138,8 +2357,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (hotspotsEnabled && hoveredHotspot != null) {
|
||||
Color oldColor = g2.getColor();
|
||||
Stroke oldStroke = g2.getStroke();
|
||||
g2.setColor(new Color(0, 220, 255)); // Bright cyan hotspot underline
|
||||
g2.setStroke(new BasicStroke(2.0f));
|
||||
g2.setColor(HOTSPOT_HOVER_COLOR); // Bright cyan hotspot underline
|
||||
g2.setStroke(HOTSPOT_HOVER_STROKE);
|
||||
|
||||
int hr0 = hoveredHotspot.getStartRow();
|
||||
int hr1 = hoveredHotspot.getEndRow();
|
||||
@@ -2200,11 +2419,17 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
boolean isUnderline = (cursorStyle == CursorStyle.UNDERLINE) ||
|
||||
(client.getInputProcessor() != null && client.getInputProcessor().isInsertMode());
|
||||
g2.setColor(CURSOR_COLOR);
|
||||
ExtendedAttribute curEa = (curAddr >= 0 && curAddr < sb.getSize()) ? sb.getDisplayCell(curAddr) : null;
|
||||
boolean isDbcs = curEa != null && (curEa.db == ExtendedAttribute.DB_LEFT || curEa.db == ExtendedAttribute.DB_RIGHT);
|
||||
int cWidth = isDbcs ? (cellWidth * 2) : cellWidth;
|
||||
if (isDbcs && curEa.db == ExtendedAttribute.DB_RIGHT && curCol > 0) {
|
||||
cx -= cellWidth;
|
||||
}
|
||||
if (isUnderline) {
|
||||
int ulH = Math.max(2, cellHeight / 6);
|
||||
g2.fillRect(cx, cy + cellHeight - ulH, cellWidth, ulH);
|
||||
g2.fillRect(cx, cy + cellHeight - ulH, cWidth, ulH);
|
||||
} else {
|
||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||
g2.fillRect(cx, cy, cWidth, cellHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2393,4 +2618,90 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
paint(g2d);
|
||||
return PAGE_EXISTS;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Java Input Method Framework (IMF / IME) Support
|
||||
// =========================================================================
|
||||
|
||||
@Override
|
||||
public InputMethodRequests getInputMethodRequests() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inputMethodTextChanged(InputMethodEvent event) {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
event.consume();
|
||||
return;
|
||||
}
|
||||
AttributedCharacterIterator text = event.getText();
|
||||
if (text != null) {
|
||||
int committedCount = event.getCommittedCharacterCount();
|
||||
char c = text.first();
|
||||
for (int i = 0; i < committedCount && c != CharacterIterator.DONE; i++) {
|
||||
client.typeCharacter(c);
|
||||
c = text.next();
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void caretPositionChanged(InputMethodEvent event) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle getTextLocation(TextHitInfo offset) {
|
||||
Point p;
|
||||
try {
|
||||
p = getLocationOnScreen();
|
||||
} catch (IllegalComponentStateException e) {
|
||||
p = new Point(0, 0);
|
||||
}
|
||||
int curRow = 0, curCol = 0;
|
||||
if (client != null && client.getScreenBuffer() != null) {
|
||||
int cols = client.getScreenBuffer().getCols();
|
||||
int curAddr = client.getScreenBuffer().getDisplayCursorAddress();
|
||||
if (cols > 0) {
|
||||
curRow = curAddr / cols;
|
||||
curCol = curAddr % cols;
|
||||
}
|
||||
}
|
||||
int cx = p.x + getRenderOffsetX() + curCol * cellWidth;
|
||||
int cy = p.y + getRenderOffsetY() + curRow * cellHeight;
|
||||
return new Rectangle(cx, cy, cellWidth, cellHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHitInfo getLocationOffset(int x, int y) {
|
||||
return TextHitInfo.leading(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInsertPositionOffset() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCommittedTextLength() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.ConnectChoiceDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionConflictTest {
|
||||
|
||||
private final List<J3270App> createdApps = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
for (J3270App app : createdApps) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private J3270App createApp() {
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
createdApps.add(app);
|
||||
return app;
|
||||
} catch (HeadlessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectChoiceDialog components, styling, and action results")
|
||||
public void testConnectChoiceDialogComponents() {
|
||||
try {
|
||||
ConnectChoiceDialog dialog = new ConnectChoiceDialog(null, "mvs.corp.local:23");
|
||||
assertEquals("Active Connection", dialog.getTitle());
|
||||
assertNotNull(dialog.getNewWindowButton());
|
||||
assertNotNull(dialog.getAbortButton());
|
||||
assertNotNull(dialog.getCancelButton());
|
||||
assertEquals("New Window", dialog.getNewWindowButton().getText());
|
||||
assertEquals("Abort Current Connection", dialog.getAbortButton().getText());
|
||||
assertEquals("Cancel", dialog.getCancelButton().getText());
|
||||
|
||||
assertTrue(dialog.getLine1().getText().contains("mvs.corp.local:23"));
|
||||
assertTrue(dialog.getLine2().getText().contains("new window"));
|
||||
assertTrue(dialog.getLine2().getText().contains("abort"));
|
||||
|
||||
// Test button click choices
|
||||
dialog.getNewWindowButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.NEW_WINDOW, dialog.getChoice());
|
||||
|
||||
ConnectChoiceDialog dialog2 = new ConnectChoiceDialog(null, "cms.test:992");
|
||||
dialog2.getAbortButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.ABORT_CURRENT, dialog2.getChoice());
|
||||
|
||||
ConnectChoiceDialog dialog3 = new ConnectChoiceDialog(null, "cics.test:23");
|
||||
dialog3.getCancelButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.CANCEL, dialog3.getChoice());
|
||||
} catch (HeadlessException ignored) {
|
||||
// Expected in headless CI environments without X11
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with NEW_WINDOW choice opens in a new window and preserves current connection")
|
||||
public void testConnectChoiceNewWindow() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
config1.setSavedProfileName("Host 1");
|
||||
app.connect(config1, false);
|
||||
|
||||
assertTrue(app.getConnectedTarget().contains("host1.test") || app.getConnectedTarget().contains("Host 1"));
|
||||
|
||||
// Configure handler to return NEW_WINDOW
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
assertTrue(target.contains("host1.test") || target.contains("Host 1"));
|
||||
return ConnectChoiceDialog.Choice.NEW_WINDOW;
|
||||
});
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
config2.setSavedProfileName("Host 2");
|
||||
app.connect(config2, true);
|
||||
|
||||
// Handler was invoked
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called when already connected");
|
||||
|
||||
// A new window was opened
|
||||
assertEquals(initialWindows + 1, J3270App.getOpenWindows().size(),
|
||||
"A new window should have been added to openWindows");
|
||||
|
||||
// Original app remains on config1
|
||||
assertEquals("host1.test", app.getClient().getConfig().getHost());
|
||||
|
||||
// Find the new window
|
||||
J3270App newWindow = null;
|
||||
for (J3270App w : J3270App.getOpenWindows()) {
|
||||
if (w != app) {
|
||||
newWindow = w;
|
||||
createdApps.add(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(newWindow, "New window must exist");
|
||||
assertNotNull(newWindow.getClient(), "New window should have a client");
|
||||
assertEquals("host2.test", newWindow.getClient().getConfig().getHost(),
|
||||
"New window must be connected to host2.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with ABORT_CURRENT choice aborts existing connection and connects in same window")
|
||||
public void testConnectChoiceAbortCurrent() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
|
||||
});
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called");
|
||||
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
|
||||
"No new window should be created when choosing ABORT_CURRENT");
|
||||
assertEquals("host2.test", app.getClient().getConfig().getHost(),
|
||||
"Same window should now be connected to host2.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with CANCEL choice keeps existing connection and creates no new window")
|
||||
public void testConnectChoiceCancel() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
return ConnectChoiceDialog.Choice.CANCEL;
|
||||
});
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called");
|
||||
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
|
||||
"No new window should be created when choosing CANCEL");
|
||||
assertEquals("host1.test", app.getClient().getConfig().getHost(),
|
||||
"Existing window must remain connected to host1.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("File menu contains Close Window item with VK_W accelerator")
|
||||
public void testCloseWindowMenuItem() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
JMenu fileMenu = null;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
if ("File".equals(mb.getMenu(i).getText())) {
|
||||
fileMenu = mb.getMenu(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(fileMenu, "File menu must exist");
|
||||
|
||||
JMenuItem closeWindowItem = null;
|
||||
for (int i = 0; i < fileMenu.getItemCount(); i++) {
|
||||
JMenuItem item = fileMenu.getItem(i);
|
||||
if (item != null && "Close Window".equals(item.getText())) {
|
||||
closeWindowItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(closeWindowItem, "File menu must contain Close Window item");
|
||||
KeyStroke ks = closeWindowItem.getAccelerator();
|
||||
assertNotNull(ks, "Close Window item must have an accelerator");
|
||||
assertEquals(KeyEvent.VK_W, ks.getKeyCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Closing one window when multiple windows are open leaves remaining window open")
|
||||
public void testMultiWindowCloseIndependent() {
|
||||
J3270App app1 = createApp();
|
||||
if (app1 == null) return;
|
||||
|
||||
J3270App app2 = createApp();
|
||||
if (app2 == null) return;
|
||||
|
||||
assertTrue(J3270App.getOpenWindows().contains(app1));
|
||||
assertTrue(J3270App.getOpenWindows().contains(app2));
|
||||
assertEquals(2, J3270App.getOpenWindows().size());
|
||||
|
||||
// Close app1
|
||||
app1.closeWindow();
|
||||
|
||||
assertFalse(J3270App.getOpenWindows().contains(app1), "app1 should be removed from open windows");
|
||||
assertTrue(J3270App.getOpenWindows().contains(app2), "app2 should remain open");
|
||||
assertEquals(1, J3270App.getOpenWindows().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Abort current connection cleanly tears down old client and connects new client in same window")
|
||||
public void testAbortCurrentConnectionGracefullyDisconnects() throws Exception {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
Telnet3270Client oldClient = app.getClient();
|
||||
assertNotNull(oldClient);
|
||||
|
||||
app.setConnectChoiceHandler((owner, target) -> ConnectChoiceDialog.Choice.ABORT_CURRENT);
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
// Client on app is immediately replaced with new client
|
||||
Telnet3270Client newClient = app.getClient();
|
||||
assertNotNull(newClient);
|
||||
assertNotSame(oldClient, newClient);
|
||||
assertEquals("host2.test", newClient.getConfig().getHost());
|
||||
|
||||
// Old client should be disconnected
|
||||
Thread.sleep(100);
|
||||
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, oldClient.getConnectionState());
|
||||
assertFalse(oldClient.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Explicit app.disconnect() tears down client and clears UI state")
|
||||
public void testExplicitDisconnectResetsUI() throws Exception {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config, false);
|
||||
|
||||
Telnet3270Client client = app.getClient();
|
||||
assertNotNull(client);
|
||||
|
||||
app.disconnect();
|
||||
|
||||
assertNull(app.getClient());
|
||||
assertFalse(app.isConnected());
|
||||
Thread.sleep(100);
|
||||
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
assertFalse(client.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When not connected, connect does not trigger abort connection dialog for previously closed connection")
|
||||
public void testNotConnectedDoesNotTriggerAbortDialog() throws Exception {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
// Simulate a previously closed connection
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
Telnet3270Client client = app.getClient();
|
||||
assertNotNull(client);
|
||||
// Force the client's FSM state to NOT_CONNECTED, simulating connection closure
|
||||
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED);
|
||||
|
||||
assertFalse(app.isConnected(), "app.isConnected() must return false when client is NOT_CONNECTED");
|
||||
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
return ConnectChoiceDialog.Choice.CANCEL;
|
||||
});
|
||||
|
||||
// Initiate a new connection
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
assertFalse(handlerCalled[0], "Connect choice handler must NOT be called when j3270 is not connected");
|
||||
assertEquals("host2.test", app.getClient().getConfig().getHost(), "Must proceed to connect to host2.test in same window");
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,8 @@ public class LoggingConfigurationTest {
|
||||
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
|
||||
|
||||
File logFile = new File("j3270.log");
|
||||
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled");
|
||||
File logFile1 = new File("j3270.log.1");
|
||||
assertTrue(logFile.exists() || logFile1.exists(), "j3270.log should be created when debug is enabled");
|
||||
|
||||
// Clean up
|
||||
J3270App.configureLogging(false);
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.nvt.NvtProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 27:
|
||||
* Build Hygiene, Release Versioning & Zero-Risk Pruning (v1.2.4)
|
||||
*/
|
||||
public class Phase27BuildAndRenderTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.1 & 27.2: Release version 1.2.4 synchronized across build files and android.useAndroidX removed")
|
||||
public void testVersionSynchronizationAndGradleProperties() throws Exception {
|
||||
File projectRoot = new File(".").getCanonicalFile();
|
||||
if (!new File(projectRoot, "settings.gradle").exists() && projectRoot.getParentFile() != null) {
|
||||
projectRoot = projectRoot.getParentFile();
|
||||
}
|
||||
|
||||
// 1. Verify build.gradle version = '1.2.4'
|
||||
File buildGradle = new File(projectRoot, "build.gradle");
|
||||
assertTrue(buildGradle.exists(), "build.gradle must exist");
|
||||
String buildGradleContent = Files.readString(buildGradle.toPath());
|
||||
assertTrue(buildGradleContent.contains("version = '1.2.4'"), "build.gradle must define version = '1.2.4'");
|
||||
assertFalse(buildGradleContent.contains("0.1.0"), "build.gradle must not contain 0.1.0");
|
||||
|
||||
|
||||
// 3. Verify setup.ps1 Implementation-Version: 1.2.4
|
||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
|
||||
String setupPs1Content = Files.readString(setupPs1.toPath());
|
||||
assertTrue(setupPs1Content.contains("Implementation-Version: 1.2.4"), "setup.ps1 must specify Implementation-Version: 1.2.4");
|
||||
assertFalse(setupPs1Content.contains("Implementation-Version: 0.1.0"), "setup.ps1 must not contain 0.1.0");
|
||||
|
||||
// 4. Verify setup.sh Implementation-Version: 1.2.4
|
||||
File setupSh = new File(projectRoot, "setup.sh");
|
||||
assertTrue(setupSh.exists(), "setup.sh must exist");
|
||||
String setupShContent = Files.readString(setupSh.toPath());
|
||||
assertTrue(setupShContent.contains("Implementation-Version: 1.2.4"), "setup.sh must specify Implementation-Version: 1.2.4");
|
||||
assertFalse(setupShContent.contains("Implementation-Version: 0.1.0"), "setup.sh must not contain 0.1.0");
|
||||
|
||||
// 5. Verify gradle.properties android.useAndroidX removed
|
||||
File gradleProps = new File(projectRoot, "gradle.properties");
|
||||
assertTrue(gradleProps.exists(), "gradle.properties must exist");
|
||||
String gradlePropsContent = Files.readString(gradleProps.toPath());
|
||||
assertFalse(gradlePropsContent.contains("android.useAndroidX"), "gradle.properties must not contain android.useAndroidX");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.3: T1-B Static Color & BasicStroke constants promoted in TerminalPanel")
|
||||
public void testHotspotHoverStaticConstants() throws Exception {
|
||||
Field colorField = TerminalPanel.class.getDeclaredField("HOTSPOT_HOVER_COLOR");
|
||||
colorField.setAccessible(true);
|
||||
assertTrue(Modifier.isStatic(colorField.getModifiers()), "HOTSPOT_HOVER_COLOR must be static");
|
||||
assertTrue(Modifier.isFinal(colorField.getModifiers()), "HOTSPOT_HOVER_COLOR must be final");
|
||||
Color hoverColor = (Color) colorField.get(null);
|
||||
assertEquals(new Color(0, 220, 255), hoverColor, "HOTSPOT_HOVER_COLOR must match bright cyan (0, 220, 255)");
|
||||
|
||||
Field strokeField = TerminalPanel.class.getDeclaredField("HOTSPOT_HOVER_STROKE");
|
||||
strokeField.setAccessible(true);
|
||||
assertTrue(Modifier.isStatic(strokeField.getModifiers()), "HOTSPOT_HOVER_STROKE must be static");
|
||||
assertTrue(Modifier.isFinal(strokeField.getModifiers()), "HOTSPOT_HOVER_STROKE must be final");
|
||||
Stroke hoverStroke = (Stroke) strokeField.get(null);
|
||||
assertNotNull(hoverStroke, "HOTSPOT_HOVER_STROKE must not be null");
|
||||
assertTrue(hoverStroke instanceof BasicStroke, "HOTSPOT_HOVER_STROKE must be BasicStroke");
|
||||
assertEquals(2.0f, ((BasicStroke) hoverStroke).getLineWidth(), 0.001f, "HOTSPOT_HOVER_STROKE line width must be 2.0f");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.4: T1-A charScratch Buffer exists and non-ASCII render renders without exceptions")
|
||||
public void testCharScratchAndNonAsciiRendering() throws Exception {
|
||||
Field scratchField = TerminalPanel.class.getDeclaredField("charScratch");
|
||||
scratchField.setAccessible(true);
|
||||
assertFalse(Modifier.isStatic(scratchField.getModifiers()), "charScratch should be an instance field confined to EDT");
|
||||
assertTrue(Modifier.isFinal(scratchField.getModifiers()), "charScratch should be final");
|
||||
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
char[] scratch = (char[]) scratchField.get(panel);
|
||||
assertNotNull(scratch, "charScratch must not be null");
|
||||
assertEquals(1, scratch.length, "charScratch must be length 1");
|
||||
|
||||
// Set up client with non-ASCII characters (e.g. U+2500 box drawing, U+00E9 acute e, U+2550 double box)
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put non-ASCII characters on screen
|
||||
sb.getCell(0).ucs4 = 0x2500; // Box drawing horizontal line
|
||||
sb.getCell(1).ucs4 = 0x00E9; // e with acute
|
||||
sb.getCell(2).ucs4 = 0x2550; // Box drawing double horizontal
|
||||
sb.getCell(3).ucs4 = 0x2308; // APL left ceiling
|
||||
sb.updateDisplaySnapshot();
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
BufferedImage img = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = img.createGraphics();
|
||||
assertDoesNotThrow(() -> panel.paint(g2), "paint must render non-ASCII cells via drawChars without error");
|
||||
g2.dispose();
|
||||
|
||||
// Check that charScratch was used
|
||||
assertTrue(scratch[0] > 0, "charScratch should have held the last drawn non-ASCII character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.5: T1-C Reusable NVT Single-Byte Buffer dispatches without allocations")
|
||||
public void testReusableNvtSingleByteBuffer() throws Exception {
|
||||
Field scratchField = TelnetFSM.class.getDeclaredField("nvtSingleByteScratch");
|
||||
scratchField.setAccessible(true);
|
||||
assertFalse(Modifier.isStatic(scratchField.getModifiers()), "nvtSingleByteScratch must be an instance field");
|
||||
assertTrue(Modifier.isFinal(scratchField.getModifiers()), "nvtSingleByteScratch must be final");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
TelnetFSM fsm = client.getTelnetFSM();
|
||||
byte[] expectedScratch = (byte[]) scratchField.get(fsm);
|
||||
assertNotNull(expectedScratch, "nvtSingleByteScratch must not be null");
|
||||
assertEquals(1, expectedScratch.length, "nvtSingleByteScratch must have length 1");
|
||||
|
||||
fsm.setConnectionState(ConnectionState.CONNECTED_NVT);
|
||||
|
||||
AtomicBoolean dispatched = new AtomicBoolean(false);
|
||||
AtomicReference<byte[]> receivedBuffer = new AtomicReference<>();
|
||||
|
||||
NvtProcessor customNvtProcessor = new NvtProcessor(client.getScreenBuffer(), new EbcdicTranslator()) {
|
||||
@Override
|
||||
public void processNVTData(byte[] buffer, int offset, int length) {
|
||||
dispatched.set(true);
|
||||
receivedBuffer.set(buffer);
|
||||
assertEquals(0, offset);
|
||||
assertEquals(1, length);
|
||||
assertEquals((byte) 'Z', buffer[offset]);
|
||||
super.processNVTData(buffer, offset, length);
|
||||
}
|
||||
};
|
||||
|
||||
Field nvtProcField = TelnetFSM.class.getDeclaredField("nvtProcessor");
|
||||
nvtProcField.setAccessible(true);
|
||||
nvtProcField.set(fsm, customNvtProcessor);
|
||||
|
||||
// Feed single byte 'Z' into FSM
|
||||
fsm.feedByte('Z');
|
||||
|
||||
assertTrue(dispatched.get(), "NvtProcessor.processNVTData should have been called");
|
||||
assertSame(expectedScratch, receivedBuffer.get(), "Buffer passed to processNVTData must be the reusable nvtSingleByteScratch array instance");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.6: J3270App has Settings import and no in-line FQCN occurrences")
|
||||
public void testJ3270AppSettingsImport() throws Exception {
|
||||
File projectRoot = new File(".").getCanonicalFile();
|
||||
if (!new File(projectRoot, "j3270").exists() && projectRoot.getParentFile() != null) {
|
||||
projectRoot = projectRoot.getParentFile();
|
||||
}
|
||||
File appFile = new File(projectRoot, "j3270/src/main/java/haus/nightmare/j3270/J3270App.java");
|
||||
assertTrue(appFile.exists(), "J3270App.java must exist");
|
||||
String content = Files.readString(appFile.toPath());
|
||||
|
||||
assertTrue(content.contains("import haus.nightmare.j3270.config.Settings;"),
|
||||
"J3270App.java must import haus.nightmare.j3270.config.Settings");
|
||||
|
||||
// The only occurrence of haus.nightmare.j3270.config.Settings should be in the import
|
||||
int firstIndex = content.indexOf("haus.nightmare.j3270.config.Settings");
|
||||
assertTrue(firstIndex >= 0, "Must contain import");
|
||||
int secondIndex = content.indexOf("haus.nightmare.j3270.config.Settings", firstIndex + 1);
|
||||
assertEquals(-1, secondIndex, "There should be no inline occurrences of haus.nightmare.j3270.config.Settings in J3270App");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.FieldInspectorDialog;
|
||||
import haus.nightmare.j3270.ui.FindDialog;
|
||||
import haus.nightmare.j3270.ui.HotspotEngine;
|
||||
import haus.nightmare.j3270.ui.ScriptDialog;
|
||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 28:
|
||||
* UI Responsiveness, Dialog Lifecycle & Event Optimization
|
||||
*/
|
||||
public class Phase28UiOptimizationTest {
|
||||
|
||||
private void setField(Object obj, String fieldName, Object val) throws Exception {
|
||||
Field f = obj.getClass().getDeclaredField(fieldName);
|
||||
f.setAccessible(true);
|
||||
f.set(obj, val);
|
||||
}
|
||||
|
||||
private void writeString(ScreenBuffer sb, int row, int col, String text) {
|
||||
int cols = sb.getDisplayCols();
|
||||
int base = row * cols + col;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
sb.getCell(base + i).ucs4 = text.charAt(i);
|
||||
}
|
||||
sb.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 28.1: T2-A Hotspot Dirty Flagging & Lazy Text Scraping eliminates redundant scans on mouseMoved")
|
||||
public void testRapidMouseMoveEventsScanOnlyOnce() throws Exception {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put hotspots on screen: PF keys and URL
|
||||
writeString(sb, 0, 0, "APPLICATION SELECTION MENU");
|
||||
writeString(sb, 2, 0, "PF1=Help PF3=Exit PF12=Cancel");
|
||||
writeString(sb, 4, 0, "Visit http://example.com/3270 for assistance");
|
||||
sb.updateDisplaySnapshot();
|
||||
|
||||
panel.setClient(client);
|
||||
HotspotEngine engine = panel.getHotspotEngine();
|
||||
engine.setUrlEnabled(true);
|
||||
engine.setPfKeyEnabled(true);
|
||||
engine.setMenuChoiceEnabled(true);
|
||||
|
||||
assertTrue(panel.isHotspotsDirty(), "Hotspots should initially be dirty on panel setup");
|
||||
long initialScanCount = engine.getScanCount();
|
||||
|
||||
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
|
||||
assertTrue(motionListeners.length > 0, "TerminalPanel must have at least one MouseMotionListener");
|
||||
MouseMotionListener mml = motionListeners[0];
|
||||
|
||||
// Simulate 1,000 rapid mouse move events across the terminal grid
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int x = 20 + (i % 200);
|
||||
int y = 30 + (i % 150);
|
||||
MouseEvent me = new MouseEvent(panel, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, x, y, 0, false);
|
||||
mml.mouseMoved(me);
|
||||
}
|
||||
|
||||
// After 1,000 moves, hotspotEngine.scan() should only have executed ONCE
|
||||
assertEquals(initialScanCount + 1, engine.getScanCount(),
|
||||
"hotspotEngine.scan() should execute exactly once across 1,000 rapid mouse move events on a clean screen");
|
||||
assertFalse(panel.isHotspotsDirty(), "Hotspots must be marked clean after scanning");
|
||||
|
||||
// Verify hotspots were actually discovered
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.getHotspots();
|
||||
assertFalse(hotspots.isEmpty(), "Hotspots on screen must be found");
|
||||
assertNotNull(engine.findHotspot(2, 0), "Hotspot at row 2 col 0 (PF1) must be found");
|
||||
|
||||
// Direct calls to HotspotEngine.scan(sb) without snapshot version changes must also bypass StringBuilder
|
||||
long beforeDirectCalls = engine.getScanCount();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
List<HotspotEngine.Hotspot> res = engine.scan(sb);
|
||||
assertFalse(res.isEmpty());
|
||||
}
|
||||
assertEquals(beforeDirectCalls, engine.getScanCount(),
|
||||
"Direct calls to HotspotEngine.scan(sb) without snapshot version change must return cachedHotspots");
|
||||
|
||||
// Now simulate screen update
|
||||
writeString(sb, 6, 0, "NEW UPDATE: PF7=Up PF8=Down");
|
||||
panel.onScreenUpdated();
|
||||
assertTrue(panel.isHotspotsDirty(), "onScreenUpdated() must flag hotspotsDirty = true");
|
||||
|
||||
// Simulate another burst of 1,000 rapid mouse moves
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int x = 30 + (i % 200);
|
||||
int y = 40 + (i % 150);
|
||||
MouseEvent me = new MouseEvent(panel, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, x, y, 0, false);
|
||||
mml.mouseMoved(me);
|
||||
}
|
||||
|
||||
assertEquals(initialScanCount + 2, engine.getScanCount(),
|
||||
"After screen update, exactly one more scan should run across 1,000 mouse moves");
|
||||
assertFalse(panel.isHotspotsDirty(), "Hotspots must be marked clean again after rescan");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 28.2: T2-B Precomputed Selection Scratch Lookup matches isCellSelected exactly across all modes")
|
||||
public void testSelectionBoundsMatchingPrecomputedScratchVsLegacy() throws Exception {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
panel.setClient(client);
|
||||
|
||||
int rows = 24;
|
||||
int cols = 80;
|
||||
|
||||
// Configuration 1: No selection
|
||||
panel.clearSelection();
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
boolean[] scratch = panel.getSelectedCellsScratch();
|
||||
assertEquals(rows * cols, scratch.length);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
assertFalse(scratch[baddr], "No cell should be selected");
|
||||
assertEquals(panel.isCellSelected(r, c), scratch[baddr]);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 2: Linear / Stream mode forward selection (spanning multiple rows)
|
||||
setField(panel, "blockSelectMode", false);
|
||||
setField(panel, "trimRectSizingHandles", false);
|
||||
setField(panel, "selectionStartRow", 2);
|
||||
setField(panel, "selectionStartCol", 15);
|
||||
setField(panel, "selectionEndRow", 5);
|
||||
setField(panel, "selectionEndCol", 42);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in linear forward mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 3: Linear / Stream mode backward selection (drag upwards)
|
||||
setField(panel, "selectionStartRow", 8);
|
||||
setField(panel, "selectionStartCol", 30);
|
||||
setField(panel, "selectionEndRow", 4);
|
||||
setField(panel, "selectionEndCol", 10);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in linear backward mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 4: Block select mode
|
||||
setField(panel, "blockSelectMode", true);
|
||||
setField(panel, "trimRectSizingHandles", false);
|
||||
setField(panel, "selectionStartRow", 3);
|
||||
setField(panel, "selectionStartCol", 20);
|
||||
setField(panel, "selectionEndRow", 7);
|
||||
setField(panel, "selectionEndCol", 60);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in block select mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 5: Trim Rect Sizing Handles mode (block geometry)
|
||||
setField(panel, "blockSelectMode", false);
|
||||
setField(panel, "trimRectSizingHandles", true);
|
||||
setField(panel, "selectionStartRow", 10);
|
||||
setField(panel, "selectionStartCol", 5);
|
||||
setField(panel, "selectionEndRow", 15);
|
||||
setField(panel, "selectionEndCol", 25);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in trim handles mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 6: Model 5 Dynamic Geometry (27 rows x 132 cols)
|
||||
rows = 27;
|
||||
cols = 132;
|
||||
client.getScreenBuffer().setDimensions(rows, cols);
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
setField(panel, "selectionStartRow", 1);
|
||||
setField(panel, "selectionStartCol", 1);
|
||||
setField(panel, "selectionEndRow", 25);
|
||||
setField(panel, "selectionEndCol", 130);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
assertEquals(rows * cols, scratch.length);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in Model 5 27x132 at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 28.3: Dialog Lifecycle & Reconnection Hardening updates cached dialogs with active client")
|
||||
public void testReconnectCycleFindDialogInteractsWithNewClient() throws Exception {
|
||||
// 1. Verify API contract and reflection for dialog lifecycle methods
|
||||
Method findSetClient = FindDialog.class.getMethod("setClient", Telnet3270Client.class);
|
||||
assertNotNull(findSetClient);
|
||||
Method findGetClient = FindDialog.class.getMethod("getClient");
|
||||
assertEquals(Telnet3270Client.class, findGetClient.getReturnType());
|
||||
|
||||
Method scriptSetClient = ScriptDialog.class.getMethod("setClient", Telnet3270Client.class);
|
||||
assertNotNull(scriptSetClient);
|
||||
Method scriptGetClient = ScriptDialog.class.getMethod("getClient");
|
||||
assertEquals(Telnet3270Client.class, scriptGetClient.getReturnType());
|
||||
|
||||
Method fieldSetClient = FieldInspectorDialog.class.getMethod("setClient", Telnet3270Client.class);
|
||||
assertNotNull(fieldSetClient);
|
||||
Method fieldGetClient = FieldInspectorDialog.class.getMethod("getClient");
|
||||
assertEquals(Telnet3270Client.class, fieldGetClient.getReturnType());
|
||||
|
||||
Method appGetFind = J3270App.class.getMethod("getFindDialog");
|
||||
assertEquals(FindDialog.class, appGetFind.getReturnType());
|
||||
Method appGetScript = J3270App.class.getMethod("getScriptDialog");
|
||||
assertEquals(ScriptDialog.class, appGetScript.getReturnType());
|
||||
Method appGetField = J3270App.class.getMethod("getFieldInspectorDialog");
|
||||
assertEquals(FieldInspectorDialog.class, appGetField.getReturnType());
|
||||
|
||||
// 2. If non-headless environment, perform end-to-end interactive GUI verification
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
TerminalPanel panel = app.getTerminalPanel();
|
||||
|
||||
// Session 1: Client 1
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.example.com", 23);
|
||||
Telnet3270Client client1 = new Telnet3270Client(config1);
|
||||
client1.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
writeString(client1.getScreenBuffer(), 1, 0, "SYSTEM_SESSION_ALPHA_123");
|
||||
|
||||
panel.setClient(client1);
|
||||
setField(app, "client", client1);
|
||||
|
||||
// Open FindDialog on session 1
|
||||
FindDialog findDialog = new FindDialog(app, client1, panel);
|
||||
assertSame(client1, findDialog.getClient());
|
||||
|
||||
// Search for string on Client 1
|
||||
findDialog.setSearchText("SYSTEM_SESSION_ALPHA_123");
|
||||
findDialog.findNext();
|
||||
int expectedPos1 = 1 * client1.getScreenBuffer().getDisplayCols();
|
||||
assertEquals(expectedPos1, client1.getScreenBuffer().getCursorAddress(),
|
||||
"FindDialog must move cursor to match on client1");
|
||||
|
||||
// Simulate Session Disconnect
|
||||
setField(app, "findDialog", findDialog);
|
||||
app.disconnect(false);
|
||||
assertNull(findDialog.getClient(), "Disconnect must nullify client in cached dialog");
|
||||
|
||||
// Subsequent find attempt while disconnected should report disconnected without errors
|
||||
findDialog.findNext();
|
||||
assertEquals(expectedPos1, client1.getScreenBuffer().getCursorAddress());
|
||||
|
||||
// Session 2: Reconnection to Client 2
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.example.com", 23);
|
||||
Telnet3270Client client2 = new Telnet3270Client(config2);
|
||||
client2.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
writeString(client2.getScreenBuffer(), 3, 10, "MAINFRAME_SESSION_BETA_999");
|
||||
|
||||
// Simulate reconnect hook in J3270App
|
||||
findDialog.setClient(client2);
|
||||
assertSame(client2, findDialog.getClient(), "FindDialog must now hold client2");
|
||||
|
||||
// Search for string on Client 2
|
||||
findDialog.setSearchText("MAINFRAME_SESSION_BETA_999");
|
||||
findDialog.findNext();
|
||||
int expectedPos2 = 3 * client2.getScreenBuffer().getDisplayCols() + 10;
|
||||
assertEquals(expectedPos2, client2.getScreenBuffer().getCursorAddress(),
|
||||
"FindDialog must move cursor to match on active client2 after reconnect");
|
||||
|
||||
// Verify search for old client1 string fails on client2
|
||||
int oldCursorPos = client2.getScreenBuffer().getCursorAddress();
|
||||
findDialog.setSearchText("SYSTEM_SESSION_ALPHA_123");
|
||||
findDialog.findNext();
|
||||
assertEquals(oldCursorPos, client2.getScreenBuffer().getCursorAddress(),
|
||||
"Search for stale text should not match on client2");
|
||||
|
||||
// Verify ScriptDialog and FieldInspectorDialog client lifecycle
|
||||
ScriptDialog scriptDialog = new ScriptDialog(app, client1, panel);
|
||||
assertSame(client1, scriptDialog.getClient());
|
||||
scriptDialog.setClient(client2);
|
||||
assertSame(client2, scriptDialog.getClient());
|
||||
|
||||
FieldInspectorDialog fieldInspector = new FieldInspectorDialog(app, client1, panel);
|
||||
assertSame(client1, fieldInspector.getClient());
|
||||
fieldInspector.setClient(client2);
|
||||
assertSame(client2, fieldInspector.getClient());
|
||||
|
||||
// Cleanup
|
||||
fieldInspector.dispose();
|
||||
findDialog.dispose();
|
||||
scriptDialog.dispose();
|
||||
app.closeWindow();
|
||||
} catch (java.awt.HeadlessException ignored) {
|
||||
// Running in headless mode: GUI instantiation safely bypassed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.CodePage;
|
||||
import haus.nightmare.lib3270j.charset.CodePageRegistry;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.HeadlessException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 31:
|
||||
* Help > About Dialogue Version Reporting and j3270 Interface Inconsistency Fixes (v1.2.4).
|
||||
*/
|
||||
public class Phase31AboutAndInterfaceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 31.1: Dedicated Version classes report v1.2.4 single source of truth")
|
||||
public void testVersionClasses() {
|
||||
// lib3270j
|
||||
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.VERSION);
|
||||
assertEquals("lib3270j", haus.nightmare.lib3270j.Version.NAME);
|
||||
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.getVersion());
|
||||
|
||||
// j3270
|
||||
assertEquals("1.2.4", haus.nightmare.j3270.Version.VERSION);
|
||||
assertEquals("j3270", haus.nightmare.j3270.Version.NAME);
|
||||
assertEquals("1.2.4", haus.nightmare.j3270.Version.getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 31.2: Help > About dialogue text properly reports j3270 v1.2.4 and lib3270j v1.2.4")
|
||||
public void testAboutTextContent() {
|
||||
String aboutText = J3270App.getAboutText();
|
||||
assertNotNull(aboutText, "About text must not be null");
|
||||
|
||||
// Verify version reports
|
||||
assertTrue(aboutText.contains("j3270 v1.2.4"), "About text must report j3270 v1.2.4");
|
||||
assertTrue(aboutText.contains("lib3270j v1.2.4"), "About text must report lib3270j v1.2.4");
|
||||
|
||||
// Verify stale pre-release versions are eradicated
|
||||
assertFalse(aboutText.contains("0.2.0"), "About text must not contain stale version 0.2.0");
|
||||
assertFalse(aboutText.contains("0.1.0"), "About text must not contain stale version 0.1.0");
|
||||
|
||||
// Verify 40+ codepages reporting
|
||||
assertTrue(aboutText.contains("40+ Pluggable EBCDIC Codepages & DBCS Support"),
|
||||
"About text must report 40+ Pluggable EBCDIC Codepages");
|
||||
assertFalse(aboutText.contains("22+"), "About text must not report stale 22+ codepages");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 31.3: CodePageRegistry.COMMON_CODE_PAGES defines 40+ supported codepages that all resolve")
|
||||
public void testCommonCodePagesRegistration() {
|
||||
String[] common = CodePageRegistry.COMMON_CODE_PAGES;
|
||||
assertNotNull(common, "COMMON_CODE_PAGES must not be null");
|
||||
assertTrue(common.length >= 40, "COMMON_CODE_PAGES must define at least 40 entries, found: " + common.length);
|
||||
|
||||
for (String entry : common) {
|
||||
String cpId = entry.split(" -")[0].trim();
|
||||
CodePage cp = CodePageRegistry.resolveCodePage(cpId);
|
||||
assertNotNull(cp, "Codepage identifier '" + cpId + "' from entry '" + entry + "' must resolve in CodePageRegistry");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 31.4: Menu bar consistency: Code Page & Graphics Mode in Connection menu, Screen History in View")
|
||||
public void testMenuBarConsistency() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
// Headless environment without X11/display
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb, "JMenuBar must exist");
|
||||
|
||||
JMenu viewMenu = null;
|
||||
JMenu actionsMenu = null;
|
||||
JMenu fileMenu = null;
|
||||
JMenu connectionMenu = null;
|
||||
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null) {
|
||||
if ("View".equals(m.getText())) viewMenu = m;
|
||||
else if ("Actions".equals(m.getText())) actionsMenu = m;
|
||||
else if ("File".equals(m.getText())) fileMenu = m;
|
||||
else if ("Connection".equals(m.getText())) connectionMenu = m;
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(viewMenu, "View menu must exist");
|
||||
assertNotNull(actionsMenu, "Actions menu must exist");
|
||||
assertNotNull(fileMenu, "File menu must exist");
|
||||
assertNotNull(connectionMenu, "Connection menu must exist");
|
||||
|
||||
// Verify Connect... and Disconnect are in Connection menu and NOT in File menu
|
||||
boolean connectInConnection = false;
|
||||
boolean disconnectInConnection = false;
|
||||
for (int i = 0; i < connectionMenu.getItemCount(); i++) {
|
||||
JMenuItem item = connectionMenu.getItem(i);
|
||||
if (item != null) {
|
||||
if ("Connect...".equals(item.getText())) connectInConnection = true;
|
||||
if ("Disconnect".equals(item.getText())) disconnectInConnection = true;
|
||||
}
|
||||
}
|
||||
assertTrue(connectInConnection, "Connect... must be in Connection menu");
|
||||
assertTrue(disconnectInConnection, "Disconnect must be in Connection menu");
|
||||
|
||||
boolean connectInFile = false;
|
||||
boolean disconnectInFile = false;
|
||||
for (int i = 0; i < fileMenu.getItemCount(); i++) {
|
||||
JMenuItem item = fileMenu.getItem(i);
|
||||
if (item != null) {
|
||||
if ("Connect...".equals(item.getText())) connectInFile = true;
|
||||
if ("Disconnect".equals(item.getText())) disconnectInFile = true;
|
||||
}
|
||||
}
|
||||
assertFalse(connectInFile, "Connect... must NOT be in File menu");
|
||||
assertFalse(disconnectInFile, "Disconnect must NOT be in File menu");
|
||||
|
||||
// Verify Screen History is in View menu
|
||||
boolean screenHistoryInView = false;
|
||||
for (int i = 0; i < viewMenu.getItemCount(); i++) {
|
||||
JMenuItem item = viewMenu.getItem(i);
|
||||
if (item != null && "Screen History...".equals(item.getText())) {
|
||||
screenHistoryInView = true;
|
||||
}
|
||||
}
|
||||
assertTrue(screenHistoryInView, "Screen History... must be present in View menu");
|
||||
|
||||
// Verify Screen History is NOT duplicated in Actions menu
|
||||
boolean screenHistoryInActions = false;
|
||||
for (int i = 0; i < actionsMenu.getItemCount(); i++) {
|
||||
JMenuItem item = actionsMenu.getItem(i);
|
||||
if (item != null && "Screen History...".equals(item.getText())) {
|
||||
screenHistoryInActions = true;
|
||||
}
|
||||
}
|
||||
assertFalse(screenHistoryInActions, "Screen History... must NOT be duplicated in Actions menu");
|
||||
|
||||
// Verify Code Page and Graphics Mode are in Connection menu, NOT View menu
|
||||
JMenu codePageSubmenu = null;
|
||||
JMenu graphicsModeSubmenu = null;
|
||||
for (int i = 0; i < connectionMenu.getItemCount(); i++) {
|
||||
JMenuItem item = connectionMenu.getItem(i);
|
||||
if (item != null) {
|
||||
if ("Code Page".equals(item.getText()) && item instanceof JMenu) {
|
||||
codePageSubmenu = (JMenu) item;
|
||||
}
|
||||
if ("Graphics Mode".equals(item.getText()) && item instanceof JMenu) {
|
||||
graphicsModeSubmenu = (JMenu) item;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertNotNull(codePageSubmenu, "Connection > Code Page submenu must exist");
|
||||
assertNotNull(graphicsModeSubmenu, "Connection > Graphics Mode submenu must exist");
|
||||
assertEquals(CodePageRegistry.COMMON_CODE_PAGES.length, codePageSubmenu.getItemCount(),
|
||||
"Connection > Code Page submenu must contain all " + CodePageRegistry.COMMON_CODE_PAGES.length + " common codepages");
|
||||
assertEquals(GraphicsMode.values().length, graphicsModeSubmenu.getItemCount(),
|
||||
"Connection > Graphics Mode submenu must contain all " + GraphicsMode.values().length + " graphics modes");
|
||||
|
||||
// Graphics Mode must be enabled when disconnected
|
||||
assertFalse(app.isConnected(), "App should not be connected by default");
|
||||
assertTrue(graphicsModeSubmenu.isEnabled(), "Graphics Mode submenu should be enabled when disconnected");
|
||||
|
||||
// Verify Code Page and Graphics Mode are NOT in View menu
|
||||
for (int i = 0; i < viewMenu.getItemCount(); i++) {
|
||||
JMenuItem item = viewMenu.getItem(i);
|
||||
if (item != null) {
|
||||
assertNotEquals("Code Page", item.getText(), "Code Page must NOT be in View menu");
|
||||
assertNotEquals("Graphics Mode", item.getText(), "Graphics Mode must NOT be in View menu");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Base 4-Color Override is in View menu
|
||||
boolean base4InView = false;
|
||||
for (int i = 0; i < viewMenu.getItemCount(); i++) {
|
||||
JMenuItem item = viewMenu.getItem(i);
|
||||
if (item != null && "Base 4-Color Override".equals(item.getText())) {
|
||||
base4InView = true;
|
||||
}
|
||||
}
|
||||
assertTrue(base4InView, "Base 4-Color Override must be present in View menu");
|
||||
|
||||
// Verify Modes menu contains strictly the 4 interactive operator modes
|
||||
JMenu modesMenu = null;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null && "Modes".equals(m.getText())) {
|
||||
modesMenu = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(modesMenu, "Modes menu must exist");
|
||||
assertEquals(4, modesMenu.getItemCount(), "Modes menu should contain exactly 4 interactive operator modes");
|
||||
assertEquals("Document Mode (DOC)", modesMenu.getItem(0).getText());
|
||||
assertEquals("Word Wrap Mode", modesMenu.getItem(1).getText());
|
||||
assertEquals("APL Keyboard Mode", modesMenu.getItem(2).getText());
|
||||
assertEquals("Insert Mode", modesMenu.getItem(3).getText());
|
||||
} finally {
|
||||
app.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.Frame;
|
||||
import java.awt.HeadlessException;
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionHostTypeConfigTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private String originalDefaultHostType;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
FileTransferDialog.resetSessionState();
|
||||
originalSavedHosts = HostStorage.getSavedHosts();
|
||||
originalDefaultHostType = Settings.getDefaultHostType();
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
FileTransferDialog.resetSessionState();
|
||||
HostStorage.clearAll();
|
||||
if (originalSavedHosts != null) {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
}
|
||||
Settings.setDefaultHostType(originalDefaultHostType);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost converts to ConnectionConfig preserving hostType and saved connection metadata")
|
||||
public void testSavedHostToConnectionConfigPreservesHostType() {
|
||||
SavedHost cmsHost = new SavedHost(
|
||||
"cms-uuid-1", "VM/CMS System", "vm370.corp.local", 23, 4, 62, 160, "",
|
||||
false, "CMS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgCms = cmsHost.toConnectionConfig();
|
||||
assertTrue(cfgCms.isSavedConnection(), "Config from SavedHost must be marked as saved connection");
|
||||
assertEquals("cms-uuid-1", cfgCms.getSavedProfileId());
|
||||
assertEquals("VM/CMS System", cfgCms.getSavedProfileName());
|
||||
assertEquals("CMS", cfgCms.getHostType());
|
||||
assertEquals(FTConfig.HostType.CMS, cfgCms.getFtHostType());
|
||||
|
||||
SavedHost cicsHost = new SavedHost(
|
||||
"cics-uuid-2", "CICS Gateway", "cics.corp.local", 23, 2, 24, 80, "LU1",
|
||||
false, "CICS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgCics = cicsHost.toConnectionConfig();
|
||||
assertTrue(cfgCics.isSavedConnection());
|
||||
assertEquals("CICS", cfgCics.getHostType());
|
||||
assertEquals(FTConfig.HostType.CICS, cfgCics.getFtHostType());
|
||||
|
||||
SavedHost tsoHost = new SavedHost(
|
||||
"tso-uuid-3", "MVS TSO", "tso.corp.local", 23, 4, 43, 80, "",
|
||||
false, "TSO", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgTso = tsoHost.toConnectionConfig();
|
||||
assertTrue(cfgTso.isSavedConnection());
|
||||
assertEquals("TSO", cfgTso.getHostType());
|
||||
assertEquals(FTConfig.HostType.TSO, cfgTso.getFtHostType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog preselects CMS when using a saved connection configured as CMS")
|
||||
public void testFileTransferDialogPrefillsFromSavedConnectionCms() {
|
||||
ConnectionConfig config = new ConnectionConfig("vm.example.com", 23);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId("vm-1");
|
||||
config.setSavedProfileName("VM370");
|
||||
config.setHostType("CMS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must preselect CMS when saved connection is configured as CMS");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog preselects CICS when using a saved connection configured as CICS")
|
||||
public void testFileTransferDialogPrefillsFromSavedConnectionCics() {
|
||||
ConnectionConfig config = new ConnectionConfig("cics.example.com", 23);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId("cics-1");
|
||||
config.setSavedProfileName("CICS Region");
|
||||
config.setHostType("CICS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CICS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must preselect CICS when saved connection is configured as CICS");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog overrides stale prior transfer state with configured host type from saved connection")
|
||||
public void testFileTransferDialogOverridesOldTransferStateWhenSavedConnectionUsed() {
|
||||
// Simulate a prior transfer performed on a TSO system
|
||||
FileTransferDialog.TransferSessionState oldState = new FileTransferDialog.TransferSessionState();
|
||||
oldState.hostType = FTConfig.HostType.TSO;
|
||||
oldState.localFile = "test.txt";
|
||||
oldState.hostFile = "USER.DATA";
|
||||
FileTransferDialog.setLastTransferState(oldState);
|
||||
|
||||
// Now connect to a CMS saved connection
|
||||
ConnectionConfig cmsConfig = new ConnectionConfig("vm.corp.internal", 23);
|
||||
cmsConfig.setSavedConnection(true);
|
||||
cmsConfig.setSavedProfileName("VM Prod");
|
||||
cmsConfig.setHostType("CMS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(cmsConfig);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must use the configured host type (CMS) of the saved connection rather than prior session's TSO");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectDialog allows host type configuration and saves it to profile")
|
||||
public void testConnectDialogHostTypeConfigurationAndProfile() {
|
||||
try {
|
||||
ConnectDialog dialog = new ConnectDialog((Frame) null);
|
||||
assertNotNull(dialog.getHostTypeCombo(), "ConnectDialog must provide a Host Type combo box");
|
||||
assertEquals("TSO", dialog.getSelectedHostType());
|
||||
|
||||
// Switch to CMS
|
||||
dialog.setInitialHostType("CMS");
|
||||
assertEquals("CMS", dialog.getSelectedHostType());
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getSelectedFtHostType());
|
||||
|
||||
// Switch to CICS
|
||||
dialog.setInitialHostType("CICS");
|
||||
assertEquals("CICS", dialog.getSelectedHostType());
|
||||
assertEquals(FTConfig.HostType.CICS, dialog.getSelectedFtHostType());
|
||||
|
||||
// Loading a SavedHost profile updates host type in ConnectDialog
|
||||
SavedHost cmsProfile = new SavedHost(
|
||||
"cms-prof-1", "CMS Dev", "cms.dev.local", 23, 4, 62, 160, "",
|
||||
false, "CMS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
dialog.loadProfile(cmsProfile);
|
||||
assertEquals("CMS", dialog.getSelectedHostType());
|
||||
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Settings persists and exports defaultHostType")
|
||||
public void testSettingsDefaultHostTypePersistenceAndIni() throws Exception {
|
||||
Settings.setDefaultHostType("CMS");
|
||||
assertEquals("CMS", Settings.getDefaultHostType());
|
||||
|
||||
File tmpIni = File.createTempFile("j3270_test_cfg", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(tmpIni)) {
|
||||
writer.println("[connection]");
|
||||
writer.println("hostType = CICS");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
|
||||
assertEquals("CICS", Settings.getDefaultHostType());
|
||||
|
||||
// Test export
|
||||
File exportFile = File.createTempFile("j3270_export", ".ini");
|
||||
exportFile.deleteOnExit();
|
||||
Settings.exportToIniFile(exportFile.getAbsolutePath());
|
||||
|
||||
String content = new String(java.nio.file.Files.readAllBytes(exportFile.toPath()));
|
||||
assertTrue(content.contains("hostType = CICS"), "Exported INI must contain hostType = CICS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("J3270App setConnectionHostType updates connection config and syncs with profile in HostStorage")
|
||||
public void testSetConnectionHostTypeUpdatesActiveConnectionAndStorage() {
|
||||
SavedHost saved = new SavedHost(
|
||||
"saved-prof-1", "Prod CMS", "vm.prod", 23, 4, 62, 160, "",
|
||||
false, "TSO", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
HostStorage.saveHost(saved);
|
||||
|
||||
ConnectionConfig config = saved.toConnectionConfig();
|
||||
assertEquals("TSO", config.getHostType());
|
||||
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
try {
|
||||
// Verify Connection menu exists in JMenuBar
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
boolean hasConnectionMenu = false;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null && "Connection".equals(m.getText())) {
|
||||
hasConnectionMenu = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(hasConnectionMenu, "JMenuBar must include a Connection menu");
|
||||
|
||||
// Simulate connecting with this config
|
||||
app.connect(config);
|
||||
|
||||
// Change host type via setConnectionHostType
|
||||
app.setConnectionHostType(FTConfig.HostType.CMS);
|
||||
assertEquals(FTConfig.HostType.CMS, app.getClient().getConfig().getFtHostType());
|
||||
|
||||
// Profile in storage should also be updated
|
||||
SavedHost reloaded = HostStorage.findById("saved-prof-1");
|
||||
assertNotNull(reloaded);
|
||||
assertEquals("CMS", reloaded.getHostType());
|
||||
} finally {
|
||||
app.dispose();
|
||||
}
|
||||
} catch (HeadlessException ignored) {
|
||||
// In headless environments where JFrame cannot be initialized, test model updates directly
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getConfig().setFtHostType(FTConfig.HostType.CMS);
|
||||
saved.setHostType(FTConfig.HostType.CMS.name());
|
||||
HostStorage.saveHost(saved);
|
||||
|
||||
SavedHost reloaded = HostStorage.findById("saved-prof-1");
|
||||
assertNotNull(reloaded);
|
||||
assertEquals("CMS", reloaded.getHostType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -99,4 +102,114 @@ public class FileTransferSessionAndCmsTest {
|
||||
// Headless environment; verified through state retention
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdsSessionStatePersistence() {
|
||||
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
|
||||
state.hostType = FTConfig.HostType.TSO;
|
||||
state.isSend = true;
|
||||
state.hostFile = "'USER.PDS.DATA'";
|
||||
state.units = FTConfig.AllocationUnit.CYLINDERS;
|
||||
state.space = "10,5";
|
||||
state.dirBlocks = "20";
|
||||
|
||||
FileTransferDialog.setLastTransferState(state);
|
||||
|
||||
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
|
||||
assertNotNull(retrieved);
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, retrieved.units);
|
||||
assertEquals("10,5", retrieved.space);
|
||||
assertEquals("20", retrieved.dirBlocks);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, null);
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, dialog.getUnitsCombo().getSelectedItem());
|
||||
assertEquals("10,5", dialog.getSpaceField().getText());
|
||||
assertEquals("20", dialog.getDirField().getText());
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostDirectoryDialogStructureAndFilter() {
|
||||
// Test default query resolution
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, null));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, ""));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, " "));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "*"));
|
||||
assertEquals("TEST EXEC", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "TEST EXEC"));
|
||||
|
||||
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, null));
|
||||
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, ""));
|
||||
assertEquals("'MY.DATASET'", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, "'MY.DATASET'"));
|
||||
|
||||
// Test CMS filter formatting
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(null));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(""));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("* * *"));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("*"));
|
||||
assertEquals("TEST * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST"));
|
||||
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC"));
|
||||
assertEquals("TEST EXEC B", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC B"));
|
||||
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST.EXEC"));
|
||||
|
||||
try {
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "");
|
||||
assertNotNull(dialog);
|
||||
assertFalse(dialog.isConfirmed());
|
||||
assertNull(dialog.getSelectedHostFile());
|
||||
assertEquals("* * A", dialog.getQueryField().getText());
|
||||
dialog.dispose();
|
||||
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog2 =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "* * *");
|
||||
assertEquals("* * A", dialog2.getQueryField().getText());
|
||||
dialog2.dispose();
|
||||
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog3 =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "TEST EXEC");
|
||||
assertEquals("TEST EXEC", dialog3.getQueryField().getText());
|
||||
dialog3.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesFilterWildcards() {
|
||||
CMSDirectoryEntry cmsA = new CMSDirectoryEntry("PROFILE", "EXEC", "A1");
|
||||
CMSDirectoryEntry cmsB = new CMSDirectoryEntry("PROFILE", "EXEC", "B1");
|
||||
CMSDirectoryEntry cmsSys = new CMSDirectoryEntry("SYSTEM", "CONFIG", "S2");
|
||||
|
||||
// * * A matches A disk files (A1, A0, etc.), excludes B and S
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* * A"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsB, "* * A"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* * A"));
|
||||
|
||||
// Blank, *, * * * matches all
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, ""));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsB, "* * *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsSys, "* * *"));
|
||||
|
||||
// Two tokens (fn ft) and dot tokens
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* EXEC"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*.EXEC"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROFILE *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROF*.*"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* EXEC"));
|
||||
|
||||
// TSO Wildcards
|
||||
TSODirectoryEntry tso = new TSODirectoryEntry("TSOUSER.TEST.DATA");
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, ""));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "* * *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*.DATA"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TEST"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TSOUSER.*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "'TSOUSER.TEST.*'"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(tso, "*.CNTL"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.ui.SettingsDialog;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionIniStorageTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private String origHost;
|
||||
private int origPort;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalSavedHosts = HostStorage.getSavedHosts();
|
||||
origHost = Settings.getAutoConnectHost();
|
||||
origPort = Settings.getAutoConnectPort();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
if (originalSavedHosts != null) {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
} else {
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
Settings.setAutoConnectHost(origHost);
|
||||
Settings.setAutoConnectPort(origPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage exports to and loads from connections.ini with full fidelity")
|
||||
public void testExportAndLoadConnectionsIniRoundtrip() throws IOException {
|
||||
List<SavedHost> hosts = new ArrayList<>();
|
||||
SavedHost h1 = new SavedHost("id-mainframe-1", "Production TSO", "mainframe.corp.com", 23, 4, 62, 160,
|
||||
"LU01", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
SavedHost h2 = new SavedHost("id-vm-2", "VM CMS Test", "vmcms.lab.net", 992, 2, 24, 80,
|
||||
"LUCMS", false, "CMS", true, false, true, "GDDM", "1047", "SOCKS5", "10.0.0.1", 1080, "proxyuser", "proxypass");
|
||||
hosts.add(h1);
|
||||
hosts.add(h2);
|
||||
|
||||
File tmpIni = File.createTempFile("connections_test", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
HostStorage.exportToIniFile(tmpIni, hosts);
|
||||
|
||||
assertTrue(tmpIni.exists());
|
||||
String content = new String(Files.readAllBytes(tmpIni.toPath()));
|
||||
assertTrue(content.contains("[Production TSO]"), "Must contain section header [Production TSO]");
|
||||
assertTrue(content.contains("[VM CMS Test]"), "Must contain section header [VM CMS Test]");
|
||||
assertTrue(content.contains("host = mainframe.corp.com"));
|
||||
assertTrue(content.contains("proxyHost = 10.0.0.1"));
|
||||
assertTrue(content.contains("proxyType = SOCKS5"));
|
||||
|
||||
List<SavedHost> loaded = HostStorage.loadFromIniFile(tmpIni);
|
||||
assertEquals(2, loaded.size(), "Should load 2 connections from INI");
|
||||
|
||||
SavedHost loaded1 = loaded.get(0);
|
||||
assertEquals("id-mainframe-1", loaded1.getId());
|
||||
assertEquals("Production TSO", loaded1.getName());
|
||||
assertEquals("mainframe.corp.com", loaded1.getHost());
|
||||
assertEquals(23, loaded1.getPort());
|
||||
assertEquals(4, loaded1.getModel());
|
||||
assertEquals(62, loaded1.getDynamicRows());
|
||||
assertEquals(160, loaded1.getDynamicCols());
|
||||
assertEquals("LU01", loaded1.getLuName());
|
||||
assertTrue(loaded1.isAutoConnect());
|
||||
assertEquals("TSO", loaded1.getHostType());
|
||||
assertFalse(loaded1.isUseTls());
|
||||
assertTrue(loaded1.isTlsVerifyCert());
|
||||
|
||||
SavedHost loaded2 = loaded.get(1);
|
||||
assertEquals("id-vm-2", loaded2.getId());
|
||||
assertEquals("VM CMS Test", loaded2.getName());
|
||||
assertEquals("vmcms.lab.net", loaded2.getHost());
|
||||
assertEquals(992, loaded2.getPort());
|
||||
assertEquals(2, loaded2.getModel());
|
||||
assertEquals(24, loaded2.getDynamicRows());
|
||||
assertEquals(80, loaded2.getDynamicCols());
|
||||
assertEquals("LUCMS", loaded2.getLuName());
|
||||
assertFalse(loaded2.isAutoConnect());
|
||||
assertEquals("CMS", loaded2.getHostType());
|
||||
assertTrue(loaded2.isUseTls());
|
||||
assertFalse(loaded2.isTlsVerifyCert());
|
||||
assertEquals("SOCKS5", loaded2.getProxyType());
|
||||
assertEquals("10.0.0.1", loaded2.getProxyHost());
|
||||
assertEquals(1080, loaded2.getProxyPort());
|
||||
assertEquals("proxyuser", loaded2.getProxyUsername());
|
||||
assertEquals("proxypass", loaded2.getProxyPassword());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage importFromIniFile merges and updates existing hosts by name or id")
|
||||
public void testImportFromIniFileMergesAndUpdates() throws IOException {
|
||||
SavedHost initial = new SavedHost("id-100", "Mainframe Prod", "old.host.org", 23, 4, 62, 160,
|
||||
"", false, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
List<SavedHost> initList = new ArrayList<>();
|
||||
initList.add(initial);
|
||||
HostStorage.saveAll(initList);
|
||||
|
||||
File tmpIni = File.createTempFile("connections_merge", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
// Update Mainframe Prod with new host address
|
||||
w.write("[Mainframe Prod]\n");
|
||||
w.write("id = id-100\n");
|
||||
w.write("host = new.host.org\n");
|
||||
w.write("port = 2323\n");
|
||||
w.write("\n");
|
||||
// Add a second connection
|
||||
w.write("[Development CICS]\n");
|
||||
w.write("host = cics.dev.org\n");
|
||||
w.write("port = 992\n");
|
||||
w.write("hostType = CICS\n");
|
||||
w.write("useTls = true\n");
|
||||
}
|
||||
|
||||
List<SavedHost> result = HostStorage.importFromIniFile(tmpIni);
|
||||
assertEquals(2, result.size(), "Import should result in 2 saved hosts (1 updated, 1 added)");
|
||||
|
||||
SavedHost updated = HostStorage.findById("id-100");
|
||||
assertNotNull(updated);
|
||||
assertEquals("new.host.org", updated.getHost());
|
||||
assertEquals(2323, updated.getPort());
|
||||
|
||||
SavedHost added = HostStorage.findByName("Development CICS");
|
||||
assertNotNull(added);
|
||||
assertEquals("cics.dev.org", added.getHost());
|
||||
assertEquals(992, added.getPort());
|
||||
assertEquals("CICS", added.getHostType());
|
||||
assertTrue(added.isUseTls());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage parses INI section prefixes and aliases properly")
|
||||
public void testImportHandlesMissingFieldsAndAliases() throws IOException {
|
||||
File tmpIni = File.createTempFile("connections_alias", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
w.write("[connection:Aliased Host]\n");
|
||||
w.write("hostname = test.alias.net\n");
|
||||
w.write("port = 1023\n");
|
||||
w.write("terminalModel = 2\n");
|
||||
w.write("type = CMS\n");
|
||||
w.write("tls = true\n");
|
||||
w.write("verifyCert = false\n");
|
||||
w.write("charset = 1047\n");
|
||||
w.write("rows = 24\n");
|
||||
w.write("cols = 80\n");
|
||||
w.write("lu = LUVM\n");
|
||||
}
|
||||
|
||||
List<SavedHost> parsed = HostStorage.loadFromIniFile(tmpIni);
|
||||
assertEquals(1, parsed.size());
|
||||
|
||||
SavedHost h = parsed.get(0);
|
||||
assertEquals("Aliased Host", h.getName());
|
||||
assertEquals("test.alias.net", h.getHost());
|
||||
assertEquals(1023, h.getPort());
|
||||
assertEquals(2, h.getModel());
|
||||
assertEquals("CMS", h.getHostType());
|
||||
assertTrue(h.isUseTls());
|
||||
assertFalse(h.isTlsVerifyCert());
|
||||
assertEquals("1047", h.getCodePage());
|
||||
assertEquals(24, h.getDynamicRows());
|
||||
assertEquals(80, h.getDynamicCols());
|
||||
assertEquals("LUVM", h.getLuName());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog contains Import Connections and Export Connections buttons and is taller")
|
||||
public void testSettingsDialogConnectionButtonsAndHeight() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
JButton btnImportConn = dialog.getBtnImportConnections();
|
||||
JButton btnExportConn = dialog.getBtnExportConnections();
|
||||
|
||||
assertNotNull(btnImportConn, "Import Connections button must exist in SettingsDialog");
|
||||
assertNotNull(btnExportConn, "Export Connections button must exist in SettingsDialog");
|
||||
|
||||
assertEquals("Import Connections...", btnImportConn.getText());
|
||||
assertEquals("Export Connections...", btnExportConn.getText());
|
||||
|
||||
// Check window height: must be taller than previous 540 to accommodate extra buttons
|
||||
assertTrue(dialog.getHeight() > 540,
|
||||
"Settings window height (" + dialog.getHeight() + ") must be taller than 540 to accommodate the extra buttons");
|
||||
|
||||
// Verify config buttons share container row1, and connection buttons share container row2
|
||||
JButton btnImport = dialog.getBtnImport();
|
||||
JButton btnExport = dialog.getBtnExport();
|
||||
assertNotNull(btnImport);
|
||||
assertNotNull(btnExport);
|
||||
|
||||
Container configRow = btnImport.getParent();
|
||||
assertNotNull(configRow);
|
||||
assertSame(configRow, btnExport.getParent(), "btnImport and btnExport must share the same parent row");
|
||||
|
||||
Container connRow = btnImportConn.getParent();
|
||||
assertNotNull(connRow);
|
||||
assertSame(connRow, btnExportConn.getParent(), "btnImportConn and btnExportConn must share the same parent row");
|
||||
|
||||
assertNotSame(configRow, connRow, "Connection buttons must be below and NOT in line with config buttons");
|
||||
|
||||
// Verify connRow is positioned below configRow in buttonPanel
|
||||
Container buttonPanel = configRow.getParent();
|
||||
assertNotNull(buttonPanel);
|
||||
assertSame(buttonPanel, connRow.getParent(), "Both rows must be in buttonPanel");
|
||||
|
||||
int configRowIdx = -1;
|
||||
int connRowIdx = -1;
|
||||
Component[] rows = buttonPanel.getComponents();
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
if (rows[i] == configRow) configRowIdx = i;
|
||||
if (rows[i] == connRow) connRowIdx = i;
|
||||
}
|
||||
assertTrue(configRowIdx >= 0, "configRow must be in buttonPanel");
|
||||
assertTrue(connRowIdx >= 0, "connRow must be in buttonPanel");
|
||||
assertTrue(connRowIdx > configRowIdx, "connRow must be below configRow in buttonPanel (" + connRowIdx + " > " + configRowIdx + ")");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog importConnectionsFile updates savedHostBox and Settings")
|
||||
public void testSettingsDialogImportConnectionsFileUpdatesDropdownAndSettings() throws IOException {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
File tmpIni = File.createTempFile("settings_dialog_import_test", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
w.write("[Auto Mainframe]\n");
|
||||
w.write("host = auto.mainframe.org\n");
|
||||
w.write("port = 992\n");
|
||||
w.write("autoConnect = true\n");
|
||||
w.write("useTls = true\n");
|
||||
}
|
||||
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
dialog.importConnectionsFile(tmpIni);
|
||||
|
||||
// Check savedHostBox updated
|
||||
JComboBox<SavedHost> box = dialog.getSavedHostBox();
|
||||
assertNotNull(box);
|
||||
assertTrue(box.getItemCount() >= 1);
|
||||
|
||||
SavedHost sel = (SavedHost) box.getSelectedItem();
|
||||
assertNotNull(sel);
|
||||
assertEquals("auto.mainframe.org", sel.getHost());
|
||||
assertTrue(sel.isAutoConnect());
|
||||
|
||||
// Check Settings updated for autoConnect host
|
||||
assertEquals("auto.mainframe.org", Settings.getAutoConnectHost());
|
||||
assertEquals(992, Settings.getAutoConnectPort());
|
||||
assertTrue(Settings.getAutoConnectTls());
|
||||
|
||||
// Test exportConnectionsFile
|
||||
File exportTarget = File.createTempFile("settings_dialog_export_test", ".ini");
|
||||
exportTarget.deleteOnExit();
|
||||
dialog.exportConnectionsFile(exportTarget);
|
||||
assertTrue(exportTarget.exists());
|
||||
String exported = new String(Files.readAllBytes(exportTarget.toPath()));
|
||||
assertTrue(exported.contains("auto.mainframe.org"));
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog importConnectionsFile throws FileNotFoundException for nonexistent file")
|
||||
public void testImportNonExistentConnectionsFileThrows() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
File nonExistent = new File("/path/to/nonexistent/connections_" + System.currentTimeMillis() + ".ini");
|
||||
assertThrows(FileNotFoundException.class, () -> {
|
||||
dialog.importConnectionsFile(nonExistent);
|
||||
});
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class AutoConnectSettingsAndTerminalCenteringTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private Settings.StartupBehavior originalStartupBehavior;
|
||||
private String originalAutoConnectHost;
|
||||
private int originalAutoConnectPort;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts());
|
||||
originalStartupBehavior = Settings.getStartupBehavior();
|
||||
originalAutoConnectHost = Settings.getAutoConnectHost();
|
||||
originalAutoConnectPort = Settings.getAutoConnectPort();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
Settings.setStartupBehavior(originalStartupBehavior);
|
||||
Settings.setAutoConnectHost(originalAutoConnectHost);
|
||||
Settings.setAutoConnectPort(originalAutoConnectPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big")
|
||||
public void testTerminalPanelCenteringLogic() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
panel.setClient(client);
|
||||
|
||||
int cols = sb.getDisplayCols(); // 80
|
||||
int rows = sb.getDisplayRows(); // 24
|
||||
int gridW = cols * panel.cellWidth; // 800
|
||||
int gridH = rows * panel.cellHeight; // 480
|
||||
|
||||
// 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0
|
||||
panel.setSize(gridW - 100, gridH - 50);
|
||||
assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained");
|
||||
assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained");
|
||||
|
||||
// 2. Panel exactly matches grid: offset must be 0
|
||||
panel.setSize(gridW, gridH);
|
||||
assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid");
|
||||
assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid");
|
||||
|
||||
// 3. Panel is larger with odd extra (extra = 7)
|
||||
panel.setSize(gridW + 7, gridH + 11);
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int leftSpace = ox;
|
||||
int rightSpace = (gridW + 7) - (ox + gridW);
|
||||
assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")");
|
||||
assertEquals(3, leftSpace);
|
||||
assertEquals(4, rightSpace);
|
||||
|
||||
int oy = panel.getRenderOffsetY();
|
||||
int topSpace = oy;
|
||||
int bottomSpace = (gridH + 11) - (oy + gridH);
|
||||
assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space");
|
||||
assertEquals(5, topSpace);
|
||||
assertEquals(6, bottomSpace);
|
||||
|
||||
// 4. Panel is larger with even extra (extra = 6)
|
||||
panel.setSize(gridW + 6, gridH + 8);
|
||||
assertEquals(3, panel.getRenderOffsetX());
|
||||
assertEquals(4, panel.getRenderOffsetY());
|
||||
|
||||
panel.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("guardedPack does not resize window when window is too small")
|
||||
public void testGuardedPackDoesNotResizeWhenTooSmall() {
|
||||
try {
|
||||
JFrame frame = new JFrame();
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
panel.setClient(client);
|
||||
|
||||
frame.getContentPane().add(panel);
|
||||
frame.setSize(400, 300);
|
||||
frame.addNotify(); // makes frame displayable
|
||||
|
||||
// Set size explicitly smaller than preferred size (800+ x 480+)
|
||||
frame.setSize(400, 300);
|
||||
Dimension sizeBefore = frame.getSize();
|
||||
|
||||
// Run guardedPack
|
||||
panel.guardedPack();
|
||||
|
||||
// Window should not have expanded
|
||||
Dimension sizeAfter = frame.getSize();
|
||||
assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small");
|
||||
assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small");
|
||||
|
||||
frame.dispose();
|
||||
panel.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
// Environment without X display
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host")
|
||||
public void testSettingsDialogSavedConnectionsDropdown() throws Exception {
|
||||
SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
|
||||
List<SavedHost> testHosts = new ArrayList<>();
|
||||
testHosts.add(host1);
|
||||
testHosts.add(host2);
|
||||
HostStorage.saveAll(testHosts);
|
||||
Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT);
|
||||
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
JComboBox<SavedHost> combo = dialog.getSavedHostBox();
|
||||
assertNotNull(combo, "savedHostBox should not be null");
|
||||
assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts");
|
||||
|
||||
// Host Alpha was autoConnect=true, so it should be initially selected
|
||||
SavedHost initialSel = (SavedHost) combo.getSelectedItem();
|
||||
assertNotNull(initialSel);
|
||||
assertEquals("id-test-1", initialSel.getId());
|
||||
|
||||
// Switch to Host Beta
|
||||
combo.setSelectedIndex(1);
|
||||
|
||||
// Trigger applySettings via reflection
|
||||
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
|
||||
applyMethod.setAccessible(true);
|
||||
applyMethod.invoke(dialog);
|
||||
|
||||
// Verify Host Beta is now the auto-connect host
|
||||
SavedHost auto = HostStorage.getAutoConnectHost();
|
||||
assertNotNull(auto, "Auto-connect host should exist");
|
||||
assertEquals("id-test-2", auto.getId());
|
||||
assertEquals("Host Beta", auto.getName());
|
||||
assertEquals("beta.mainframe.test", auto.getHost());
|
||||
assertEquals(992, auto.getPort());
|
||||
|
||||
SavedHost loadedAlpha = HostStorage.findById("id-test-1");
|
||||
assertNotNull(loadedAlpha);
|
||||
assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true");
|
||||
|
||||
// Verify Settings autoConnectHost/Port synchronized
|
||||
assertEquals("beta.mainframe.test", Settings.getAutoConnectHost());
|
||||
assertEquals(992, Settings.getAutoConnectPort());
|
||||
|
||||
// Switch startup behavior to SHOW_CONNECT and apply
|
||||
Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox");
|
||||
sbField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
JComboBox<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) sbField.get(dialog);
|
||||
sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT);
|
||||
|
||||
applyMethod.invoke(dialog);
|
||||
assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} finally {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged")
|
||||
public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() {
|
||||
try {
|
||||
JFrame frame = new JFrame();
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
frame.getContentPane().add(panel);
|
||||
frame.addNotify();
|
||||
|
||||
// Pack for Model 4 (43 rows, 80 cols)
|
||||
panel.packForModel(43, 80);
|
||||
Dimension model4Size = frame.getSize();
|
||||
assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows");
|
||||
assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols");
|
||||
|
||||
// Pack for Model 5 (27 rows, 132 cols)
|
||||
panel.packForModel(27, 132);
|
||||
Dimension model5Size = frame.getSize();
|
||||
assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols");
|
||||
|
||||
// Set custom size and verify guardedPack leaves the window alone
|
||||
frame.setSize(600, 400);
|
||||
Dimension fixedSize = frame.getSize();
|
||||
|
||||
panel.guardedPack();
|
||||
assertEquals(fixedSize.width, frame.getWidth());
|
||||
assertEquals(fixedSize.height, frame.getHeight());
|
||||
|
||||
frame.dispose();
|
||||
panel.dispose();
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.InputMethodEvent;
|
||||
import java.text.AttributedString;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class DBCSInputAndClipboardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters")
|
||||
public void testDBCSClipboardCopyNoDoubling() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2
|
||||
ExtendedAttribute cell0 = sb.getCell(0);
|
||||
cell0.db = ExtendedAttribute.DB_LEFT;
|
||||
cell0.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell1 = sb.getCell(1);
|
||||
cell1.db = ExtendedAttribute.DB_RIGHT;
|
||||
cell1.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell2 = sb.getCell(2);
|
||||
cell2.db = ExtendedAttribute.DB_NONE;
|
||||
cell2.ucs4 = 'A';
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
// Stream selection covering cells 0..2 (row 0, col 0..2)
|
||||
panel.select(0, 0, 0, 2);
|
||||
String selected = panel.getSelectedText();
|
||||
|
||||
assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters")
|
||||
public void testDBCSBlockSelectionNoDoubling() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
panel.setBlockSelectMode(true);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2
|
||||
ExtendedAttribute cell0 = sb.getCell(0);
|
||||
cell0.db = ExtendedAttribute.DB_LEFT;
|
||||
cell0.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell1 = sb.getCell(1);
|
||||
cell1.db = ExtendedAttribute.DB_RIGHT;
|
||||
cell1.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell2 = sb.getCell(2);
|
||||
cell2.db = ExtendedAttribute.DB_NONE;
|
||||
cell2.ucs4 = 'B';
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
panel.select(0, 0, 0, 2);
|
||||
String selected = panel.getSelectedText();
|
||||
|
||||
assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InputMethodRequests returns cursor bounds and processes committed text")
|
||||
public void testDBCSInputMethodRequestsAndEvents() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this");
|
||||
|
||||
Rectangle loc = panel.getTextLocation(null);
|
||||
assertNotNull(loc, "Text location must not be null");
|
||||
assertTrue(loc.width > 0, "Cursor width must be positive");
|
||||
assertTrue(loc.height > 0, "Cursor height must be positive");
|
||||
|
||||
// Simulate committed IME text '\u3000'
|
||||
AttributedString as = new AttributedString("\u3000");
|
||||
InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
|
||||
as.getIterator(), 1, null, null);
|
||||
panel.inputMethodTextChanged(imeEvent);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
|
||||
assertEquals('\u3000', sb.getCell(0).ucs4);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
|
||||
assertEquals('\u3000', sb.getCell(1).ucs4);
|
||||
assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InputProcessor backspace over DBCS character deletes both cells")
|
||||
public void testInputProcessorDBCSTypingAndBackspace() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
InputProcessor input = client.getInputProcessor();
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
sb.setCursorAddress(0);
|
||||
|
||||
// Type DBCS character
|
||||
input.typeCharacter('\u3000');
|
||||
assertEquals(2, sb.getCursorAddress());
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
|
||||
|
||||
// Backspace over DBCS character
|
||||
input.backspace();
|
||||
assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells");
|
||||
assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)");
|
||||
assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)");
|
||||
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db);
|
||||
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db);
|
||||
}
|
||||
}
|
||||
@@ -189,4 +189,106 @@ public class HotspotEngineTest {
|
||||
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
|
||||
assertEquals(0, empty.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects ISPF primary option menu choices (numbers, two-digit, and letters)")
|
||||
public void testIspfPrimaryOptionMenuDetection() {
|
||||
String screen =
|
||||
" Menu Utilities Compilers Options Status Help\n" +
|
||||
" ──────────────────────────────────────────────────────────────────────────────\n" +
|
||||
" ISPF Primary Option Menu\n" +
|
||||
" Option ===>\n" +
|
||||
" \n" +
|
||||
" 0 Settings Terminal and user parameters User ID . : RUDI\n" +
|
||||
" 1 View Display source data or listings Time. . . : 12:31\n" +
|
||||
" 2 Edit Create or change source data Terminal. : 3278\n" +
|
||||
" 3 Utilities Perform utility functions Screen. . : 1\n" +
|
||||
" 4 Foreground Interactive language processing Language. : ENGLISH\n" +
|
||||
" 5 Batch Submit job for language processing Appl ID . : ISR\n" +
|
||||
" 6 Command Enter TSO or Workstation commands TSO logon : DEVPROC\n" +
|
||||
" 7 Dialog Test Perform dialog testing TSO prefix: RUDI\n" +
|
||||
" 9 IBM Products IBM program development products System ID : VS01\n" +
|
||||
" 10 SCLM SW Configuration Library Manager Release . : ISPF 7.5\n" +
|
||||
" 11 Workplace ISPF Object/Action Workplace\n" +
|
||||
" M More Additional workplace options\n" +
|
||||
" X Exit Terminate ISPF using log and list defaults";
|
||||
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 18, 80);
|
||||
|
||||
// 14 total hotspots: 13 ISPF menu choices (0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, M, X) + 1 [ENTER] key on row 11
|
||||
assertEquals(14, hotspots.size());
|
||||
List<HotspotEngine.Hotspot> menuChoices = hotspots.stream()
|
||||
.filter(h -> h.getType() == HotspotEngine.HotspotType.MENU_CHOICE)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
assertEquals(13, menuChoices.size());
|
||||
|
||||
// Option 0
|
||||
HotspotEngine.Hotspot h0 = menuChoices.get(0);
|
||||
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, h0.getType());
|
||||
assertEquals("0 Settings", h0.getDisplayText());
|
||||
assertEquals("0[enter]", h0.getAction());
|
||||
assertEquals("Select option 0 (Settings)", h0.getTooltip());
|
||||
assertEquals(5, h0.getStartRow());
|
||||
|
||||
// Option 1
|
||||
HotspotEngine.Hotspot h1 = menuChoices.get(1);
|
||||
assertEquals("1 View", h1.getDisplayText());
|
||||
assertEquals("1[enter]", h1.getAction());
|
||||
assertEquals("Select option 1 (View)", h1.getTooltip());
|
||||
assertEquals(6, h1.getStartRow());
|
||||
|
||||
// Option 2
|
||||
HotspotEngine.Hotspot h2 = menuChoices.get(2);
|
||||
assertEquals("2 Edit", h2.getDisplayText());
|
||||
assertEquals("2[enter]", h2.getAction());
|
||||
assertEquals("Select option 2 (Edit)", h2.getTooltip());
|
||||
|
||||
// Option 10 (two-digit, index 9 after 0,1,2,3,4,5,6,7,9)
|
||||
HotspotEngine.Hotspot h10 = menuChoices.get(9);
|
||||
assertEquals("10 SCLM", h10.getDisplayText());
|
||||
assertEquals("10[enter]", h10.getAction());
|
||||
assertEquals("Select option 10 (SCLM)", h10.getTooltip());
|
||||
|
||||
// Option M (letter, index 11)
|
||||
HotspotEngine.Hotspot hM = menuChoices.get(11);
|
||||
assertEquals("M More", hM.getDisplayText());
|
||||
assertEquals("M[enter]", hM.getAction());
|
||||
assertEquals("Select option M (More)", hM.getTooltip());
|
||||
|
||||
// Option X (exit, index 12)
|
||||
HotspotEngine.Hotspot hX = menuChoices.get(12);
|
||||
assertEquals("X Exit", hX.getDisplayText());
|
||||
assertEquals("X[enter]", hX.getAction());
|
||||
assertEquals("Select option X (Exit)", hX.getTooltip());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects ISPF dotted sub-options, dashes, and multi-column menus")
|
||||
public void testIspfSubOptionsAndMultiColumn() {
|
||||
String screen =
|
||||
" 3.1 Library - Compress or print data set\n" +
|
||||
" 3.2 DataSet - Allocate, rename, or delete\n" +
|
||||
" 1 - View 2 - Edit";
|
||||
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
|
||||
assertEquals(4, hotspots.size());
|
||||
|
||||
HotspotEngine.Hotspot h31 = hotspots.get(0);
|
||||
assertEquals("3.1 Library", h31.getDisplayText());
|
||||
assertEquals("3.1[enter]", h31.getAction());
|
||||
assertEquals("Select option 3.1 (Library)", h31.getTooltip());
|
||||
|
||||
HotspotEngine.Hotspot h32 = hotspots.get(1);
|
||||
assertEquals("3.2 DataSet", h32.getDisplayText());
|
||||
assertEquals("3.2[enter]", h32.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h1 = hotspots.get(2);
|
||||
assertEquals("1 - View", h1.getDisplayText());
|
||||
assertEquals("1[enter]", h1.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h2 = hotspots.get(3);
|
||||
assertEquals("2 - Edit", h2.getDisplayText());
|
||||
assertEquals("2[enter]", h2.getAction());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,4 +98,238 @@ public class TerminalPanelSelectionTest {
|
||||
// Handled gracefully in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Mouse press and release on TerminalPanel requests focus")
|
||||
public void testTerminalPanelMousePressRequestsFocus() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
assertTrue(panel.isFocusable());
|
||||
|
||||
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||
assertTrue(mouseListeners.length > 0);
|
||||
|
||||
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, 50, 50, 1, false, MouseEvent.BUTTON1);
|
||||
mouseListeners[0].mousePressed(pressEvent);
|
||||
|
||||
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, 50, 50, 1, false, MouseEvent.BUTTON1);
|
||||
mouseListeners[0].mouseReleased(releaseEvent);
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Clicking an unprotected, intensified line (e.g. XEDIT current line) does not transmit AID_SELECT")
|
||||
public void testClickOnUnprotectedIntensifiedFieldDoesNotTriggerAidSelect() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Row 3 (address 240): Unprotected, intensified line with spaces (XEDIT located line)
|
||||
int row3Start = 3 * 80;
|
||||
sb.setCellFA(row3Start, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL)); // Unprotected, high intensity (detectable)
|
||||
// Space designator and COBOL text
|
||||
String lineText = " IDENTIFICATION DIVISION.";
|
||||
for (int i = 0; i < lineText.length(); i++) {
|
||||
sb.getCell(row3Start + 1 + i).ucs4 = lineText.charAt(i);
|
||||
sb.getCell(row3Start + 1 + i).ec = (byte) (lineText.charAt(i) == ' ' ? 0x40 : 0xC1 + i);
|
||||
}
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||
assertTrue(mouseListeners.length > 0);
|
||||
MouseListener ml = mouseListeners[0];
|
||||
|
||||
// Click at row 3, col 28 (like in j3270.log)
|
||||
int clickX = ox + 28 * cellW + cellW / 2;
|
||||
int clickY = oy + 3 * cellH + cellH / 2;
|
||||
|
||||
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mousePressed(pressEvent);
|
||||
|
||||
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mouseReleased(releaseEvent);
|
||||
|
||||
// Cursor must move to clicked address without sending AID_SELECT (AID 0x7E)
|
||||
assertEquals(row3Start + 28, sb.getCursorAddress(), "Cursor must be placed at clicked position");
|
||||
assertEquals(AID_NO, client.getInputProcessor().getLastAid(), "No AID must be transmitted on single click");
|
||||
|
||||
// Text on line must NOT have been modified or cleared
|
||||
assertEquals('I', (char) sb.getCell(row3Start + 8).ucs4);
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Clicking a toggle field (? -> >) toggles the designator without transmitting AID")
|
||||
public void testClickOnToggleField() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Field with ? designator
|
||||
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||
sb.getCell(1).ucs4 = '?';
|
||||
sb.getCell(1).ec = (byte) 0x6F;
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener ml = panel.getMouseListeners()[0];
|
||||
|
||||
int clickX = ox + 1 * cellW + cellW / 2;
|
||||
int clickY = oy + cellH / 2;
|
||||
|
||||
ml.mousePressed(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
ml.mouseReleased(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
|
||||
assertEquals('>', (char) sb.getCell(1).ucs4, "Toggle field ? should toggle to > on click");
|
||||
assertEquals(AID_NO, client.getInputProcessor().getLastAid(), "No AID transmitted for deferred toggle field");
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Clicking an unprotected space-designator field when lightPenMode is enabled transmits AID_SELECT")
|
||||
public void testClickInLightPenModeTransmitsAidSelect() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Unprotected, high-intensity field with space designator
|
||||
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
|
||||
sb.getCell(1).ucs4 = ' ';
|
||||
sb.getCell(1).ec = (byte) 0x40;
|
||||
|
||||
panel.setClient(client);
|
||||
panel.toggleLightPen();
|
||||
assertTrue(panel.isLightPenMode());
|
||||
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener ml = panel.getMouseListeners()[0];
|
||||
|
||||
int clickX = ox + 1 * cellW + cellW / 2;
|
||||
int clickY = oy + cellH / 2;
|
||||
|
||||
ml.mousePressed(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
ml.mouseReleased(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
|
||||
assertEquals(AID_SELECT, client.getInputProcessor().getLastAid(), "AID_SELECT must be transmitted in lightPenMode");
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Clicking an ISPF menu choice navigates to home/option line, types choice, and sends AID_ENTER")
|
||||
public void testClickOnIspfMenuChoiceSubmitsViaHomeAndEnter() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Row 3 (col 0): Protected label "Option ===> "
|
||||
int row3Start = 3 * 80;
|
||||
sb.setCellFA(row3Start, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
String promptText = "Option ===> ";
|
||||
for (int i = 0; i < promptText.length(); i++) {
|
||||
sb.getCell(row3Start + 1 + i).ucs4 = promptText.charAt(i);
|
||||
sb.getCell(row3Start + 1 + i).ec = (byte) (0xC1 + i);
|
||||
}
|
||||
|
||||
// Unprotected command input field after "Option ===> " at address 253 (row 3 col 13)
|
||||
int inputFieldAddr = row3Start + 13;
|
||||
sb.setCellFA(inputFieldAddr, (byte) FA_PRINTABLE); // Unprotected!
|
||||
for (int i = 0; i < 20; i++) {
|
||||
sb.getCell(inputFieldAddr + 1 + i).ucs4 = ' ';
|
||||
sb.getCell(inputFieldAddr + 1 + i).ec = (byte) 0x40;
|
||||
}
|
||||
|
||||
// Row 6 (address 480): Protected field with ISPF menu option " 1 View"
|
||||
int row6Start = 6 * 80;
|
||||
sb.setCellFA(row6Start, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
String menuText = " 1 View Display source data or listings";
|
||||
for (int i = 0; i < menuText.length(); i++) {
|
||||
sb.getCell(row6Start + 1 + i).ucs4 = menuText.charAt(i);
|
||||
sb.getCell(row6Start + 1 + i).ec = (byte) (menuText.charAt(i) == ' ' ? 0x40 : 0xC1 + i);
|
||||
}
|
||||
|
||||
sb.updateFormatted();
|
||||
sb.updateDisplaySnapshot();
|
||||
|
||||
panel.setClient(client);
|
||||
panel.setHotspotsDirty(true);
|
||||
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener ml = panel.getMouseListeners()[0];
|
||||
|
||||
// Click at row 6, col 2 (on the "1" in " 1 View")
|
||||
int clickX = ox + 2 * cellW + cellW / 2;
|
||||
int clickY = oy + 6 * cellH + cellH / 2;
|
||||
|
||||
ml.mousePressed(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
ml.mouseReleased(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
|
||||
// AID_ENTER must have been transmitted
|
||||
assertEquals(AID_ENTER, client.getInputProcessor().getLastAid(), "Clicking menu choice must transmit AID_ENTER");
|
||||
|
||||
// Keyboard lock should be standard system lock awaiting host response, NOT protected field error
|
||||
assertNotEquals(com.ibm.eNetwork.ECL.ECLConstants.INHIBIT_PROTECTED_FIELD, client.getOIA().getInputInhibited(),
|
||||
"Keyboard must not be locked with protected field violation");
|
||||
|
||||
// Option "1" must have been typed into the unprotected command input field (address inputFieldAddr + 1)
|
||||
assertEquals('1', (char) sb.getCell(inputFieldAddr + 1).ucs4, "Option '1' must be placed on the command/option line");
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -17,3 +23,39 @@ jar {
|
||||
'Implementation-Version': archiveVersion
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
groupId = 'haus.nightmare.j3270'
|
||||
artifactId = 'lib3270j'
|
||||
version = project.version
|
||||
|
||||
pom {
|
||||
name = 'lib3270j'
|
||||
description = 'Pure Java IBM 3270 terminal emulation and protocol library with Host On-Demand (HOD) parity'
|
||||
url = 'https://github.com/rudi/j3270'
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Unlicense'
|
||||
url = 'https://unlicense.org/'
|
||||
distribution = 'repo'
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = 'rudi'
|
||||
name = 'Rudi'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = 'scm:git:git://github.com/rudi/j3270.git'
|
||||
developerConnection = 'scm:git:ssh://github.com/rudi/j3270.git'
|
||||
url = 'https://github.com/rudi/j3270'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,18 @@ public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements EC
|
||||
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
|
||||
}
|
||||
|
||||
public short getAttribute() { return GetAttribute(); }
|
||||
public boolean IsDBCS() {
|
||||
return delegate != null && delegate.isDBCS();
|
||||
}
|
||||
|
||||
public boolean isDBCS() { return IsDBCS(); }
|
||||
|
||||
public boolean IsDBCSOnly() {
|
||||
return delegate != null && delegate.isDBCSOnly();
|
||||
}
|
||||
|
||||
public boolean isDBCSOnly() { return IsDBCSOnly(); }
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
|
||||
}
|
||||
|
||||
|
||||
@@ -511,4 +511,61 @@ public class ECLPS implements ECLConstants, Serializable, Cloneable {
|
||||
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
|
||||
return delegate.GetScreen(buffer, len, row, col, length, plane);
|
||||
}
|
||||
|
||||
// ========== DBCS Character Status ==========
|
||||
|
||||
public boolean IsDBCSChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCSChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); }
|
||||
|
||||
public boolean IsDBCSChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCSChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); }
|
||||
|
||||
public boolean IsDBCS1stChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCS1stChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); }
|
||||
|
||||
public boolean IsDBCS1stChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCS1stChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); }
|
||||
|
||||
public boolean IsDBCS2ndChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCS2ndChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); }
|
||||
|
||||
public boolean IsDBCS2ndChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCS2ndChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); }
|
||||
|
||||
public boolean IsSOChar(int pos) {
|
||||
return delegate != null && delegate.IsSOChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSOChar(int pos) { return IsSOChar(pos); }
|
||||
|
||||
public boolean IsSIChar(int pos) {
|
||||
return delegate != null && delegate.IsSIChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSIChar(int pos) { return IsSIChar(pos); }
|
||||
|
||||
public boolean IsSOSIChar(int pos) {
|
||||
return delegate != null && delegate.IsSOSIChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); }
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,12 @@ public class ConnectionConfig {
|
||||
private int reconnectMaxRetries = 5;
|
||||
private int tcpUserTimeoutMs = 0;
|
||||
|
||||
// Host operating system type (TSO, CMS, CICS)
|
||||
private String hostType = "TSO";
|
||||
private boolean savedConnection = false;
|
||||
private String savedProfileId = null;
|
||||
private String savedProfileName = null;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
@@ -312,6 +318,51 @@ public class ConnectionConfig {
|
||||
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
|
||||
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
|
||||
|
||||
public String getHostType() {
|
||||
return hostType != null ? hostType : "TSO";
|
||||
}
|
||||
|
||||
public void setHostType(String hostType) {
|
||||
this.hostType = (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO";
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ft.FTConfig.HostType getFtHostType() {
|
||||
if (hostType != null) {
|
||||
try {
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(hostType.trim().toUpperCase());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
|
||||
}
|
||||
|
||||
public void setFtHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ftHostType) {
|
||||
this.hostType = (ftHostType != null) ? ftHostType.name() : "TSO";
|
||||
}
|
||||
|
||||
public boolean isSavedConnection() {
|
||||
return savedConnection;
|
||||
}
|
||||
|
||||
public void setSavedConnection(boolean savedConnection) {
|
||||
this.savedConnection = savedConnection;
|
||||
}
|
||||
|
||||
public String getSavedProfileId() {
|
||||
return savedProfileId;
|
||||
}
|
||||
|
||||
public void setSavedProfileId(String savedProfileId) {
|
||||
this.savedProfileId = savedProfileId;
|
||||
}
|
||||
|
||||
public String getSavedProfileName() {
|
||||
return savedProfileName;
|
||||
}
|
||||
|
||||
public void setSavedProfileName(String savedProfileName) {
|
||||
this.savedProfileName = savedProfileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
|
||||
@@ -11,6 +11,7 @@ import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Main API entry point for lib3270j.
|
||||
@@ -46,6 +47,7 @@ public class Telnet3270Client {
|
||||
|
||||
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
private volatile Thread reconnectThread;
|
||||
private volatile boolean intentionallyDisconnected = false;
|
||||
|
||||
public Telnet3270Client(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
@@ -130,6 +132,7 @@ public class Telnet3270Client {
|
||||
* then returns while telnet/TN3270E negotiation continues asynchronously.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
intentionallyDisconnected = false;
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
|
||||
" model=" + config.getModel());
|
||||
|
||||
@@ -193,8 +196,16 @@ public class Telnet3270Client {
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
intentionallyDisconnected = true;
|
||||
cancelAutoReconnect();
|
||||
if (connection != null) {
|
||||
try {
|
||||
if (fsm != null) {
|
||||
fsm.sendTelnetDisconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Error during Telnet disconnect negotiation", e);
|
||||
}
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
@@ -203,14 +214,20 @@ public class Telnet3270Client {
|
||||
|
||||
public void cancelAutoReconnect() {
|
||||
reconnecting.set(false);
|
||||
if (reconnectThread != null) {
|
||||
reconnectThread.interrupt();
|
||||
Thread t = reconnectThread;
|
||||
if (t != null) {
|
||||
t.interrupt();
|
||||
try {
|
||||
t.join(200);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
reconnectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void initiateAutoReconnect() {
|
||||
if (config == null || !config.isAutoReconnect()) {
|
||||
if (intentionallyDisconnected || config == null || !config.isAutoReconnect()) {
|
||||
return;
|
||||
}
|
||||
if (!reconnecting.compareAndSet(false, true)) {
|
||||
@@ -368,6 +385,9 @@ public class Telnet3270Client {
|
||||
* Check if connected (any state past TCP pending).
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
if (fsm != null && fsm.getConnectionState() == ConnectionState.NOT_CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
return connection != null && connection.isConnected();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
/**
|
||||
* Version information for lib3270j.
|
||||
*/
|
||||
public final class Version {
|
||||
|
||||
/**
|
||||
* Hardcoded fallback release version matching build.gradle and pom coordinates.
|
||||
*/
|
||||
public static final String VERSION = "1.2.4";
|
||||
|
||||
/**
|
||||
* Canonical library name.
|
||||
*/
|
||||
public static final String NAME = "lib3270j";
|
||||
|
||||
private Version() {}
|
||||
|
||||
/**
|
||||
* Retrieves the runtime version string of lib3270j.
|
||||
* Inspects the package implementation version first (populated when loaded from a JAR),
|
||||
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
|
||||
*
|
||||
* @return the version string (e.g. "1.2.4")
|
||||
*/
|
||||
public static String getVersion() {
|
||||
Package pkg = Version.class.getPackage();
|
||||
if (pkg != null) {
|
||||
String implVer = pkg.getImplementationVersion();
|
||||
if (implVer != null && !implVer.isBlank()) {
|
||||
return implVer;
|
||||
}
|
||||
}
|
||||
return VERSION;
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,16 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
|
||||
int[] sbcsMapping, String nioCharsetName) {
|
||||
super(id, description, cpgid, cgcsgid, sbcsMapping);
|
||||
if (nioCharsetName != null) {
|
||||
for (String candidate : nioCharsetName.split(",")) {
|
||||
candidate = candidate.trim();
|
||||
try {
|
||||
if (Charset.isSupported(nioCharsetName)) {
|
||||
this.nioCharset = Charset.forName(nioCharsetName);
|
||||
if (Charset.isSupported(candidate)) {
|
||||
this.nioCharset = Charset.forName(candidate);
|
||||
break;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
// Initialize ideographic space (0x4040 <-> \u3000)
|
||||
registerDbcsPair(0x4040, '\u3000');
|
||||
}
|
||||
@@ -250,4 +254,78 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
|
||||
public byte[] utf8WithSOSIToEbcdic(String s) {
|
||||
return stringToEbcdic(s, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Double-byte character set Graphic Character Set Global ID (DBGID/CGCSGID) matching HoD DBGIDTable.
|
||||
*/
|
||||
public int getDbCgcsgid() {
|
||||
switch (getCpgid()) {
|
||||
case 930:
|
||||
case 939:
|
||||
case 290:
|
||||
return 370; // 0x0172
|
||||
case 933:
|
||||
return 934; // 0x03A6
|
||||
case 935:
|
||||
return 937; // 0x03A9
|
||||
case 937:
|
||||
return 935; // 0x03A7
|
||||
case 1390:
|
||||
case 1399:
|
||||
case 1364:
|
||||
case 1371:
|
||||
case 1388:
|
||||
return 65535; // 0xFFFF
|
||||
default:
|
||||
return 370;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Double-byte character set Code Page Global ID (DBCS CPGID) matching HoD DBGIDTable.
|
||||
*/
|
||||
public int getDbCpgid() {
|
||||
switch (getCpgid()) {
|
||||
case 930:
|
||||
case 939:
|
||||
case 290:
|
||||
case 1390:
|
||||
case 1399:
|
||||
return 300; // 0x012C
|
||||
case 933:
|
||||
case 1364:
|
||||
return 834; // 0x0342
|
||||
case 935:
|
||||
case 1388:
|
||||
return 837; // 0x0345
|
||||
case 937:
|
||||
case 1371:
|
||||
return 835; // 0x0343
|
||||
default:
|
||||
return 300;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Double-byte character set section start byte matching HoD DBCSSectTable.
|
||||
*/
|
||||
public byte getDbcsSectionStart() {
|
||||
return 0x41;
|
||||
}
|
||||
|
||||
/**
|
||||
* Double-byte character set section end byte matching HoD DBCSSectTable.
|
||||
*/
|
||||
public byte getDbcsSectionEnd() {
|
||||
switch (getCpgid()) {
|
||||
case 290:
|
||||
case 930:
|
||||
case 935:
|
||||
case 939:
|
||||
return 0x7F;
|
||||
default:
|
||||
return (byte) 0xFE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,58 @@ public class CodePageRegistry {
|
||||
private static final Map<String, CodePage> CODE_PAGES = new LinkedHashMap<>();
|
||||
private static final Map<String, String> ALIASES = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Standard human-readable codepage option labels for UI dropdowns and menus.
|
||||
* Covers core SBCS, all 10 Euro variants (1140-1149), Extended SBCS (Arabic,
|
||||
* Hebrew, Thai, Cyrillic, Turkish), and Mixed/Extended DBCS.
|
||||
*/
|
||||
public static final String[] COMMON_CODE_PAGES = {
|
||||
"037 - US / Canada / Brazil",
|
||||
"1047 - IBM Open Systems / z/OS Unix",
|
||||
"500 - International Latin-1",
|
||||
"273 - Germany / Austria",
|
||||
"277 - Denmark / Norway",
|
||||
"278 - Sweden / Finland",
|
||||
"280 - Italy",
|
||||
"284 - Spain / Latin America",
|
||||
"285 - United Kingdom",
|
||||
"297 - France",
|
||||
"870 - Eastern Europe / Latin-2",
|
||||
"871 - Iceland",
|
||||
"875 - Greece (Greek)",
|
||||
"1026 - Turkey (Turkish Latin-5)",
|
||||
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
|
||||
"905 - Turkey (Turkish Latin-3)",
|
||||
"420 - Arabic Bilingual",
|
||||
"424 - Hebrew (with Latin)",
|
||||
"803 - Hebrew Character Set",
|
||||
"838 - Thai Extended",
|
||||
"1160 - Thai Euro \u20AC",
|
||||
"1025 - Cyrillic Multilingual",
|
||||
"1123 - Cyrillic Ukraine",
|
||||
"1154 - Cyrillic Euro \u20AC",
|
||||
"880 - Cyrillic (Russian)",
|
||||
"1140 - US / Canada (Euro \u20AC)",
|
||||
"1141 - Germany / Austria (Euro \u20AC)",
|
||||
"1142 - Denmark / Norway (Euro \u20AC)",
|
||||
"1143 - Sweden / Finland (Euro \u20AC)",
|
||||
"1144 - Italy (Euro \u20AC)",
|
||||
"1145 - Spain / Latin America (Euro \u20AC)",
|
||||
"1146 - United Kingdom (Euro \u20AC)",
|
||||
"1147 - France (Euro \u20AC)",
|
||||
"1148 - International (Euro \u20AC)",
|
||||
"1149 - Iceland (Euro \u20AC)",
|
||||
"290 - Japanese Katakana SBCS",
|
||||
"1027 - Japanese Latin SBCS",
|
||||
"930 - Japanese Katakana Mixed DBCS",
|
||||
"939 - Japanese Latin Mixed DBCS",
|
||||
"935 - Simplified Chinese Mixed DBCS",
|
||||
"937 - Traditional Chinese Mixed DBCS",
|
||||
"1388 - Simplified Chinese Extended DBCS",
|
||||
"1371 - Traditional Chinese Extended DBCS",
|
||||
"933 - Korean Mixed DBCS"
|
||||
};
|
||||
|
||||
static {
|
||||
// Register core SBCS codepages
|
||||
register(new Cp037());
|
||||
@@ -311,6 +363,12 @@ public class CodePageRegistry {
|
||||
register(new Cp1388());
|
||||
register(new Cp1371());
|
||||
|
||||
// Japanese Katakana & Latin SBCS / DBCS Euro
|
||||
register(new Cp290());
|
||||
register(new Cp1027());
|
||||
register(new Cp1390());
|
||||
register(new Cp1399());
|
||||
|
||||
// Setup Extended Aliases
|
||||
addAlias("ar", "420");
|
||||
addAlias("arabic", "420");
|
||||
@@ -360,10 +418,12 @@ public class CodePageRegistry {
|
||||
addAlias("zh-tw-ext", "1371");
|
||||
|
||||
// HoD Converter aliases mapping all HoD converter names to CodePage / Charset
|
||||
addAlias("1390", "930");
|
||||
addAlias("1390jis2004", "930");
|
||||
addAlias("1399", "939");
|
||||
addAlias("1399jis2004", "939");
|
||||
addAlias("1390", "1390");
|
||||
addAlias("1390jis2004", "1390");
|
||||
addAlias("1399", "1399");
|
||||
addAlias("1399jis2004", "1399");
|
||||
addAlias("290", "290");
|
||||
addAlias("1027", "1027");
|
||||
addAlias("937macau", "937");
|
||||
addAlias("1364", "933");
|
||||
addAlias("1379", "937");
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1027 (Japanese Latin Extended SBCS).
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp939.singleByteToChar.
|
||||
* CCSID / CPGID: 1027, GCSGID: 1172.
|
||||
*/
|
||||
public class Cp1027 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1027";
|
||||
public static final String DESCRIPTION = "Japanese Latin Extended - EBCDIC";
|
||||
public static final int CPGID = 1027;
|
||||
public static final int GCSGID = 1172;
|
||||
|
||||
private static final String SBCS_STR =
|
||||
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
|
||||
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
|
||||
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
|
||||
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
|
||||
" \ufffd\uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\u00a2.<(+|" +
|
||||
"&\uff69\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\uff70\uff71!$*);\u00ac" +
|
||||
"-/\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\ufffd,%_>?" +
|
||||
"\uff7a\uff7b\uff7c\uff7d\uff7e\uff7f\uff80\uff81\uff82`:#@'=\"" +
|
||||
"\ufffdabcdefghi\uff83\uff84\uff85\uff86\uff87\uff88" +
|
||||
"\ufffdjklmnopqr\uff89\uff8a\uff8b\uff8c\uff8d\uff8e" +
|
||||
"\u203e~stuvwxyz\uff8f\uff90\uff91[\uff92\uff93^" +
|
||||
"\u00a3\u00a5\uff94\uff95\uff96\uff97\uff98\uff99\uff9a\uff9b\uff9c\uff9d]\uff9e\uff9f" +
|
||||
"{ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
|
||||
"}JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
|
||||
"\\\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
|
||||
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
|
||||
|
||||
public static final int[] MAPPING = new int[256];
|
||||
static {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
|
||||
}
|
||||
}
|
||||
|
||||
public Cp1027() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1390 (Japanese Katakana Extended Mixed DBCS with Euro support: host SBCS CP290+Euro / DBCS CP16684).
|
||||
* CCSID / CPGID: 1390, GCSGID: 1172.
|
||||
*/
|
||||
public class Cp1390 extends AbstractDBCSCodePage {
|
||||
|
||||
public static final String ID = "1390";
|
||||
public static final String DESCRIPTION = "Japanese Katakana Extended Mixed DBCS (with Euro)";
|
||||
public static final int CPGID = 1390;
|
||||
public static final int GCSGID = 1172;
|
||||
|
||||
public static final int[] MAPPING = Cp290.MAPPING.clone();
|
||||
static {
|
||||
// Euro symbol at 0xE1
|
||||
MAPPING[0xE1] = 0x20AC;
|
||||
}
|
||||
|
||||
public Cp1390() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1390,IBM1390,Cp1390,x-IBM930,IBM930,Cp930");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1399 (Japanese Latin Extended Mixed DBCS with Euro support: host SBCS CP1027+Euro / DBCS CP16684).
|
||||
* CCSID / CPGID: 1399, GCSGID: 1172.
|
||||
*/
|
||||
public class Cp1399 extends AbstractDBCSCodePage {
|
||||
|
||||
public static final String ID = "1399";
|
||||
public static final String DESCRIPTION = "Japanese Latin Extended Mixed DBCS (with Euro)";
|
||||
public static final int CPGID = 1399;
|
||||
public static final int GCSGID = 1172;
|
||||
|
||||
public static final int[] MAPPING = Cp1027.MAPPING.clone();
|
||||
static {
|
||||
// Euro symbol at 0xE1
|
||||
MAPPING[0xE1] = 0x20AC;
|
||||
}
|
||||
|
||||
public Cp1399() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1399,IBM1399,Cp1399,x-IBM939,IBM939,Cp939");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 290 (Japanese Katakana Extended SBCS).
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp290.
|
||||
* CCSID / CPGID: 290, GCSGID: 1172.
|
||||
*/
|
||||
public class Cp290 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "290";
|
||||
public static final String DESCRIPTION = "Japanese Katakana Extended - EBCDIC";
|
||||
public static final int CPGID = 290;
|
||||
public static final int GCSGID = 1172;
|
||||
|
||||
private static final String SBCS_STR =
|
||||
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
|
||||
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
|
||||
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
|
||||
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
|
||||
" \uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\uff69\u00a3.<(+|" +
|
||||
"&\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\ufffd\uff70\ufffd!\u00a5*);\u00ac" +
|
||||
"-/\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ufffd,%_>?" +
|
||||
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000:#@'=\"" +
|
||||
"\u0000\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\uff7a\u0000\uff7b\uff7c\uff7d\uff7e" +
|
||||
"\uff7f\uff80\uff81\uff82\uff83\uff84\uff85\uff86\uff87\uff88\uff89\u0000\ufffd\uff8a\uff8b\uff8c" +
|
||||
"\u0000\u203e\uff8d\uff8e\uff8f\uff90\uff91\uff92\uff93\uff94\uff95\u0000\uff96\uff97\uff98\uff99" +
|
||||
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\uff9a\uff9b\uff9c\uff9d\uff9e\uff9f" +
|
||||
"\u0000ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
|
||||
"\u0000JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
|
||||
"$\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
|
||||
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
|
||||
|
||||
public static final int[] MAPPING = new int[256];
|
||||
static {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
|
||||
}
|
||||
}
|
||||
|
||||
public Cp290() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,6 @@ public class Cp930 extends AbstractDBCSCodePage {
|
||||
public static final int GCSGID = 1172;
|
||||
|
||||
public Cp930() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM930");
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, Cp290.MAPPING, "x-IBM930,IBM930,Cp930");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ public class Cp939 extends AbstractDBCSCodePage {
|
||||
public static final int GCSGID = 1172;
|
||||
|
||||
public Cp939() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM939");
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, Cp1027.MAPPING, "x-IBM939,IBM939,Cp939");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,35 @@ public class EbcdicTranslator {
|
||||
return isDBCSCodePage();
|
||||
}
|
||||
|
||||
public synchronized int getDbCgcsgid() {
|
||||
if (activeCodePage instanceof AbstractDBCSCodePage) {
|
||||
return ((AbstractDBCSCodePage) activeCodePage).getDbCgcsgid();
|
||||
}
|
||||
return 370;
|
||||
}
|
||||
|
||||
public synchronized int getDbCpgid() {
|
||||
if (activeCodePage instanceof AbstractDBCSCodePage) {
|
||||
return ((AbstractDBCSCodePage) activeCodePage).getDbCpgid();
|
||||
}
|
||||
return 300;
|
||||
}
|
||||
|
||||
public synchronized byte getDbcsSectionStart() {
|
||||
if (activeCodePage instanceof AbstractDBCSCodePage) {
|
||||
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionStart();
|
||||
}
|
||||
return 0x41;
|
||||
}
|
||||
|
||||
public synchronized byte getDbcsSectionEnd() {
|
||||
if (activeCodePage instanceof AbstractDBCSCodePage) {
|
||||
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionEnd();
|
||||
}
|
||||
return 0x7F;
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Custom Character Translation Overrides
|
||||
// =========================================================================
|
||||
|
||||
+214
-11
@@ -53,6 +53,7 @@ public class DataStreamProcessor {
|
||||
private boolean unlockPending = false;
|
||||
private boolean unlockSysPending = false;
|
||||
private boolean rcvdRead = false;
|
||||
private boolean qreplyPending = false;
|
||||
|
||||
// Modal SA (set attribute) character attributes
|
||||
private byte currentFg = 0;
|
||||
@@ -60,6 +61,8 @@ public class DataStreamProcessor {
|
||||
private byte currentGr = 0;
|
||||
private byte currentCs = 0;
|
||||
private byte currentOl = 0;
|
||||
private byte currentTr = 0;
|
||||
private boolean inDBCS = false;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
@@ -181,11 +184,13 @@ public class DataStreamProcessor {
|
||||
switch (cmd) {
|
||||
case CMD_W:
|
||||
case SNA_CMD_W:
|
||||
qreplyPending = false;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processWrite(data, offset, length, false);
|
||||
break;
|
||||
case CMD_EW:
|
||||
case SNA_CMD_EW:
|
||||
qreplyPending = false;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine(">>> ERASE/WRITE: clearing screen (default size)");
|
||||
@@ -206,6 +211,7 @@ public class DataStreamProcessor {
|
||||
break;
|
||||
case CMD_EWA:
|
||||
case SNA_CMD_EWA:
|
||||
qreplyPending = false;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
|
||||
@@ -234,7 +240,12 @@ public class DataStreamProcessor {
|
||||
case SNA_CMD_RM:
|
||||
rcvdRead = true;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (qreplyPending) {
|
||||
qreplyPending = false;
|
||||
sendAllQueryReplies();
|
||||
} else {
|
||||
processReadModified(false);
|
||||
}
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
@@ -244,7 +255,11 @@ public class DataStreamProcessor {
|
||||
break;
|
||||
case CMD_EAU:
|
||||
case SNA_CMD_EAU:
|
||||
qreplyPending = false;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
currentFg = 0;
|
||||
@@ -252,6 +267,7 @@ public class DataStreamProcessor {
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
currentTr = 0;
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
@@ -277,6 +293,7 @@ public class DataStreamProcessor {
|
||||
cmd == CMD_EWA || cmd == SNA_CMD_EWA);
|
||||
if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
|
||||
// Debug: dump non-empty screen lines
|
||||
@@ -376,10 +393,14 @@ public class DataStreamProcessor {
|
||||
if (kbdRestore) {
|
||||
unlockPending = true;
|
||||
unlockSysPending = true;
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
}
|
||||
|
||||
if (!contentionResolution && kbdRestore && inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
|
||||
if (resetMdt) {
|
||||
@@ -395,6 +416,8 @@ public class DataStreamProcessor {
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
currentTr = 0;
|
||||
inDBCS = false;
|
||||
}
|
||||
|
||||
if (eraseFirst) {
|
||||
@@ -405,6 +428,8 @@ public class DataStreamProcessor {
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
currentTr = 0;
|
||||
inDBCS = false;
|
||||
}
|
||||
|
||||
// Process orders and data starting at byte 2
|
||||
@@ -435,6 +460,7 @@ public class DataStreamProcessor {
|
||||
}
|
||||
|
||||
case ORDER_SF: { // Start Field
|
||||
inDBCS = false;
|
||||
if (pos + 1 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
@@ -456,6 +482,7 @@ public class DataStreamProcessor {
|
||||
}
|
||||
|
||||
case ORDER_SFE: { // Start Field Extended
|
||||
inDBCS = false;
|
||||
if (pos + 1 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
@@ -502,6 +529,7 @@ public class DataStreamProcessor {
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
currentTr = 0;
|
||||
break;
|
||||
case XA_FOREGROUND:
|
||||
currentFg = (byte) attrValue;
|
||||
@@ -530,6 +558,9 @@ public class DataStreamProcessor {
|
||||
case 0x84:
|
||||
currentOl = (byte) attrValue;
|
||||
break;
|
||||
case XA_TRANSPARENCY:
|
||||
currentTr = (byte) attrValue;
|
||||
break;
|
||||
}
|
||||
pos += 3;
|
||||
lastWasOrder = true;
|
||||
@@ -591,6 +622,44 @@ public class DataStreamProcessor {
|
||||
if (toAddr >= size)
|
||||
toAddr = toAddr % size;
|
||||
|
||||
boolean isDbcsSession = (translator != null && translator.isDBCS());
|
||||
if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS) && pos + 4 < end) {
|
||||
int fillChar2 = data[pos + 4] & 0xFF;
|
||||
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(fillChar, fillChar2) : '?';
|
||||
do {
|
||||
ExtendedAttribute ea1 = screen.getCell(baddr);
|
||||
ea1.fa = 0;
|
||||
ea1.ec = (byte) fillChar;
|
||||
ea1.ucs4 = uni;
|
||||
ea1.fg = currentFg;
|
||||
ea1.bg = currentBg;
|
||||
ea1.gr = (byte) currentGr;
|
||||
ea1.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea1.db = ExtendedAttribute.DB_LEFT;
|
||||
ea1.ol = currentOl;
|
||||
ea1.tr = currentTr;
|
||||
baddr = (baddr + 1) % size;
|
||||
if (baddr == toAddr) break;
|
||||
|
||||
ExtendedAttribute ea2 = screen.getCell(baddr);
|
||||
ea2.fa = 0;
|
||||
ea2.ec = (byte) fillChar2;
|
||||
ea2.ucs4 = uni;
|
||||
ea2.fg = currentFg;
|
||||
ea2.bg = currentBg;
|
||||
ea2.gr = (byte) currentGr;
|
||||
ea2.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea2.db = ExtendedAttribute.DB_RIGHT;
|
||||
ea2.ol = currentOl;
|
||||
ea2.tr = currentTr;
|
||||
baddr = (baddr + 1) % size;
|
||||
} while (baddr != toAddr);
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 5;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle GE (graphic escape) prefix
|
||||
byte fillCs = currentCs;
|
||||
char fillUcs4 = 0;
|
||||
@@ -614,7 +683,9 @@ public class DataStreamProcessor {
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = fillCs;
|
||||
ea.db = ExtendedAttribute.DB_NONE;
|
||||
ea.ol = currentOl;
|
||||
ea.tr = currentTr;
|
||||
baddr = (baddr + 1) % size;
|
||||
} while (baddr != toAddr);
|
||||
screen.setBufferAddress(baddr);
|
||||
@@ -644,6 +715,7 @@ public class DataStreamProcessor {
|
||||
ea.gr = 0;
|
||||
ea.cs = 0;
|
||||
ea.ol = 0;
|
||||
ea.tr = 0;
|
||||
}
|
||||
}
|
||||
baddr = (baddr + 1) % size;
|
||||
@@ -669,6 +741,7 @@ public class DataStreamProcessor {
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.ol = currentOl;
|
||||
ea.tr = currentTr;
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 2;
|
||||
@@ -677,6 +750,98 @@ public class DataStreamProcessor {
|
||||
}
|
||||
|
||||
default: {
|
||||
boolean isDbcsSession = (translator != null && translator.isDBCS());
|
||||
if (isDbcsSession && b == 0x0E) { // Shift-Out (enter DBCS mode)
|
||||
inDBCS = true;
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0;
|
||||
ea.ec = (byte) 0x0E;
|
||||
ea.ucs4 = ' ';
|
||||
ea.cs = ExtendedAttribute.CS_BASE;
|
||||
ea.db = ExtendedAttribute.DB_SO;
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.ol = currentOl;
|
||||
ea.tr = currentTr;
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 1;
|
||||
lastWasOrder = false;
|
||||
break;
|
||||
} else if (isDbcsSession && b == 0x0F) { // Shift-In (exit DBCS mode)
|
||||
inDBCS = false;
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0;
|
||||
ea.ec = (byte) 0x0F;
|
||||
ea.ucs4 = ' ';
|
||||
ea.cs = ExtendedAttribute.CS_BASE;
|
||||
ea.db = ExtendedAttribute.DB_SI;
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.ol = currentOl;
|
||||
ea.tr = currentTr;
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 1;
|
||||
lastWasOrder = false;
|
||||
break;
|
||||
} else if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS)) {
|
||||
if (pos + 1 < end) {
|
||||
int b2 = data[pos + 1] & 0xFF;
|
||||
if (b2 == 0x0F) { // Orphaned single byte before SI
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0;
|
||||
ea.ec = (byte) b;
|
||||
ea.ucs4 = translator.ebcdicToUnicode(b);
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = currentCs;
|
||||
ea.ol = currentOl;
|
||||
ea.tr = currentTr;
|
||||
ea.db = ExtendedAttribute.DB_NONE;
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 1;
|
||||
lastWasOrder = false;
|
||||
break;
|
||||
}
|
||||
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(b, b2) : '?';
|
||||
ExtendedAttribute ea1 = screen.getCell(baddr);
|
||||
ea1.fa = 0;
|
||||
ea1.ec = (byte) b;
|
||||
ea1.ucs4 = uni;
|
||||
ea1.fg = currentFg;
|
||||
ea1.bg = currentBg;
|
||||
ea1.gr = (byte) currentGr;
|
||||
ea1.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea1.db = ExtendedAttribute.DB_LEFT;
|
||||
ea1.ol = currentOl;
|
||||
ea1.tr = currentTr;
|
||||
|
||||
int nextAddr = (baddr + 1) % size;
|
||||
ExtendedAttribute ea2 = screen.getCell(nextAddr);
|
||||
ea2.fa = 0;
|
||||
ea2.ec = (byte) b2;
|
||||
ea2.ucs4 = uni;
|
||||
ea2.fg = currentFg;
|
||||
ea2.bg = currentBg;
|
||||
ea2.gr = (byte) currentGr;
|
||||
ea2.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea2.db = ExtendedAttribute.DB_RIGHT;
|
||||
ea2.ol = currentOl;
|
||||
ea2.tr = currentTr;
|
||||
|
||||
baddr = (baddr + 2) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 2;
|
||||
lastWasOrder = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular data byte (EBCDIC character)
|
||||
// Format control codes display as nulls/blanks per 3270 spec
|
||||
boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF ||
|
||||
@@ -688,14 +853,18 @@ public class DataStreamProcessor {
|
||||
|
||||
if (isFCOrder) {
|
||||
ea.ec = 0; // Display as null/blank
|
||||
ea.ucs4 = 0;
|
||||
} else {
|
||||
ea.ec = (byte) b;
|
||||
ea.ucs4 = (translator != null) ? translator.ebcdicToUnicode(b) : (char) b;
|
||||
}
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = currentCs;
|
||||
ea.db = ExtendedAttribute.DB_NONE;
|
||||
ea.ol = currentOl;
|
||||
ea.tr = currentTr;
|
||||
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
@@ -773,13 +942,26 @@ public class DataStreamProcessor {
|
||||
// ========== Read Buffer ==========
|
||||
|
||||
public void processReadBuffer() {
|
||||
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||
? inputProcessor.getLastAid()
|
||||
: AID_NO;
|
||||
processReadBuffer(aid);
|
||||
}
|
||||
|
||||
public void processReadBuffer(int aid) {
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
byte[] data = inputProcessor.buildReadBufferInboundData(aid);
|
||||
if (outputSender != null) {
|
||||
outputSender.send3270Data(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
outputPos = 0;
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||
? inputProcessor.getLastAid()
|
||||
: AID_NO;
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
@@ -796,7 +978,7 @@ public class DataStreamProcessor {
|
||||
// Buffer contents depending on Reply Mode
|
||||
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
|
||||
// Extended Field or Character Mode
|
||||
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
|
||||
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0, curTr = 0;
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute()) {
|
||||
@@ -807,6 +989,7 @@ public class DataStreamProcessor {
|
||||
if (ea.gr != 0) count++;
|
||||
if (ea.cs != 0) count++;
|
||||
if (ea.ol != 0) count++;
|
||||
if (ea.tr != 0) count++;
|
||||
|
||||
outputWrite(ORDER_SFE);
|
||||
outputWrite(count);
|
||||
@@ -825,6 +1008,7 @@ public class DataStreamProcessor {
|
||||
}
|
||||
if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); }
|
||||
if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); }
|
||||
if (ea.tr != 0) { outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF); }
|
||||
} else {
|
||||
if (mode == SF_SRM_CHAR) {
|
||||
// In character mode, output SA if character attributes differ
|
||||
@@ -850,6 +1034,10 @@ public class DataStreamProcessor {
|
||||
outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF);
|
||||
curCs = ea.cs;
|
||||
}
|
||||
if (ea.tr != curTr) {
|
||||
outputWrite(ORDER_SA); outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF);
|
||||
curTr = ea.tr;
|
||||
}
|
||||
}
|
||||
outputWrite(ea.ec & 0xFF);
|
||||
}
|
||||
@@ -873,12 +1061,18 @@ public class DataStreamProcessor {
|
||||
// ========== Read Modified ==========
|
||||
|
||||
public void processReadModified(boolean all) {
|
||||
int aid = (inputProcessor != null && inputProcessor.isKeyboardLocked() && inputProcessor.getLastAid() != 0 && inputProcessor.getLastAid() != AID_NO)
|
||||
? inputProcessor.getLastAid()
|
||||
: AID_NO;
|
||||
processReadModified(aid, all);
|
||||
}
|
||||
|
||||
public void processReadModified(int aid, boolean all) {
|
||||
if (ftDft != null && ftDft.readModified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputProcessor != null) {
|
||||
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
|
||||
if (outputSender != null) {
|
||||
@@ -889,9 +1083,6 @@ public class DataStreamProcessor {
|
||||
|
||||
outputPos = 0;
|
||||
|
||||
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||
? inputProcessor.getLastAid()
|
||||
: AID_NO;
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
@@ -1054,9 +1245,11 @@ public class DataStreamProcessor {
|
||||
log.info("ReadPartition Query — sending base query replies");
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
qreplyPending = true;
|
||||
sendAllQueryReplies();
|
||||
break;
|
||||
case SF_RP_QLIST:
|
||||
qreplyPending = true;
|
||||
if (fieldLen >= 6) {
|
||||
int listType = data[offset + 5] & 0xFF;
|
||||
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
|
||||
@@ -1079,14 +1272,16 @@ public class DataStreamProcessor {
|
||||
sendAllQueryReplies();
|
||||
}
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
processReadModified(true);
|
||||
processReadModified(AID_QREPLY, true);
|
||||
break;
|
||||
case SNA_CMD_RB:
|
||||
processReadBuffer();
|
||||
processReadBuffer(AID_QREPLY);
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
processReadModified(false);
|
||||
processReadModified(AID_QREPLY, false);
|
||||
break;
|
||||
default:
|
||||
log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type));
|
||||
@@ -1517,6 +1712,14 @@ public class DataStreamProcessor {
|
||||
processReadModified(true);
|
||||
}
|
||||
|
||||
public boolean isQreplyPending() {
|
||||
return qreplyPending;
|
||||
}
|
||||
|
||||
public void setQreplyPending(boolean qreplyPending) {
|
||||
this.qreplyPending = qreplyPending;
|
||||
}
|
||||
|
||||
public void processWriteStructuredField(byte[] data) {
|
||||
if (data != null && data.length > 0) {
|
||||
processWriteStructuredField(data, 0, data.length);
|
||||
|
||||
@@ -8,6 +8,12 @@ import java.util.Arrays;
|
||||
* Reusable, resizable byte buffer that provides zero-copy access to its internal
|
||||
* array and supports ByteBuffer slicing to minimize heap allocations during
|
||||
* network stream processing.
|
||||
* <p>
|
||||
* <b>Threading Contract:</b> This class is NOT thread-safe and is designed for
|
||||
* high-throughput single-threaded or thread-confined stream accumulation (such as
|
||||
* Telnet network receiver loops matching IBM Host On-Demand {@code sArray} and
|
||||
* {@code sb_buffer}). External synchronization must be used if instances are shared
|
||||
* across threads.
|
||||
*/
|
||||
public class FastByteBuffer extends OutputStream {
|
||||
|
||||
@@ -31,13 +37,13 @@ public class FastByteBuffer extends OutputStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int b) {
|
||||
public void write(int b) {
|
||||
ensureCapacity(count + 1);
|
||||
buf[count++] = (byte) b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b, int off, int len) {
|
||||
public void write(byte[] b, int off, int len) {
|
||||
if (b == null || len <= 0) return;
|
||||
ensureCapacity(count + len);
|
||||
System.arraycopy(b, off, buf, count, len);
|
||||
@@ -48,29 +54,29 @@ public class FastByteBuffer extends OutputStream {
|
||||
* Direct reference to internal buffer array.
|
||||
* Use {@link #size()} to determine active length.
|
||||
*/
|
||||
public synchronized byte[] buffer() {
|
||||
public byte[] buffer() {
|
||||
return buf;
|
||||
}
|
||||
|
||||
public synchronized int size() {
|
||||
public int size() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public synchronized void reset() {
|
||||
public void reset() {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read-only ByteBuffer view wrapping active bytes without copying.
|
||||
*/
|
||||
public synchronized ByteBuffer asByteBuffer() {
|
||||
public ByteBuffer asByteBuffer() {
|
||||
return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read-only ByteBuffer slice for a sub-range without copying.
|
||||
*/
|
||||
public synchronized ByteBuffer slice(int offset, int length) {
|
||||
public ByteBuffer slice(int offset, int length) {
|
||||
if (offset < 0 || length < 0 || offset + length > count) {
|
||||
throw new IndexOutOfBoundsException("offset=" + offset + " length=" + length + " size=" + count);
|
||||
}
|
||||
@@ -80,7 +86,7 @@ public class FastByteBuffer extends OutputStream {
|
||||
/**
|
||||
* Produces a copied byte array if an isolated copy is explicitly required.
|
||||
*/
|
||||
public synchronized byte[] toByteArray() {
|
||||
public byte[] toByteArray() {
|
||||
return Arrays.copyOf(buf, count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ public class QueryReplyBuilder {
|
||||
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
|
||||
QR_AUXDA, // 0x99 - Auxiliary Devices
|
||||
QR_IMP_PART, // 0xa6 - Implicit Partition Sizes
|
||||
QR_TRANSPARENCY, // 0xa8 - Background Transparency
|
||||
};
|
||||
|
||||
// Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING
|
||||
@@ -218,6 +219,9 @@ public class QueryReplyBuilder {
|
||||
// Implicit Partition (0xA6)
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
// Background Transparency (0xA8)
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
|
||||
|
||||
log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")");
|
||||
return out.toByteArray();
|
||||
}
|
||||
@@ -245,9 +249,9 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
|
||||
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
|
||||
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
|
||||
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
|
||||
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
|
||||
@@ -317,11 +321,7 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
break;
|
||||
case QR_TRANSPARENCY: // 0xA8
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_SEGMENT: // 0xB0
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
@@ -390,10 +390,17 @@ public class QueryReplyBuilder {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||
boolean wroteDbcs = false;
|
||||
for (int code : codes) {
|
||||
if (isDbcs && !wroteDbcs && code == QR_DDM) {
|
||||
out.write(QR_OUTLINING);
|
||||
out.write(QR_DBCS_ASIA);
|
||||
wroteDbcs = true;
|
||||
}
|
||||
out.write(code);
|
||||
if (isDbcs && code == QR_OUTLINING) {
|
||||
if (isDbcs && code == QR_OUTLINING && !wroteDbcs) {
|
||||
out.write(QR_DBCS_ASIA); // 0x91
|
||||
wroteDbcs = true;
|
||||
}
|
||||
}
|
||||
return out.toByteArray();
|
||||
@@ -441,7 +448,7 @@ public class QueryReplyBuilder {
|
||||
|
||||
public int getCharWidth(int rows) {
|
||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||
return 12;
|
||||
return 12; // 0x0C
|
||||
}
|
||||
return SW_3279_2; // 9
|
||||
}
|
||||
@@ -452,6 +459,9 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public int getCharHeight(int rows) {
|
||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||
return 24; // 0x18
|
||||
}
|
||||
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||
}
|
||||
|
||||
@@ -477,9 +487,54 @@ public class QueryReplyBuilder {
|
||||
|
||||
int cgcsgid = 0x02B9;
|
||||
int cpgid = 0x0025;
|
||||
boolean isDbcs = false;
|
||||
if (screen != null && screen.getTranslator() != null) {
|
||||
cgcsgid = screen.getTranslator().getCgcsgid();
|
||||
cpgid = screen.getTranslator().getCpgid();
|
||||
isDbcs = screen.getTranslator().isDBCS();
|
||||
}
|
||||
|
||||
if (isDbcs) {
|
||||
int dbCgcsgid = screen.getTranslator().getDbCgcsgid();
|
||||
int dbCpgid = screen.getTranslator().getDbCpgid();
|
||||
byte secStart = screen.getTranslator().getDbcsSectionStart();
|
||||
byte secEnd = screen.getTranslator().getDbcsSectionEnd();
|
||||
|
||||
// 42-byte DBCS Character Sets QR payload (total 46 bytes structured field, matching HoD DS3270.java xxxqrcs)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(42);
|
||||
out.write(0x8E); // flags: GE | DBCS / 2-byte charsets (0x8E)
|
||||
out.write(0x00); // flags 2
|
||||
out.write(charW); // SDW (12)
|
||||
out.write(charH); // SDH (24)
|
||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); // 4 reserved bytes
|
||||
out.write(0x0B); // DL = 11 bytes per descriptor
|
||||
|
||||
// Descriptor 1: Base SBCS (11 bytes)
|
||||
out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
|
||||
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
|
||||
|
||||
// Descriptor 2: DBCS Set 1 (11 bytes)
|
||||
out.write(0x01); // Set ID = 1
|
||||
out.write(0x00); // Flags
|
||||
out.write(0x80); // Local ID = 0x80 (DBCS)
|
||||
out.write((charW * 2) & 0xFF); // Double cell width (24)
|
||||
out.write(charH & 0xFF); // Cell height (24)
|
||||
out.write(secStart); // Section start (e.g. 0x41)
|
||||
out.write(secEnd); // Section end (e.g. 0xFE or 0x7F)
|
||||
out.write((dbCgcsgid >> 8) & 0xFF); out.write(dbCgcsgid & 0xFF);
|
||||
out.write((dbCpgid >> 8) & 0xFF); out.write(dbCpgid & 0xFF);
|
||||
|
||||
// Descriptor 3: APL / Text (11 bytes)
|
||||
out.write(0x01); out.write(0x00); out.write(0xF1);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x03); out.write(0xC3); // CGCSGID 963
|
||||
out.write(0x01); out.write(0x36); // CPGID 310
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||
@@ -531,6 +586,7 @@ public class QueryReplyBuilder {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
public byte[] buildColor() {
|
||||
return STATIC_QR_COLOR.clone();
|
||||
}
|
||||
|
||||
@@ -5,13 +5,27 @@ package haus.nightmare.lib3270j.ecl;
|
||||
*/
|
||||
public interface ECLConstants {
|
||||
|
||||
// Presentation space logical planes
|
||||
// Presentation space logical planes (Bitmask & ordinal definitions matching IBM Host On-Demand)
|
||||
int PLANE_TEXT = 1; // Unicode/EBCDIC characters
|
||||
int PLANE_COLOR = 2; // Color attributes
|
||||
int PLANE_HILITE = 3; // Extended highlighting
|
||||
int PLANE_EXTENDED = 4; // Extended character sets / outlining
|
||||
int PLANE_FIELD = 5; // Field attribute definition bytes
|
||||
int PLANE_DBCS = 6; // Double-byte character plane
|
||||
int PLANE_DBCS = 6; // Double-byte character plane (lib3270j convention)
|
||||
int PLANE_GRID = 8; // Grid lines / outlining plane
|
||||
|
||||
// Canonical IBM Host On-Demand ECLConstants plane bitmasks
|
||||
int TEXT_PLANE = 1;
|
||||
int COLOR_PLANE = 2;
|
||||
int DBCS_PLANE = 4;
|
||||
int GRID_PLANE = 8;
|
||||
int FIELD_PLANE = 16;
|
||||
int EXFIELD_PLANE = 32;
|
||||
int NLS_PLANE = 64;
|
||||
int HOST_PLANE = 128;
|
||||
int RAWTEXT_PLANE = 256;
|
||||
int COLOR_ATTRIBUTES_PLANE = 512;
|
||||
int ALL_PLANES = 319;
|
||||
|
||||
// Color definitions (IBM 3279 standard 16-color palette)
|
||||
char COLOR_NEUTRAL_BLACK = 0;
|
||||
|
||||
@@ -295,6 +295,54 @@ public class ECLField {
|
||||
}
|
||||
public void Erase() { erase(); }
|
||||
|
||||
/**
|
||||
* Check whether this field contains any DBCS characters or SO/SI delimiters.
|
||||
*/
|
||||
public boolean isDBCS() {
|
||||
if (ps == null || length <= 0) return false;
|
||||
ScreenBuffer sb = ps.getScreenBuffer();
|
||||
if (sb == null) return false;
|
||||
int size = sb.getRows() * sb.getCols();
|
||||
if (size <= 0) return false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
int addr = (dataStart + i) % size;
|
||||
ExtendedAttribute ea = sb.getCell(addr);
|
||||
if (ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|
||||
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean IsDBCS() { return isDBCS(); }
|
||||
|
||||
/**
|
||||
* Check whether this field contains only DBCS characters (excluding nulls / spaces).
|
||||
*/
|
||||
public boolean isDBCSOnly() {
|
||||
if (ps == null || length <= 0) return false;
|
||||
ScreenBuffer sb = ps.getScreenBuffer();
|
||||
if (sb == null) return false;
|
||||
int size = sb.getRows() * sb.getCols();
|
||||
if (size <= 0) return false;
|
||||
boolean hasDBCS = false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
int addr = (dataStart + i) % size;
|
||||
ExtendedAttribute ea = sb.getCell(addr);
|
||||
if (ea != null) {
|
||||
if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|
||||
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
|
||||
hasDBCS = true;
|
||||
} else if (ea.ec != 0 && ea.ec != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasDBCS;
|
||||
}
|
||||
public boolean IsDBCSOnly() { return isDBCSOnly(); }
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -25,9 +25,22 @@ public class ECLFieldList extends Vector<ECLField> {
|
||||
refresh();
|
||||
}
|
||||
|
||||
public ECLFieldList(ECLPS ps) {
|
||||
this(ps, ps != null ? ps.getScreenBuffer() : null);
|
||||
}
|
||||
|
||||
public ECLField getField(int index) {
|
||||
return (index >= 0 && index < size()) ? get(index) : null;
|
||||
}
|
||||
|
||||
public ECLField GetField(int index) {
|
||||
return getField(index);
|
||||
}
|
||||
|
||||
public ECLPS getPS() { return ps; }
|
||||
public ScreenBuffer getScreenBuffer() { return screen; }
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the field list by scanning the presentation space buffer.
|
||||
*/
|
||||
|
||||
@@ -264,10 +264,12 @@ public class ECLPS implements ECLConstants {
|
||||
case PLANE_TEXT:
|
||||
if (ea.isFieldAttribute()) {
|
||||
destBuffer[i] = ' ';
|
||||
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
|
||||
destBuffer[i] = ' ';
|
||||
} else if (ea.ucs4 != 0) {
|
||||
destBuffer[i] = (char) ea.ucs4;
|
||||
} else if (ea.ec != 0) {
|
||||
destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||
destBuffer[i] = (translator != null) ? translator.ebcdicToUnicode(ea.ec & 0xFF) : (char) (ea.ec & 0xFF);
|
||||
} else {
|
||||
destBuffer[i] = ' ';
|
||||
}
|
||||
@@ -278,12 +280,43 @@ public class ECLPS implements ECLConstants {
|
||||
case PLANE_HILITE:
|
||||
destBuffer[i] = (char) (ea.gr & 0xFF);
|
||||
break;
|
||||
case PLANE_EXTENDED:
|
||||
case DBCS_PLANE: // 4 (HoD DBCS_PLANE) or PLANE_EXTENDED for SBCS
|
||||
if (translator != null && translator.isDBCS()) {
|
||||
if (ea.db == ExtendedAttribute.DB_LEFT) {
|
||||
destBuffer[i] = (char) 0xC0; // DB_DBHIGH (first half of DBCS)
|
||||
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
|
||||
destBuffer[i] = (char) 0x80; // DB_DBCS (second half of DBCS)
|
||||
} else if (ea.db == ExtendedAttribute.DB_SO) {
|
||||
destBuffer[i] = (char) 0x20; // DB_SO
|
||||
} else if (ea.db == ExtendedAttribute.DB_SI) {
|
||||
destBuffer[i] = (char) 0x10; // DB_SI
|
||||
} else {
|
||||
destBuffer[i] = 0;
|
||||
}
|
||||
} else {
|
||||
destBuffer[i] = (char) (ea.cs & 0xFF);
|
||||
}
|
||||
break;
|
||||
case PLANE_FIELD:
|
||||
case FIELD_PLANE: // 16
|
||||
destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0;
|
||||
break;
|
||||
case PLANE_DBCS: // 6
|
||||
if (ea.db == ExtendedAttribute.DB_LEFT) {
|
||||
destBuffer[i] = (char) 0xC0;
|
||||
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
|
||||
destBuffer[i] = (char) 0x80;
|
||||
} else if (ea.db == ExtendedAttribute.DB_SO) {
|
||||
destBuffer[i] = (char) 0x20;
|
||||
} else if (ea.db == ExtendedAttribute.DB_SI) {
|
||||
destBuffer[i] = (char) 0x10;
|
||||
} else {
|
||||
destBuffer[i] = 0;
|
||||
}
|
||||
break;
|
||||
case GRID_PLANE: // 8 (HoD GRID_PLANE / Outlining)
|
||||
destBuffer[i] = (char) (ea.ol & 0xFF);
|
||||
break;
|
||||
default:
|
||||
destBuffer[i] = ' ';
|
||||
break;
|
||||
@@ -292,6 +325,107 @@ public class ECLPS implements ECLConstants {
|
||||
return copyLen;
|
||||
}
|
||||
|
||||
public synchronized boolean IsDBCSChar(int pos) {
|
||||
if (screen == null) return false;
|
||||
int size = getSize();
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
ExtendedAttribute ea = screen.getCell(pos);
|
||||
return ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT);
|
||||
}
|
||||
|
||||
public synchronized boolean isDBCSChar(int pos) {
|
||||
return IsDBCSChar(pos);
|
||||
}
|
||||
|
||||
public synchronized boolean IsDBCSChar(int row, int col) {
|
||||
if (screen == null) return false;
|
||||
return IsDBCSChar(screen.rowColToAddress(row, col));
|
||||
}
|
||||
|
||||
public synchronized boolean isDBCSChar(int row, int col) {
|
||||
return IsDBCSChar(row, col);
|
||||
}
|
||||
|
||||
public synchronized boolean IsDBCS1stChar(int pos) {
|
||||
if (screen == null) return false;
|
||||
int size = getSize();
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
ExtendedAttribute ea = screen.getCell(pos);
|
||||
return ea != null && ea.db == ExtendedAttribute.DB_LEFT;
|
||||
}
|
||||
|
||||
public synchronized boolean isDBCS1stChar(int pos) {
|
||||
return IsDBCS1stChar(pos);
|
||||
}
|
||||
|
||||
public synchronized boolean IsDBCS1stChar(int row, int col) {
|
||||
if (screen == null) return false;
|
||||
return IsDBCS1stChar(screen.rowColToAddress(row, col));
|
||||
}
|
||||
|
||||
public synchronized boolean isDBCS1stChar(int row, int col) {
|
||||
return IsDBCS1stChar(row, col);
|
||||
}
|
||||
|
||||
public synchronized boolean IsDBCS2ndChar(int pos) {
|
||||
if (screen == null) return false;
|
||||
int size = getSize();
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
ExtendedAttribute ea = screen.getCell(pos);
|
||||
return ea != null && ea.db == ExtendedAttribute.DB_RIGHT;
|
||||
}
|
||||
|
||||
public synchronized boolean isDBCS2ndChar(int pos) {
|
||||
return IsDBCS2ndChar(pos);
|
||||
}
|
||||
|
||||
public synchronized boolean IsDBCS2ndChar(int row, int col) {
|
||||
if (screen == null) return false;
|
||||
return IsDBCS2ndChar(screen.rowColToAddress(row, col));
|
||||
}
|
||||
|
||||
public synchronized boolean isDBCS2ndChar(int row, int col) {
|
||||
return IsDBCS2ndChar(row, col);
|
||||
}
|
||||
|
||||
public synchronized boolean IsSOChar(int pos) {
|
||||
if (screen == null) return false;
|
||||
int size = getSize();
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
ExtendedAttribute ea = screen.getCell(pos);
|
||||
return ea != null && (ea.db == ExtendedAttribute.DB_SO || (ea.ec & 0xFF) == 0x0E);
|
||||
}
|
||||
|
||||
public synchronized boolean isSOChar(int pos) {
|
||||
return IsSOChar(pos);
|
||||
}
|
||||
|
||||
public synchronized boolean IsSIChar(int pos) {
|
||||
if (screen == null) return false;
|
||||
int size = getSize();
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
ExtendedAttribute ea = screen.getCell(pos);
|
||||
return ea != null && (ea.db == ExtendedAttribute.DB_SI || (ea.ec & 0xFF) == 0x0F);
|
||||
}
|
||||
|
||||
public synchronized boolean isSIChar(int pos) {
|
||||
return IsSIChar(pos);
|
||||
}
|
||||
|
||||
public synchronized boolean IsSOSIChar(int pos) {
|
||||
return IsSOChar(pos) || IsSIChar(pos);
|
||||
}
|
||||
|
||||
public synchronized boolean isSOSIChar(int pos) {
|
||||
return IsSOSIChar(pos);
|
||||
}
|
||||
|
||||
|
||||
public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) {
|
||||
if (cArray == null || len <= 0 || screen == null) return 0;
|
||||
int minR = Math.max(1, Math.min(sRow, eRow));
|
||||
|
||||
@@ -9,10 +9,11 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public class FTConfig {
|
||||
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FB|VB|VBS|FBS|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?(?:[\\s,]+(\\d+))?\\)?");
|
||||
private static final Pattern DIR_PATTERN = Pattern.compile("(?i)(?:DIR|DIRECTORY)[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
|
||||
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
|
||||
@@ -68,6 +69,7 @@ public class FTConfig {
|
||||
private int blksize = 0;
|
||||
private int primarySpace = 0;
|
||||
private int secondarySpace = 0;
|
||||
private int directoryBlocks = 0;
|
||||
private int avblock = 0;
|
||||
private int dftBufferSize = FTConstants.DFT_BUF;
|
||||
private String otherOptions = null;
|
||||
@@ -172,6 +174,20 @@ public class FTConfig {
|
||||
public int getSecondarySpace() { return secondarySpace; }
|
||||
public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; }
|
||||
|
||||
public int getDirectoryBlocks() { return directoryBlocks; }
|
||||
public void setDirectoryBlocks(int directoryBlocks) { this.directoryBlocks = Math.max(0, directoryBlocks); }
|
||||
public void setDirectoryBlocks(String dir) {
|
||||
if (dir == null || dir.trim().isEmpty()) {
|
||||
this.directoryBlocks = 0;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.directoryBlocks = Math.max(0, Integer.parseInt(dir.trim()));
|
||||
} catch (NumberFormatException e) {
|
||||
this.directoryBlocks = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int getAvblock() { return avblock; }
|
||||
public void setAvblock(int avblock) { this.avblock = avblock; }
|
||||
|
||||
@@ -217,9 +233,9 @@ public class FTConfig {
|
||||
this.recfm = RecordFormat.DEFAULT;
|
||||
} else {
|
||||
String u = r.trim().toUpperCase();
|
||||
if (u.equals("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED;
|
||||
else if (u.equals("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE;
|
||||
else if (u.equals("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED;
|
||||
if (u.startsWith("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED;
|
||||
else if (u.startsWith("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE;
|
||||
else if (u.startsWith("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED;
|
||||
else this.recfm = RecordFormat.DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -233,14 +249,23 @@ public class FTConfig {
|
||||
}
|
||||
|
||||
public void setSpace(String s) {
|
||||
// Space string like "10,5" for primary/secondary
|
||||
// Space string like "10,5" or "10,5,10" for primary/secondary/directory
|
||||
if (s == null || s.trim().isEmpty()) return;
|
||||
String[] parts = s.split(",");
|
||||
try {
|
||||
if (parts.length > 0) this.primarySpace = Integer.parseInt(parts[0].trim());
|
||||
if (parts.length > 1) this.secondarySpace = Integer.parseInt(parts[1].trim());
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
if (parts.length > 0 && !parts[0].trim().isEmpty()) {
|
||||
this.primarySpace = Integer.parseInt(parts[0].trim());
|
||||
if (this.units == AllocationUnit.DEFAULT) {
|
||||
this.units = AllocationUnit.TRACKS;
|
||||
}
|
||||
}
|
||||
if (parts.length > 1 && !parts[1].trim().isEmpty()) {
|
||||
this.secondarySpace = Integer.parseInt(parts[1].trim());
|
||||
}
|
||||
if (parts.length > 2 && !parts[2].trim().isEmpty()) {
|
||||
this.directoryBlocks = Math.max(0, Integer.parseInt(parts[2].trim()));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +304,22 @@ public class FTConfig {
|
||||
if (spaceMatcher.group(2) != null) {
|
||||
this.secondarySpace = Integer.parseInt(spaceMatcher.group(2));
|
||||
}
|
||||
if (spaceMatcher.group(3) != null) {
|
||||
this.directoryBlocks = Math.max(0, Integer.parseInt(spaceMatcher.group(3)));
|
||||
}
|
||||
if (this.units == AllocationUnit.DEFAULT) {
|
||||
this.units = AllocationUnit.TRACKS;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher dirMatcher = DIR_PATTERN.matcher(trimmed);
|
||||
if (dirMatcher.find()) {
|
||||
try {
|
||||
this.directoryBlocks = Math.max(0, Integer.parseInt(dirMatcher.group(1)));
|
||||
if (this.units == AllocationUnit.DEFAULT) {
|
||||
this.units = AllocationUnit.TRACKS;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
@@ -501,7 +542,12 @@ public class FTConfig {
|
||||
StringBuilder cmd = new StringBuilder();
|
||||
|
||||
// Host program selection: IND$FILE or APVUFILE
|
||||
cmd.append(getProgramName()).append(" ");
|
||||
// For TSO, prefix with "TSO " so it can be called from any ISPF command line or native prompt
|
||||
String prog = getProgramName();
|
||||
if (hostType == HostType.TSO && !prog.toUpperCase().startsWith("TSO ")) {
|
||||
cmd.append("TSO ");
|
||||
}
|
||||
cmd.append(prog).append(" ");
|
||||
cmd.append(isReceive() ? "GET " : "PUT ");
|
||||
cmd.append(hostFilename != null ? hostFilename.trim() : "");
|
||||
|
||||
@@ -568,14 +614,18 @@ public class FTConfig {
|
||||
opts.append(" BLKSIZE(").append(blksize).append(")");
|
||||
}
|
||||
}
|
||||
if (units != AllocationUnit.DEFAULT) {
|
||||
if (units != AllocationUnit.DEFAULT || primarySpace > 0 || directoryBlocks > 0) {
|
||||
AllocationUnit effectiveUnits = (units != AllocationUnit.DEFAULT) ? units : AllocationUnit.TRACKS;
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("SPACE(").append(primarySpace);
|
||||
if (secondarySpace > 0) {
|
||||
if (secondarySpace > 0 || directoryBlocks > 0) {
|
||||
opts.append(",").append(secondarySpace);
|
||||
}
|
||||
if (directoryBlocks > 0) {
|
||||
opts.append(",").append(directoryBlocks);
|
||||
}
|
||||
opts.append(")");
|
||||
switch (units) {
|
||||
switch (effectiveUnits) {
|
||||
case TRACKS: opts.append(" TRACKS"); break;
|
||||
case CYLINDERS: opts.append(" CYLINDERS"); break;
|
||||
case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break;
|
||||
|
||||
@@ -816,15 +816,14 @@ public class FTDft {
|
||||
|
||||
for (int i = 0; i < lineStr.length(); i++) {
|
||||
char ch = lineStr.charAt(i);
|
||||
int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1;
|
||||
boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF);
|
||||
if (charIsDbcs) {
|
||||
int dbcsVal = (isDbcs && translator != null) ? translator.unicodeToDbcs(ch) : -1;
|
||||
if (dbcsVal != -1) {
|
||||
if (!inDbcsState && !noSo) {
|
||||
converted.write(0x0E); // Shift-Out
|
||||
inDbcsState = true;
|
||||
}
|
||||
converted.write((ebc >> 8) & 0xFF);
|
||||
converted.write(ebc & 0xFF);
|
||||
converted.write((dbcsVal >> 8) & 0xFF);
|
||||
converted.write(dbcsVal & 0xFF);
|
||||
} else {
|
||||
if (inDbcsState && !noSo) {
|
||||
converted.write(0x0F); // Shift-In
|
||||
|
||||
@@ -17,7 +17,7 @@ public class CMSDirectoryParser {
|
||||
return entries;
|
||||
}
|
||||
|
||||
String[] lines = text.split("\r?\n");
|
||||
String[] lines = splitIntoLines(text);
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
@@ -26,6 +26,16 @@ public class CMSDirectoryParser {
|
||||
|
||||
String upper = trimmed.toUpperCase();
|
||||
if (upper.startsWith("FILENAME") || upper.startsWith("DIRECTORY") || upper.startsWith("FILELIST")) continue;
|
||||
if (upper.startsWith("READY;") || upper.startsWith("READY(") || upper.equals("READY")
|
||||
|| upper.startsWith("MORE...") || upper.startsWith("HOLDING")
|
||||
|| upper.startsWith("RUNNING") || upper.equals("RUNNING")
|
||||
|| upper.startsWith("DMSLST") || upper.startsWith("DMS")) continue;
|
||||
|
||||
if (upper.endsWith("MORE...") || upper.endsWith("HOLDING") || upper.endsWith("RUNNING")) {
|
||||
int idx = Math.max(upper.lastIndexOf("MORE..."), Math.max(upper.lastIndexOf("HOLDING"), upper.lastIndexOf("RUNNING")));
|
||||
trimmed = trimmed.substring(0, idx).trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
}
|
||||
|
||||
CMSDirectoryEntry entry = parseCmsLine(trimmed);
|
||||
if (entry != null) {
|
||||
@@ -36,6 +46,24 @@ public class CMSDirectoryParser {
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static String[] splitIntoLines(String text) {
|
||||
if (text.contains("\n") || text.contains("\r")) {
|
||||
return text.split("\r?\n");
|
||||
}
|
||||
if (text.length() >= 80) {
|
||||
int lineLen = (text.length() % 132 == 0 && text.length() % 80 != 0) ? 132 : 80;
|
||||
int numLines = (text.length() + lineLen - 1) / lineLen;
|
||||
String[] lines = new String[numLines];
|
||||
for (int i = 0; i < numLines; i++) {
|
||||
int start = i * lineLen;
|
||||
int end = Math.min(start + lineLen, text.length());
|
||||
lines[i] = text.substring(start, end);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
return new String[]{text};
|
||||
}
|
||||
|
||||
private static CMSDirectoryEntry parseCmsLine(String line) {
|
||||
String[] tokens = line.split("\\s+");
|
||||
if (tokens.length < 2) return null;
|
||||
|
||||
@@ -11,12 +11,16 @@ import java.util.regex.Pattern;
|
||||
public class TSODirectoryParser {
|
||||
|
||||
private static final Pattern DSN_PATTERN = Pattern.compile("([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)+)");
|
||||
private static final Pattern NONVSAM_PATTERN = Pattern.compile("NONVSAM\\s+-+\\s+([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern VOLSER_PATTERN = Pattern.compile("VOLSER-+([A-Z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("RECFM-+([A-Z]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("LRECL-+([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern BLKSIZE_PATTERN = Pattern.compile("BLKSIZE-+([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DSORG_PATTERN = Pattern.compile("DSORG-+([A-Z]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NONVSAM_PATTERN = Pattern.compile("(?:NONVSAM|CLUSTER|GDG|ALIAS|AIX|PATH)\\s*[-:=]+\\s*([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)*)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern VOLSER_PATTERN = Pattern.compile("VOLSER\\s*[-:=]+\\s*([A-Z0-9*@#$&]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("RECFM\\s*[-:=]+\\s*([A-Z0-9,]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("(?:AVG|MAX)?LRECL\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern BLKSIZE_PATTERN = Pattern.compile("(?:CISIZE|BLKSIZE)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DSORG_PATTERN = Pattern.compile("DSORG\\s*[-:=]+\\s*([A-Z0-9\\-]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern EXTENTS_PATTERN = Pattern.compile("(?:VOL-)?EXTENTS\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern SPACE_PRI_PATTERN = Pattern.compile("(?:SPACE-PRI|ALLOC-PRM|ALLOC_PRM)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern SPACE_TYPE_PATTERN = Pattern.compile("SPACE-TYPE\\s*[-:=]+\\s*([A-Z]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TRACKS_PATTERN = Pattern.compile("(?:TRACKS|TRKS)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* Parse raw text containing TSO dataset listings into a structured list of TSODirectoryEntry.
|
||||
@@ -27,19 +31,27 @@ public class TSODirectoryParser {
|
||||
return entries;
|
||||
}
|
||||
|
||||
String[] lines = text.split("\r?\n");
|
||||
String[] lines = splitIntoLines(text);
|
||||
|
||||
// Check if text is LISTCAT format
|
||||
if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT")) {
|
||||
if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT") || text.toUpperCase().contains("IDCAMS")
|
||||
|| text.toUpperCase().contains("CLUSTER") || text.toUpperCase().contains("GDG")) {
|
||||
parseListcat(lines, entries);
|
||||
if (!entries.isEmpty()) return entries;
|
||||
}
|
||||
|
||||
// Check if text is LISTDS format
|
||||
if (text.toUpperCase().contains("--RECFM-") || text.toUpperCase().contains("--VOLUMES--")) {
|
||||
parseListds(lines, entries);
|
||||
if (!entries.isEmpty()) return entries;
|
||||
}
|
||||
|
||||
// Check for ISPF DSLIST or general tabular format
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**")) continue;
|
||||
if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**") || trimmed.startsWith("***")) continue;
|
||||
if (trimmed.equalsIgnoreCase("READY") || trimmed.toUpperCase().startsWith("READY;")) continue;
|
||||
if (trimmed.toUpperCase().startsWith("COMMAND") || trimmed.toUpperCase().startsWith("DSLIST") || trimmed.toUpperCase().startsWith("DATA SETS")) continue;
|
||||
|
||||
TSODirectoryEntry entry = parseTabularLine(trimmed);
|
||||
@@ -51,38 +63,196 @@ public class TSODirectoryParser {
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static String[] splitIntoLines(String text) {
|
||||
if (text.contains("\n") || text.contains("\r")) {
|
||||
return text.split("\r?\n");
|
||||
}
|
||||
if (text.length() >= 80) {
|
||||
int lineLen = (text.length() % 132 == 0 && text.length() % 80 != 0) ? 132 : 80;
|
||||
int numLines = (text.length() + lineLen - 1) / lineLen;
|
||||
String[] lines = new String[numLines];
|
||||
for (int i = 0; i < numLines; i++) {
|
||||
int start = i * lineLen;
|
||||
int end = Math.min(start + lineLen, text.length());
|
||||
lines[i] = text.substring(start, end);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
return new String[]{text};
|
||||
}
|
||||
|
||||
private static void parseListcat(String[] lines, List<TSODirectoryEntry> entries) {
|
||||
TSODirectoryEntry current = null;
|
||||
String currentSpaceType = null;
|
||||
for (String line : lines) {
|
||||
String upper = line.toUpperCase();
|
||||
Matcher nonvsamMat = NONVSAM_PATTERN.matcher(upper);
|
||||
if (nonvsamMat.find()) {
|
||||
if (current != null && current.getName() != null) {
|
||||
if (current.getTracksAllocated() > 0 && current.getExtents() == 0) {
|
||||
current.setExtents(1);
|
||||
}
|
||||
entries.add(current);
|
||||
}
|
||||
current = new TSODirectoryEntry(nonvsamMat.group(1));
|
||||
if (upper.contains("CLUSTER") || upper.contains("AIX") || upper.contains("PATH")) {
|
||||
current.setDsorg("VSAM");
|
||||
}
|
||||
currentSpaceType = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
Matcher volMat = VOLSER_PATTERN.matcher(upper);
|
||||
if (volMat.find()) current.setVolume(volMat.group(1));
|
||||
if (volMat.find() && current.getVolume() == null) current.setVolume(volMat.group(1));
|
||||
|
||||
Matcher recMat = RECFM_PATTERN.matcher(upper);
|
||||
if (recMat.find()) current.setRecfm(recMat.group(1));
|
||||
if (recMat.find()) {
|
||||
String raw = recMat.group(1).toUpperCase();
|
||||
if (raw.contains("VAR") && raw.contains("BLOCK")) current.setRecfm("VB");
|
||||
else if (raw.contains("FIX") && raw.contains("BLOCK")) current.setRecfm("FB");
|
||||
else if (raw.contains("VAR")) current.setRecfm("V");
|
||||
else if (raw.contains("FIX")) current.setRecfm("F");
|
||||
else if (raw.contains("UNDEF")) current.setRecfm("U");
|
||||
else current.setRecfm(raw);
|
||||
}
|
||||
|
||||
Matcher lreclMat = LRECL_PATTERN.matcher(upper);
|
||||
if (lreclMat.find()) {
|
||||
if (lreclMat.find() && current.getLrecl() == 0) {
|
||||
try { current.setLrecl(Integer.parseInt(lreclMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher blkMat = BLKSIZE_PATTERN.matcher(upper);
|
||||
if (blkMat.find()) {
|
||||
if (blkMat.find() && current.getBlksize() == 0) {
|
||||
try { current.setBlksize(Integer.parseInt(blkMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher dsorgMat = DSORG_PATTERN.matcher(upper);
|
||||
if (dsorgMat.find()) current.setDsorg(dsorgMat.group(1));
|
||||
if (dsorgMat.find() && current.getDsorg() == null) current.setDsorg(dsorgMat.group(1));
|
||||
|
||||
Matcher extMat = EXTENTS_PATTERN.matcher(upper);
|
||||
if (extMat.find() && current.getExtents() == 0) {
|
||||
try { current.setExtents(Integer.parseInt(extMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher typeMat = SPACE_TYPE_PATTERN.matcher(upper);
|
||||
if (typeMat.find()) {
|
||||
currentSpaceType = typeMat.group(1);
|
||||
}
|
||||
|
||||
Matcher trkMat = TRACKS_PATTERN.matcher(upper);
|
||||
if (trkMat.find() && current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(trkMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
} else {
|
||||
Matcher priMat = SPACE_PRI_PATTERN.matcher(upper);
|
||||
if (priMat.find() && current.getTracksAllocated() == 0) {
|
||||
try {
|
||||
int pri = Integer.parseInt(priMat.group(1));
|
||||
if (currentSpaceType != null && currentSpaceType.startsWith("CYL")) {
|
||||
pri = pri * 15;
|
||||
}
|
||||
current.setTracksAllocated(pri);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current != null && current.getName() != null) {
|
||||
if (current.getTracksAllocated() > 0 && current.getExtents() == 0) {
|
||||
current.setExtents(1);
|
||||
}
|
||||
entries.add(current);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseListds(String[] lines, List<TSODirectoryEntry> entries) {
|
||||
TSODirectoryEntry current = null;
|
||||
boolean inRecfm = false;
|
||||
boolean inVol = false;
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
String upper = trimmed.toUpperCase();
|
||||
|
||||
if (upper.startsWith("--RECFM-")) {
|
||||
inRecfm = true;
|
||||
inVol = false;
|
||||
continue;
|
||||
}
|
||||
if (upper.startsWith("--VOLUMES--")) {
|
||||
inVol = true;
|
||||
inRecfm = false;
|
||||
continue;
|
||||
}
|
||||
if (upper.startsWith("--ALLOCATION") || upper.startsWith("--TRACKS-")) {
|
||||
inRecfm = false;
|
||||
inVol = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inRecfm && current != null) {
|
||||
String[] toks = trimmed.split("\\s+");
|
||||
if (toks.length >= 1 && toks[0].matches("^[A-Z]+$")) current.setRecfm(toks[0]);
|
||||
if (toks.length >= 2 && toks[1].matches("^\\d+$")) {
|
||||
try { current.setLrecl(Integer.parseInt(toks[1])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (toks.length >= 3 && toks[2].matches("^\\d+$")) {
|
||||
try { current.setBlksize(Integer.parseInt(toks[2])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (toks.length >= 4 && toks[3].matches("^[A-Z]+$")) current.setDsorg(toks[3]);
|
||||
inRecfm = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inVol && current != null) {
|
||||
String[] toks = trimmed.split("\\s+");
|
||||
if (toks.length >= 1 && toks[0].matches("^[A-Z0-9*@#$&]{1,6}$")) {
|
||||
current.setVolume(toks[0]);
|
||||
}
|
||||
inVol = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
Matcher trkMat = TRACKS_PATTERN.matcher(upper);
|
||||
if (trkMat.find() && current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(trkMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
} else {
|
||||
Matcher priMat = SPACE_PRI_PATTERN.matcher(upper);
|
||||
if (priMat.find() && current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(priMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
Matcher extMat = EXTENTS_PATTERN.matcher(upper);
|
||||
if (extMat.find() && current.getExtents() == 0) {
|
||||
try { current.setExtents(Integer.parseInt(extMat.group(1))); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
// Check for tabular numbers row under --TRACKS-ALLOCATED-EXTENTS-
|
||||
String[] numToks = trimmed.split("\\s+");
|
||||
if (numToks.length >= 2 && numToks[0].matches("^\\d+$") && numToks[1].matches("^\\d+$")) {
|
||||
if (current.getTracksAllocated() == 0) {
|
||||
try { current.setTracksAllocated(Integer.parseInt(numToks[0])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (current.getExtents() == 0) {
|
||||
try { current.setExtents(Integer.parseInt(numToks[1])); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String[] toks = trimmed.split("\\s+");
|
||||
if (toks.length > 0) {
|
||||
String cleanTok = toks[0].replace("'", "").replace("\"", "");
|
||||
if (DSN_PATTERN.matcher(cleanTok.toUpperCase()).matches()) {
|
||||
if (current != null && current.getName() != null) {
|
||||
entries.add(current);
|
||||
}
|
||||
current = new TSODirectoryEntry(cleanTok.toUpperCase());
|
||||
inRecfm = false;
|
||||
inVol = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current != null && current.getName() != null) {
|
||||
@@ -108,6 +278,8 @@ public class TSODirectoryParser {
|
||||
|
||||
TSODirectoryEntry entry = new TSODirectoryEntry(tokens[dsnIdx].toUpperCase());
|
||||
boolean seenFormat = false;
|
||||
int numBeforeFormat = 0;
|
||||
int numAfterFormat = 0;
|
||||
|
||||
// Parse remaining tokens
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
@@ -117,7 +289,7 @@ public class TSODirectoryParser {
|
||||
if (tok.matches("^(PS|PO|PO-E|VSAM|DA|IS)$")) {
|
||||
entry.setDsorg(tok);
|
||||
seenFormat = true;
|
||||
} else if (tok.matches("^(F|FB|V|VB|U|VBS|FBS)$")) {
|
||||
} else if (tok.matches("^(F|FB|V|VB|U|VBS|FBS|FBA|VBA|FA|VA|FBM|VBM)$")) {
|
||||
entry.setRecfm(tok);
|
||||
seenFormat = true;
|
||||
} else if (tok.matches("^(3390|3380|TAPE|VIO)$")) {
|
||||
@@ -129,23 +301,29 @@ public class TSODirectoryParser {
|
||||
} else {
|
||||
entry.setReferencedDate(tok);
|
||||
}
|
||||
} else if (tok.matches("^[A-Z0-9]{6}$") && entry.getVolume() == null && !tok.matches("^\\d+$")) {
|
||||
} else if (tok.matches("^[A-Z0-9*@#$&]{1,6}$") && entry.getVolume() == null && !tok.matches("^\\d+$")
|
||||
&& !tok.matches("^(PAGE|TOTAL|TRACKS|TRKS|EXTENTS|VOLUME|DSORG|RECFM|DATASET|NAME|DSN)$")) {
|
||||
entry.setVolume(tok);
|
||||
} else if (tok.matches("^\\d+$")) {
|
||||
int val = Integer.parseInt(tok);
|
||||
if (!seenFormat) {
|
||||
if (entry.getTracksAllocated() == 0) {
|
||||
if (numBeforeFormat == 0) {
|
||||
entry.setTracksAllocated(val);
|
||||
} else if (entry.getPercentUsed() == 0 && val <= 100) {
|
||||
numBeforeFormat++;
|
||||
} else if (numBeforeFormat == 1 && val <= 100) {
|
||||
entry.setPercentUsed(val);
|
||||
} else if (entry.getExtents() == 0 && val <= 128) {
|
||||
numBeforeFormat++;
|
||||
} else if (numBeforeFormat <= 2 && val <= 128) {
|
||||
entry.setExtents(val);
|
||||
numBeforeFormat++;
|
||||
}
|
||||
} else {
|
||||
if (entry.getLrecl() == 0 && val <= 32760) {
|
||||
if (numAfterFormat == 0 && val <= 32760) {
|
||||
entry.setLrecl(val);
|
||||
} else if (entry.getBlksize() == 0 && val <= 32760) {
|
||||
numAfterFormat++;
|
||||
} else if (numAfterFormat == 1 && val <= 32760) {
|
||||
entry.setBlksize(val);
|
||||
numAfterFormat++;
|
||||
} else if (entry.getTracksAllocated() == 0) {
|
||||
entry.setTracksAllocated(val);
|
||||
}
|
||||
@@ -153,6 +331,10 @@ public class TSODirectoryParser {
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.getTracksAllocated() > 0 && entry.getExtents() == 0) {
|
||||
entry.setExtents(1);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ package haus.nightmare.lib3270j.graphics;
|
||||
* and clicking the visual menu emitted gy = 224 (missing the [187..200] menu bounding box).</li>
|
||||
* <li><b>Failure Mode 3</b>: Intermediate raster buffering (e.g. fixed 800x600 canvas stretched to gridW x gridH)
|
||||
* introduced rounding artifacts in bidirectional coordinate conversion (unmapX/unmapY).</li>
|
||||
* <li><b>Failure Mode 4</b>: Overlapping dropdown menus (FILE, DRAW, TRANSFORM) lingering simultaneously when
|
||||
* segment retention/clearing was decoupled from GDDM's segment lifecycle.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class GddmCoordinateTransform {
|
||||
|
||||
@@ -444,34 +444,11 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
/**
|
||||
* Total vertical presentation space units.
|
||||
*
|
||||
* <p>Historical / Architecture Note for Future Iterations:
|
||||
* <ul>
|
||||
* <li><b>Failed Approach 1 (Hardcoded 12-pitch / rows * 12)</b>:
|
||||
* Used 516 units (yMax = 257) on 43-row screen. Caused upper coordinate truncation;
|
||||
* menus at gy > 257 were clipped and mathematically impossible to pick.</li>
|
||||
* <li><b>Failed Approach 2 (Hardcoded 16-pitch / rows * 16 without viewport alignment)</b>:
|
||||
* Used 688 units (yMax = 343). Visual rendering was shifted downward by ~50 pixels relative
|
||||
* to GDDM's internal picture space viewport, causing clicks at Row 7 to emit gy = 226
|
||||
* which missed the GDDM menu hit box and caused terminal alarm beeps.</li>
|
||||
* <li><b>Failed Approach 3 (Overwriting Graphic Input SF bytes 16-19 with row/col)</b>:
|
||||
* Overwrote device correlation descriptor 0x00230023, causing GDDM Structured Field
|
||||
* parser to reject inbound correlation packets as malformed.</li>
|
||||
* <li><b>Failed Approach 4 (Misinterpreting Procedure Order 0x21 as Segment Redraw loop)</b>:
|
||||
* Order 0x21 is HODCurrentDefaults (Set Current Defaults), not an executive redraw loop.
|
||||
* Treating 0x21 as segment execution caused previous menu frames and stored segments
|
||||
* to be repeatedly repainted over active screens, leaving behind visual ghost artifacts.</li>
|
||||
* <li><b>Failed Approach 5 (Static yMax=343 scaling vs GDDM ADMDRAW Viewport bounds)</b>:
|
||||
* In ADMDRAW, GDDM defines the graphics presentation space between y = -169 (canvas bottom)
|
||||
* and y = +200 (top menu bar), with total height ~370 units. Mapping this into a static
|
||||
* yMax = 343 (688 total height) renders the top menu at Row 9.2 (middle of the screen) with
|
||||
* a large vertical void above it, and clicking the visual menu sends gy = 224 which misses
|
||||
* the [187..200] menu bounding box. Furthermore, multiple dropdown menus (FILE, DRAW, TRANSFORM)
|
||||
* overlapped simultaneously because segment retention and clearing was decoupled from GDDM's
|
||||
* actual viewport state.</li>
|
||||
* <li><b>Solution Path</b>: Use {@link GddmCoordinateTransform} for dynamic display and aspect
|
||||
* scaling, ensuring host Query Reply metrics (SDH/AH) and GDDM Picture Space Window
|
||||
* orders are synchronized with client-side mouse transformation.</li>
|
||||
* </ul>
|
||||
* <p>Calculated as {@code screenRows * charHeight} matching 3179G presentation
|
||||
* space metrics. Dynamic display and aspect scaling relative to host picture
|
||||
* space and viewport bounds are managed by {@link GddmCoordinateTransform}.
|
||||
*
|
||||
* @return Total presentation space height in units
|
||||
*/
|
||||
public int getTotalHeight() {
|
||||
int rows = screenRows > 0 ? screenRows : 24;
|
||||
|
||||
@@ -129,6 +129,9 @@ public class InputProcessor {
|
||||
lockStateListener.onLockStateChanged(locked);
|
||||
}
|
||||
}
|
||||
if (!locked) {
|
||||
this.lastAid = AID_NO;
|
||||
}
|
||||
}
|
||||
public boolean isInsertMode() { return insertMode; }
|
||||
public void setInsertMode(boolean insert) {
|
||||
@@ -292,9 +295,10 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode)
|
||||
// Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode, or DBCS)
|
||||
boolean isAplChar = false;
|
||||
int ebc = -1;
|
||||
int dbcs = -1;
|
||||
if (aplKeyboardMode) {
|
||||
int aplCode = getAplCodeForChar(ch);
|
||||
if (aplCode >= 0) {
|
||||
@@ -303,10 +307,62 @@ public class InputProcessor {
|
||||
isAplChar = true;
|
||||
}
|
||||
}
|
||||
if (ebc < 0) {
|
||||
ebc = translator.unicodeToEbcdic(ch);
|
||||
if (ebc < 0 && translator != null && translator.isDBCS()) {
|
||||
dbcs = translator.unicodeToDbcs(ch);
|
||||
}
|
||||
if (ebc < 0 && dbcs < 0) {
|
||||
ebc = (translator != null) ? translator.unicodeToEbcdic(ch) : (int) ch;
|
||||
}
|
||||
if (ebc < 0 && dbcs < 0) return;
|
||||
|
||||
if (dbcs >= 0) {
|
||||
// DBCS character typing
|
||||
if (insertMode) {
|
||||
if (!screen.insertChar(baddr, ch)) {
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
|
||||
}
|
||||
setKeyboardLocked(true);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
int nextPos = screen.incrementAddress(baddr);
|
||||
if (screen.isFormatted() && screen.getCell(nextPos).isFieldAttribute()) {
|
||||
// Cannot fit 2-cell DBCS at the last position of the field
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
|
||||
}
|
||||
setKeyboardLocked(true);
|
||||
return;
|
||||
}
|
||||
int b1 = (dbcs >> 8) & 0xFF;
|
||||
int b2 = dbcs & 0xFF;
|
||||
ExtendedAttribute ea1 = screen.getCell(baddr);
|
||||
ea1.fa = 0;
|
||||
ea1.ec = (byte) b1;
|
||||
ea1.ucs4 = ch;
|
||||
ea1.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea1.db = ExtendedAttribute.DB_LEFT;
|
||||
|
||||
ExtendedAttribute ea2 = screen.getCell(nextPos);
|
||||
ea2.fa = 0;
|
||||
ea2.ec = (byte) b2;
|
||||
ea2.ucs4 = ch;
|
||||
ea2.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea2.db = ExtendedAttribute.DB_RIGHT;
|
||||
|
||||
if (screen.isFormatted()) {
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
}
|
||||
screen.setCursorAddress(screen.incrementAddress(nextPos));
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return;
|
||||
}
|
||||
if (ebc < 0) return;
|
||||
|
||||
if (insertMode) {
|
||||
// Insert mode: shift characters right from cursor to end of field
|
||||
@@ -703,7 +759,7 @@ public class InputProcessor {
|
||||
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
|
||||
*/
|
||||
public byte[] buildReadModifiedInboundData() {
|
||||
return buildReadModifiedInboundData(lastAid != 0 ? lastAid : AID_ENTER, false);
|
||||
return buildReadModifiedInboundData(lastAid != 0 ? lastAid : AID_NO, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1476,38 +1532,12 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
public void deleteChar() {
|
||||
if (!screen.isFormatted()) return;
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
addr = ((addr % size) + size) % size;
|
||||
|
||||
// Cannot delete a field attribute
|
||||
if (screen.getCell(addr).isFieldAttribute()) return;
|
||||
|
||||
if (screen.isFormatted()) {
|
||||
byte faVal = screen.getFieldAttributeAt(addr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
// Shift characters left within the field
|
||||
int shiftAddr = addr;
|
||||
int count = 0;
|
||||
while (count < size) {
|
||||
int next = screen.incrementAddress(shiftAddr);
|
||||
if (screen.getCell(next).isFieldAttribute()) {
|
||||
screen.getCell(shiftAddr).ec = 0;
|
||||
screen.getCell(shiftAddr).ucs4 = 0;
|
||||
break;
|
||||
}
|
||||
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
|
||||
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
|
||||
shiftAddr = next;
|
||||
count++;
|
||||
}
|
||||
|
||||
int faAddr = screen.findFieldAttribute(addr);
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.deleteChar(addr);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
@@ -1525,6 +1555,9 @@ public class InputProcessor {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (screen.getCell(prev).db == ExtendedAttribute.DB_RIGHT) {
|
||||
prev = screen.decrementAddress(prev);
|
||||
}
|
||||
screen.setCursorAddress(prev);
|
||||
deleteChar();
|
||||
}
|
||||
@@ -1629,6 +1662,7 @@ public class InputProcessor {
|
||||
/** Reset (unlock keyboard, cancel insert mode, clear OIA error states). */
|
||||
public void reset() {
|
||||
setKeyboardLocked(false);
|
||||
lastAid = AID_NO;
|
||||
insertMode = false;
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
|
||||
@@ -97,6 +97,11 @@ public class ExtendedAttribute {
|
||||
/** Validation attribute. */
|
||||
public byte vl;
|
||||
|
||||
// Transparency attribute values (0x46 / XA_TRANSPARENCY)
|
||||
public static final byte TR_DEFAULT = 0x00;
|
||||
public static final byte TR_OR = (byte) 0xF0;
|
||||
public static final byte TR_OPAQUE = (byte) 0xFF;
|
||||
|
||||
/** Transparency attribute. */
|
||||
public byte tr;
|
||||
|
||||
@@ -105,6 +110,14 @@ public class ExtendedAttribute {
|
||||
*/
|
||||
public char ucs4;
|
||||
|
||||
public ExtendedAttribute() {}
|
||||
|
||||
public ExtendedAttribute(ExtendedAttribute other) {
|
||||
if (other != null) {
|
||||
copyFrom(other);
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear all attributes. */
|
||||
public void clear() {
|
||||
ec = 0;
|
||||
|
||||
@@ -197,48 +197,99 @@ public class ScreenBuffer {
|
||||
return buffer[addr];
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable snapshot container for tear-free, lock-free screen display.
|
||||
* Published atomically to UI thread / EDT via volatile reference release.
|
||||
*/
|
||||
public static final class DisplaySnapshot {
|
||||
public final ExtendedAttribute[] cells;
|
||||
public final int rows;
|
||||
public final int cols;
|
||||
public final int cursorAddress;
|
||||
public final long version;
|
||||
|
||||
public DisplaySnapshot(ExtendedAttribute[] cells, int rows, int cols, int cursorAddress) {
|
||||
this(cells, rows, cols, cursorAddress, 0L);
|
||||
}
|
||||
|
||||
public DisplaySnapshot(ExtendedAttribute[] cells, int rows, int cols, int cursorAddress, long version) {
|
||||
this.cells = cells;
|
||||
this.rows = rows;
|
||||
this.cols = cols;
|
||||
this.cursorAddress = cursorAddress;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public ExtendedAttribute getCell(int addr) {
|
||||
if (cells == null || addr < 0 || addr >= cells.length) {
|
||||
return null;
|
||||
}
|
||||
return cells[addr];
|
||||
}
|
||||
|
||||
public int getRows() { return rows; }
|
||||
public int getCols() { return cols; }
|
||||
public int getCursorAddress() { return cursorAddress; }
|
||||
public long getVersion() { return version; }
|
||||
}
|
||||
|
||||
private volatile DisplaySnapshot currentSnapshot;
|
||||
private ExtendedAttribute[] displaySnapshot;
|
||||
private int displayRows;
|
||||
private int displayCols;
|
||||
private int displayCursorAddress;
|
||||
private long snapshotVersion = 0;
|
||||
|
||||
/**
|
||||
* Atomically creates a snapshot of the current presentation buffer for tear-free rendering.
|
||||
* Takes ~2 microseconds and eliminates mutual thread contention with the UI thread.
|
||||
* Atomically creates an immutable snapshot of the current presentation buffer for tear-free rendering.
|
||||
* Published via a volatile release fence, eliminating mutual monitor contention with the UI paint loop.
|
||||
*/
|
||||
public synchronized void updateDisplaySnapshot() {
|
||||
int size = rows * cols;
|
||||
if (displaySnapshot == null || displaySnapshot.length < size) {
|
||||
displaySnapshot = new ExtendedAttribute[size];
|
||||
ExtendedAttribute[] snapshotCells = new ExtendedAttribute[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
displaySnapshot[i] = new ExtendedAttribute();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < size; i++) {
|
||||
displaySnapshot[i].copyFrom(buffer[i]);
|
||||
ExtendedAttribute ea = new ExtendedAttribute();
|
||||
ea.copyFrom(buffer[i]);
|
||||
snapshotCells[i] = ea;
|
||||
}
|
||||
this.displayRows = rows;
|
||||
this.displayCols = cols;
|
||||
this.displayCursorAddress = cursorAddress;
|
||||
this.snapshotVersion++;
|
||||
this.displaySnapshot = snapshotCells;
|
||||
this.currentSnapshot = new DisplaySnapshot(snapshotCells, rows, cols, cursorAddress, snapshotVersion);
|
||||
}
|
||||
|
||||
public synchronized ExtendedAttribute getDisplayCell(int addr) {
|
||||
if (displaySnapshot == null || addr < 0 || addr >= displayRows * displayCols) {
|
||||
public DisplaySnapshot getDisplaySnapshot() {
|
||||
return currentSnapshot;
|
||||
}
|
||||
|
||||
public long getSnapshotVersion() {
|
||||
DisplaySnapshot snap = this.currentSnapshot;
|
||||
return snap != null ? snap.version : snapshotVersion;
|
||||
}
|
||||
|
||||
public ExtendedAttribute getDisplayCell(int addr) {
|
||||
DisplaySnapshot snap = this.currentSnapshot;
|
||||
if (snap == null || addr < 0 || addr >= snap.cells.length) {
|
||||
return getCell(addr);
|
||||
}
|
||||
return displaySnapshot[addr];
|
||||
return snap.cells[addr];
|
||||
}
|
||||
|
||||
public synchronized int getDisplayRows() {
|
||||
return displayRows > 0 ? displayRows : rows;
|
||||
public int getDisplayRows() {
|
||||
DisplaySnapshot snap = this.currentSnapshot;
|
||||
return (snap != null && snap.rows > 0) ? snap.rows : (displayRows > 0 ? displayRows : rows);
|
||||
}
|
||||
|
||||
public synchronized int getDisplayCols() {
|
||||
return displayCols > 0 ? displayCols : cols;
|
||||
public int getDisplayCols() {
|
||||
DisplaySnapshot snap = this.currentSnapshot;
|
||||
return (snap != null && snap.cols > 0) ? snap.cols : (displayCols > 0 ? displayCols : cols);
|
||||
}
|
||||
|
||||
public synchronized int getDisplayCursorAddress() {
|
||||
return displayRows > 0 ? displayCursorAddress : cursorAddress;
|
||||
public int getDisplayCursorAddress() {
|
||||
DisplaySnapshot snap = this.currentSnapshot;
|
||||
return (snap != null && snap.rows > 0) ? snap.cursorAddress : (displayRows > 0 ? displayCursorAddress : cursorAddress);
|
||||
}
|
||||
|
||||
// ========== Dimension accessors ==========
|
||||
@@ -346,6 +397,10 @@ public class ScreenBuffer {
|
||||
oldAddr = this.cursorAddress;
|
||||
this.cursorAddress = addr;
|
||||
this.displayCursorAddress = addr;
|
||||
DisplaySnapshot snap = this.currentSnapshot;
|
||||
if (snap != null) {
|
||||
this.currentSnapshot = new DisplaySnapshot(snap.cells, snap.rows, snap.cols, addr, snap.version);
|
||||
}
|
||||
}
|
||||
if (oldAddr != addr) {
|
||||
notifyCursorMoved(oldAddr, addr);
|
||||
@@ -727,6 +782,14 @@ public class ScreenBuffer {
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = buffer[i];
|
||||
if (!ea.isFieldAttribute()) {
|
||||
if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT) {
|
||||
// Preserve decoded DBCS character
|
||||
continue;
|
||||
}
|
||||
if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
|
||||
ea.ucs4 = ' ';
|
||||
continue;
|
||||
}
|
||||
if (ea.ec == 0) {
|
||||
ea.ucs4 = 0;
|
||||
} else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE
|
||||
@@ -736,6 +799,9 @@ public class ScreenBuffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (translator != null && translator.isDBCS()) {
|
||||
processSOSI();
|
||||
}
|
||||
}
|
||||
|
||||
private char getAplGraphic(int ec) {
|
||||
@@ -1124,7 +1190,7 @@ public class ScreenBuffer {
|
||||
int count = 0;
|
||||
while (count < size) {
|
||||
int next = incrementAddress(shiftAddr);
|
||||
if (getCell(next).isFieldAttribute()) {
|
||||
if (getCell(next).isFieldAttribute() || count == size - 1) {
|
||||
getCell(shiftAddr).clear();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -556,17 +556,42 @@ public class TelnetConnection {
|
||||
running = false;
|
||||
stopKeepAlive();
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.flush();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.shutdownInput();
|
||||
try {
|
||||
if (!socket.isOutputShutdown()) {
|
||||
socket.shutdownOutput();
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
// Allow host a brief drain window to receive EOF / FIN-ACK
|
||||
if (readerThread != null && readerThread.isAlive() && Thread.currentThread() != readerThread) {
|
||||
try {
|
||||
readerThread.join(200);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
} else if (inputStream != null) {
|
||||
try {
|
||||
socket.setSoTimeout(100);
|
||||
byte[] b = new byte[256];
|
||||
while (inputStream.read(b) != -1) {}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.log(Level.FINE, "Error during disconnect", e);
|
||||
}
|
||||
} finally {
|
||||
socket = null;
|
||||
inputStream = null;
|
||||
outputStream = null;
|
||||
readerThread = null;
|
||||
}
|
||||
log.info("Disconnected");
|
||||
}
|
||||
|
||||
@@ -574,7 +599,7 @@ public class TelnetConnection {
|
||||
* Check if the socket is connected.
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return socket != null && socket.isConnected() && !socket.isClosed();
|
||||
return running && socket != null && socket.isConnected() && !socket.isClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -629,7 +654,13 @@ public class TelnetConnection {
|
||||
fsm.onError("Network loop error: " + t.getMessage());
|
||||
}
|
||||
} finally {
|
||||
running = false;
|
||||
stopKeepAlive();
|
||||
try {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ public class TelnetFSM {
|
||||
// 3270 input buffer (accumulated between telnet framing)
|
||||
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
|
||||
|
||||
// Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C)
|
||||
private final byte[] nvtSingleByteScratch = new byte[1];
|
||||
|
||||
// Sub-negotiation buffer
|
||||
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
|
||||
|
||||
@@ -324,7 +327,8 @@ public class TelnetFSM {
|
||||
boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
|
||||
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
|
||||
if (isPlainNvt) {
|
||||
nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1);
|
||||
nvtSingleByteScratch[0] = (byte) c;
|
||||
nvtProcessor.processNVTData(nvtSingleByteScratch, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1366,6 +1370,71 @@ public class TelnetFSM {
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send TN3270E UNBIND request to release LU session cleanly (RFC 2355 Section 9).
|
||||
* @param reason SNA UNBIND reason code (e.g. UNBIND_NORMAL = 0x01)
|
||||
*/
|
||||
public synchronized void sendUnbind(int reason) {
|
||||
if (!tn3270eNegotiated || !tn3270eBound) {
|
||||
return;
|
||||
}
|
||||
if (connection == null || !connection.isConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] unbindData = new byte[EH_SIZE + 1];
|
||||
unbindData[0] = (byte) DT_UNBIND;
|
||||
unbindData[1] = 0; // Request flags
|
||||
unbindData[2] = (byte) RSF_NO_RESPONSE; // Response flags
|
||||
unbindData[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
|
||||
unbindData[4] = (byte) (eXmitSeq & 0xFF);
|
||||
unbindData[5] = (byte) (reason & 0xFF);
|
||||
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
|
||||
log.info("Sending TN3270E UNBIND reason=" + unbindReasonName(reason));
|
||||
sendRecord(unbindData);
|
||||
tn3270eBound = false;
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Failed to send TN3270E UNBIND", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send graceful Telnet de-negotiation (UNBIND if bound, followed by WONT/DONT for active options).
|
||||
*/
|
||||
public synchronized void sendTelnetDisconnect() {
|
||||
if (connection == null || !connection.isConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (tn3270eNegotiated && tn3270eBound) {
|
||||
sendUnbind(UNBIND_NORMAL);
|
||||
}
|
||||
// De-negotiate TN3270E first if active
|
||||
if (myOpts[TELOPT_TN3270E]) {
|
||||
sendCommand(WONT, TELOPT_TN3270E);
|
||||
myOpts[TELOPT_TN3270E] = false;
|
||||
}
|
||||
if (hisOpts[TELOPT_TN3270E]) {
|
||||
sendCommand(DONT, TELOPT_TN3270E);
|
||||
hisOpts[TELOPT_TN3270E] = false;
|
||||
}
|
||||
// De-negotiate all other active options
|
||||
for (int opt = 0; opt < 256; opt++) {
|
||||
if (myOpts[opt]) {
|
||||
sendCommand(WONT, opt);
|
||||
myOpts[opt] = false;
|
||||
}
|
||||
if (hisOpts[opt]) {
|
||||
sendCommand(DONT, opt);
|
||||
hisOpts[opt] = false;
|
||||
}
|
||||
}
|
||||
log.fine("Sent Telnet option de-negotiation");
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Error sending Telnet de-negotiation", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Check if we should transition to 3270 mode ==========
|
||||
|
||||
private void checkIn3270() {
|
||||
@@ -1531,6 +1600,9 @@ public class TelnetFSM {
|
||||
|
||||
private void changeState(ConnectionState newState) {
|
||||
ConnectionState old = this.connectionState;
|
||||
if (old == newState) {
|
||||
return;
|
||||
}
|
||||
this.connectionState = newState;
|
||||
log.info("State: " + old + " -> " + newState);
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
|
||||
@@ -37,6 +37,7 @@ public class FileTransferFileObject implements Serializable {
|
||||
private int blksize = 0;
|
||||
private int primarySpace = 0;
|
||||
private int secondarySpace = 0;
|
||||
private int directoryBlocks = 0;
|
||||
private int avblock = 0;
|
||||
private String options = "";
|
||||
|
||||
@@ -201,6 +202,14 @@ public class FileTransferFileObject implements Serializable {
|
||||
this.secondarySpace = secondarySpace;
|
||||
}
|
||||
|
||||
public int getDirectoryBlocks() {
|
||||
return directoryBlocks;
|
||||
}
|
||||
|
||||
public void setDirectoryBlocks(int directoryBlocks) {
|
||||
this.directoryBlocks = Math.max(0, directoryBlocks);
|
||||
}
|
||||
|
||||
public int getAvblock() {
|
||||
return avblock;
|
||||
}
|
||||
@@ -241,6 +250,7 @@ public class FileTransferFileObject implements Serializable {
|
||||
config.setBlksize(blksize);
|
||||
config.setPrimarySpace(primarySpace);
|
||||
config.setSecondarySpace(secondarySpace);
|
||||
config.setDirectoryBlocks(directoryBlocks);
|
||||
config.setAvblock(avblock);
|
||||
if (options != null && !options.isEmpty()) {
|
||||
config.parseOptions(options);
|
||||
@@ -263,6 +273,7 @@ public class FileTransferFileObject implements Serializable {
|
||||
this.blksize = config.getBlksize();
|
||||
this.primarySpace = config.getPrimarySpace();
|
||||
this.secondarySpace = config.getSecondarySpace();
|
||||
this.directoryBlocks = config.getDirectoryBlocks();
|
||||
this.avblock = config.getAvblock();
|
||||
}
|
||||
|
||||
|
||||
@@ -603,10 +603,260 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_PROCESSING);
|
||||
Vector result = new Vector();
|
||||
|
||||
String dataToParse = hostQuery;
|
||||
if (hostQuery == null || (!hostQuery.contains("\n") && !hostQuery.contains("\r")
|
||||
&& !hostQuery.toUpperCase().contains("NONVSAM") && !hostQuery.toUpperCase().contains("IN-CAT")
|
||||
&& !hostQuery.toUpperCase().contains("FORMAT"))) {
|
||||
if (session != null && session.getPS() != null) {
|
||||
try {
|
||||
ECLPS ps = session.getPS();
|
||||
ScreenBuffer sb = ps.getScreenBuffer();
|
||||
StringBuilder sbAll = new StringBuilder();
|
||||
int maxPages = 500;
|
||||
|
||||
if (hostType == VM_CMS) {
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
String filter;
|
||||
if (hostQuery == null || hostQuery.trim().isEmpty()) {
|
||||
filter = "* * A";
|
||||
} else {
|
||||
String clean = hostQuery.replace("'", "").replace("\"", "").trim();
|
||||
if (clean.equals("* * *") || clean.equals("*")) {
|
||||
filter = "* * A";
|
||||
} else if (clean.contains(".")) {
|
||||
int dot = clean.indexOf('.');
|
||||
filter = clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
|
||||
} else {
|
||||
String[] parts = clean.split("\\s+");
|
||||
if (parts.length == 1) {
|
||||
filter = parts[0] + " * A";
|
||||
} else if (parts.length == 2) {
|
||||
filter = parts[0] + " " + parts[1] + " A";
|
||||
} else {
|
||||
filter = parts[0] + " " + parts[1] + " " + parts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
ps.SendKeys("LISTFILE " + filter + " ( nohead alloc date[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
long startWait = System.currentTimeMillis();
|
||||
long timeoutMs = 10000;
|
||||
String screen = "";
|
||||
boolean isMore = false;
|
||||
|
||||
while (System.currentTimeMillis() - startWait < timeoutMs) {
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(500);
|
||||
}
|
||||
int rows = ps.getRows();
|
||||
int cols = ps.getCols();
|
||||
screen = ps.copyString(0, 0, rows - 1, cols - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
|
||||
isMore = true;
|
||||
break;
|
||||
}
|
||||
if (upper.contains("READY;") || upper.contains("READY(")) {
|
||||
break;
|
||||
}
|
||||
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
sbAll.append(screen).append("\n");
|
||||
|
||||
if (isMore) {
|
||||
ps.SendKeys("[clear]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically clear the listing from the terminal presentation space
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
|
||||
} else {
|
||||
// TSO
|
||||
int rows = ps.getRows();
|
||||
int cols = ps.getCols();
|
||||
String initialScreen = ps.copyString(0, 0, rows - 1, cols - 1);
|
||||
boolean isFormatted = sb != null && sb.isFormatted();
|
||||
boolean hasCmdArrow = initialScreen.contains("===>");
|
||||
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
|
||||
|
||||
String filter = (hostQuery != null && !hostQuery.trim().isEmpty()) ? hostQuery.trim() : "";
|
||||
String tsoCmd = "TSO LISTCAT ALL";
|
||||
if (!filter.isEmpty()) {
|
||||
if (filter.startsWith("'") && filter.endsWith("'") && filter.length() > 2) {
|
||||
filter = filter.substring(1, filter.length() - 1);
|
||||
}
|
||||
int paren = filter.indexOf('(');
|
||||
if (paren > 0) filter = filter.substring(0, paren);
|
||||
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
String level = filter;
|
||||
if (level.endsWith(".*")) {
|
||||
level = level.substring(0, level.length() - 2);
|
||||
} else if (level.endsWith("*")) {
|
||||
level = level.substring(0, level.length() - 1);
|
||||
}
|
||||
if (level.isEmpty()) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isIspf) {
|
||||
int arrowPos = initialScreen.indexOf("===>");
|
||||
if (arrowPos >= 0 && sb != null) {
|
||||
int inputAddr = sb.findNextUnprotected(arrowPos);
|
||||
if (inputAddr > 0) {
|
||||
sb.setCursorAddress(inputAddr);
|
||||
sb.updateDisplaySnapshot();
|
||||
} else {
|
||||
ps.SendKeys("[home]");
|
||||
}
|
||||
} else {
|
||||
ps.SendKeys("[home]");
|
||||
}
|
||||
ps.SendKeys("[erase_eof]");
|
||||
ps.SendKeys(tsoCmd + "[enter]");
|
||||
} else {
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(3000);
|
||||
}
|
||||
ps.SendKeys(tsoCmd + "[enter]");
|
||||
}
|
||||
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
|
||||
// Wait for output to start appearing
|
||||
long startCmd = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(200);
|
||||
}
|
||||
int r = ps.getRows();
|
||||
int c = ps.getCols();
|
||||
String s = ps.copyString(0, 0, r - 1, c - 1);
|
||||
String u = s.toUpperCase();
|
||||
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|
||||
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|
||||
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|
||||
|| u.contains("NO ENTRY") || s.contains(" --- ")) {
|
||||
break;
|
||||
}
|
||||
try { Thread.sleep(80); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
String prevScreen = "";
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(300);
|
||||
}
|
||||
int r = ps.getRows();
|
||||
int c = ps.getCols();
|
||||
String screen = ps.copyString(0, 0, r - 1, c - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
prevScreen = screen;
|
||||
sbAll.append(screen).append("\n");
|
||||
|
||||
if (upper.contains("***")) {
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|
||||
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")) {
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(1000);
|
||||
}
|
||||
|
||||
int r2 = ps.getRows();
|
||||
int c2 = ps.getCols();
|
||||
String s2 = ps.copyString(0, 0, r2 - 1, c2 - 1);
|
||||
if (s2.toUpperCase().contains("***")) {
|
||||
prevScreen = s2;
|
||||
sbAll.append(s2).append("\n");
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
|
||||
sbAll.append(s2).append("\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal clean-up
|
||||
if (isIspf) {
|
||||
ps.SendKeys("[enter]");
|
||||
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(1500);
|
||||
}
|
||||
|
||||
int r = ps.getRows();
|
||||
int c = ps.getCols();
|
||||
String currentScr = ps.copyString(0, 0, r - 1, c - 1);
|
||||
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|
||||
|| currentScr.contains(" --- ") || (sb != null && !sb.isFormatted())) {
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ps.SendKeys("[clear]");
|
||||
if (session.getOIA() != null) {
|
||||
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
|
||||
session.getOIA().waitForInput(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataToParse = sbAll.toString();
|
||||
} catch (Exception ignored) {
|
||||
// Fall back to hostQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (hostType == VM_CMS) {
|
||||
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING);
|
||||
List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(hostQuery);
|
||||
List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(dataToParse);
|
||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||
String hName = entry.getFilename() + " " + entry.getFiletype();
|
||||
String lName = entry.getFilename() + "." + entry.getFiletype();
|
||||
@@ -622,7 +872,7 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
}
|
||||
} else {
|
||||
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING);
|
||||
List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(hostQuery);
|
||||
List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(dataToParse);
|
||||
for (TSODirectoryEntry entry : tsoEntries) {
|
||||
String name = entry.getName();
|
||||
if (hostFiles != null) hostFiles.addElement(name);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 30:
|
||||
* Library Distribution, Maven Artifacts & Release Tagging (v1.2.4)
|
||||
*/
|
||||
public class Phase30LibraryDistributionTest {
|
||||
|
||||
private File locateProjectRoot() {
|
||||
File current = new File(".").getAbsoluteFile();
|
||||
while (current != null) {
|
||||
if (new File(current, "settings.gradle").exists() || new File(current, "build.gradle").exists()) {
|
||||
if (new File(current, "lib3270j").exists() && new File(current, "j3270").exists()) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
current = current.getParentFile();
|
||||
}
|
||||
return new File(".").getAbsoluteFile();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.1: lib3270j/build.gradle configures maven-publish, POM metadata, and artifacts")
|
||||
public void testMavenPublishConfigurationInBuildGradle() throws Exception {
|
||||
File projectRoot = locateProjectRoot();
|
||||
File libGradleFile = new File(projectRoot, "lib3270j/build.gradle");
|
||||
assertTrue(libGradleFile.exists(), "lib3270j/build.gradle must exist");
|
||||
|
||||
String content = Files.readString(libGradleFile.toPath());
|
||||
|
||||
// Plugin validation
|
||||
assertTrue(content.contains("id 'maven-publish'"), "maven-publish plugin must be applied");
|
||||
assertTrue(content.contains("withSourcesJar()"), "withSourcesJar() must be enabled");
|
||||
assertTrue(content.contains("withJavadocJar()"), "withJavadocJar() must be enabled");
|
||||
|
||||
// Publication coordinates
|
||||
assertTrue(content.contains("groupId = 'haus.nightmare.j3270'"), "groupId must be haus.nightmare.j3270");
|
||||
assertTrue(content.contains("artifactId = 'lib3270j'"), "artifactId must be lib3270j");
|
||||
|
||||
// POM metadata
|
||||
assertTrue(content.contains("name = 'lib3270j'"), "POM name must be lib3270j");
|
||||
assertTrue(content.contains("description = 'Pure Java IBM 3270 terminal emulation and protocol library with Host On-Demand (HOD) parity'"),
|
||||
"POM description must describe HOD parity");
|
||||
assertTrue(content.contains("The Unlicense"), "POM must declare The Unlicense");
|
||||
assertFalse(content.contains("Apache License"), "POM must not declare Apache License");
|
||||
assertTrue(content.contains("scm:git:git://github.com/rudi/j3270.git"), "POM must declare SCM connection");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.2: Standalone build scripts package lib3270j.jar with correct manifest metadata")
|
||||
public void testBuildScriptsStandaloneLibraryPackaging() throws Exception {
|
||||
File projectRoot = locateProjectRoot();
|
||||
|
||||
|
||||
// 2. setup.sh
|
||||
File setupSh = new File(projectRoot, "setup.sh");
|
||||
assertTrue(setupSh.exists(), "setup.sh must exist");
|
||||
String setupShContent = Files.readString(setupSh.toPath());
|
||||
assertTrue(setupShContent.contains("lib3270j.jar"), "setup.sh must package lib3270j.jar");
|
||||
assertTrue(setupShContent.contains("Implementation-Title: lib3270j"), "setup.sh must set Implementation-Title: lib3270j");
|
||||
assertTrue(setupShContent.contains("Implementation-Version: 1.2.4"), "setup.sh must set Implementation-Version: 1.2.4");
|
||||
|
||||
// 3. setup.ps1
|
||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
|
||||
String setupPs1Content = Files.readString(setupPs1.toPath());
|
||||
assertTrue(setupPs1Content.contains("lib3270j.jar"), "setup.ps1 must package lib3270j.jar");
|
||||
assertTrue(setupPs1Content.contains("Implementation-Title: lib3270j"), "setup.ps1 must set Implementation-Title: lib3270j");
|
||||
assertTrue(setupPs1Content.contains("Implementation-Version: 1.2.4"), "setup.ps1 must set Implementation-Version: 1.2.4");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.2: Core subsystem packages and classes are present and loadable")
|
||||
public void testCoreSubsystemsPresent() {
|
||||
// Telnet & Protocol
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.telnet.TelnetFSM"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.Telnet3270Client"));
|
||||
|
||||
// Screen Buffer & Concurrency
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.screen.ScreenBuffer"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.screen.ScreenBuffer$DisplaySnapshot"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.screen.ExtendedAttribute"));
|
||||
|
||||
// Charset & EBCDIC
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.charset.EbcdicTranslator"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.charset.CodePage"));
|
||||
|
||||
// GOCA & Graphics
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.graphics.GocaDecoder"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.graphics.GraphicsMode"));
|
||||
|
||||
// ECL (standard and HOD compatibility namespaces)
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.ecl.ECLSession"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.eNetwork.ECL.ECLSession"));
|
||||
assertDoesNotThrow(() -> Class.forName("com.ibm.eNetwork.ECL.ECLSession"));
|
||||
assertDoesNotThrow(() -> Class.forName("com.ibm.eNetwork.ECL.ECLPS"));
|
||||
assertDoesNotThrow(() -> Class.forName("com.ibm.eNetwork.beans.HOD.Macro"));
|
||||
|
||||
// FastByteBuffer de-synchronized accumulation
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.datastream.FastByteBuffer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.2: Maven local published artifacts exist and validate if maven-publish was executed")
|
||||
public void testMavenLocalPublishedArtifacts() throws Exception {
|
||||
String userHome = System.getProperty("user.home");
|
||||
File m2ArtifactDir = new File(userHome, ".m2/repository/haus/nightmare/j3270/lib3270j/1.2.4");
|
||||
if (m2ArtifactDir.exists()) {
|
||||
File pomFile = new File(m2ArtifactDir, "lib3270j-1.2.4.pom");
|
||||
File jarFile = new File(m2ArtifactDir, "lib3270j-1.2.4.jar");
|
||||
File sourcesFile = new File(m2ArtifactDir, "lib3270j-1.2.4-sources.jar");
|
||||
File javadocFile = new File(m2ArtifactDir, "lib3270j-1.2.4-javadoc.jar");
|
||||
|
||||
assertTrue(pomFile.exists() && pomFile.length() > 0, "POM file must exist and be non-empty");
|
||||
assertTrue(jarFile.exists() && jarFile.length() > 0, "JAR file must exist and be non-empty");
|
||||
assertTrue(sourcesFile.exists() && sourcesFile.length() > 0, "Sources JAR must exist and be non-empty");
|
||||
assertTrue(javadocFile.exists() && javadocFile.length() > 0, "Javadoc JAR must exist and be non-empty");
|
||||
|
||||
String pomXml = Files.readString(pomFile.toPath());
|
||||
assertTrue(pomXml.contains("<groupId>haus.nightmare.j3270</groupId>"), "POM must contain groupId");
|
||||
assertTrue(pomXml.contains("<artifactId>lib3270j</artifactId>"), "POM must contain artifactId");
|
||||
assertTrue(pomXml.contains("<version>1.2.4</version>"), "POM must contain version 1.2.4");
|
||||
assertTrue(pomXml.contains("The Unlicense"), "POM must contain Unlicense");
|
||||
assertFalse(pomXml.contains("Apache"), "lib3270j POM must not contain Apache license");
|
||||
assertFalse(pomXml.contains("<dependencies>"), "lib3270j POM must have zero external dependencies");
|
||||
}
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class DBCSDataStreamFullTest {
|
||||
|
||||
@Test
|
||||
public void testDBCSDataStreamShiftOutShiftIn() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("930");
|
||||
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
|
||||
|
||||
// Data stream:
|
||||
// EW, WCC, SBA (0, 0), SO (0x0E), 0x40 0x40 (\u3000 ideographic space), SI (0x0F)
|
||||
byte[] stream = new byte[] {
|
||||
(byte) CMD_EW,
|
||||
(byte) 0xC3,
|
||||
(byte) ORDER_SBA,
|
||||
(byte) 0x40, (byte) 0x40,
|
||||
(byte) 0x0E, // Shift-Out
|
||||
(byte) 0x40, (byte) 0x40, // DBCS ideographic space
|
||||
(byte) 0x0F // Shift-In
|
||||
};
|
||||
|
||||
processor.processRecord(stream, 0, stream.length, true);
|
||||
|
||||
ExtendedAttribute cell0 = screen.getCell(0);
|
||||
assertEquals(ExtendedAttribute.DB_SO, cell0.db, "Cell 0 must be DB_SO");
|
||||
|
||||
ExtendedAttribute cell1 = screen.getCell(1);
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, cell1.db, "Cell 1 must be DB_LEFT");
|
||||
assertEquals('\u3000', cell1.ucs4, "Cell 1 ucs4 must be \\u3000");
|
||||
|
||||
ExtendedAttribute cell2 = screen.getCell(2);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, cell2.db, "Cell 2 must be DB_RIGHT");
|
||||
assertEquals('\u3000', cell2.ucs4, "Cell 2 ucs4 must be \\u3000");
|
||||
|
||||
ExtendedAttribute cell3 = screen.getCell(3);
|
||||
assertEquals(ExtendedAttribute.DB_SI, cell3.db, "Cell 3 must be DB_SI");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDBCSRepeatToAddressOrder() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("930");
|
||||
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
|
||||
|
||||
// In DBCS mode (after SO), ORDER_RA consumes 2 data bytes and fills pairs
|
||||
byte[] stream = new byte[] {
|
||||
(byte) CMD_EW,
|
||||
(byte) 0xC3,
|
||||
(byte) ORDER_SBA,
|
||||
(byte) 0x40, (byte) 0x40,
|
||||
(byte) 0x0E, // SO at 0
|
||||
(byte) ORDER_RA,
|
||||
(byte) 0x40, (byte) 0x4B, // RA up to address 11 (covers cells 1..10)
|
||||
(byte) 0x40, (byte) 0x40, // 2-byte repeat char (\u3000)
|
||||
(byte) 0x0F // SI at 11
|
||||
};
|
||||
|
||||
processor.processRecord(stream, 0, stream.length, true);
|
||||
|
||||
assertEquals(ExtendedAttribute.DB_SO, screen.getCell(0).db);
|
||||
for (int i = 1; i < 11; i += 2) {
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, screen.getCell(i).db, "Cell " + i + " must be DB_LEFT");
|
||||
assertEquals('\u3000', screen.getCell(i).ucs4);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, screen.getCell(i + 1).db, "Cell " + (i + 1) + " must be DB_RIGHT");
|
||||
assertEquals('\u3000', screen.getCell(i + 1).ucs4);
|
||||
}
|
||||
assertEquals(ExtendedAttribute.DB_SI, screen.getCell(11).db);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLPlanesDBCSAndOutlining() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("930");
|
||||
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
|
||||
|
||||
byte[] stream = new byte[] {
|
||||
(byte) CMD_EW,
|
||||
(byte) 0xC3,
|
||||
(byte) ORDER_SBA,
|
||||
(byte) 0x40, (byte) 0x40,
|
||||
(byte) 0x0E, // SO at 0
|
||||
(byte) 0x40, (byte) 0x40, // DBCS at 1, 2
|
||||
(byte) 0x0F // SI at 3
|
||||
};
|
||||
processor.processRecord(stream, 0, stream.length, true);
|
||||
|
||||
// Add outlining to cell 1
|
||||
screen.getCell(1).ol = (byte) (ExtendedAttribute.OL_BOX);
|
||||
|
||||
ECLPS eclPS = new ECLPS(screen, null, translator);
|
||||
|
||||
char[] dbcsPlane = new char[4];
|
||||
int count = eclPS.getPlane(ECLConstants.DBCS_PLANE, dbcsPlane, 0, 4);
|
||||
assertEquals(4, count);
|
||||
assertEquals(0x20, dbcsPlane[0], "SO must be 0x20 in DBCS plane");
|
||||
assertEquals(0xC0, dbcsPlane[1], "DB_LEFT must be 0xC0 (DB_DBHIGH)");
|
||||
assertEquals(0x80, dbcsPlane[2], "DB_RIGHT must be 0x80 (DB_DBCS)");
|
||||
assertEquals(0x10, dbcsPlane[3], "SI must be 0x10 in DBCS plane");
|
||||
|
||||
char[] gridPlane = new char[4];
|
||||
eclPS.getPlane(ECLConstants.GRID_PLANE, gridPlane, 0, 4);
|
||||
assertEquals((char) (ExtendedAttribute.OL_BOX & 0xFF), gridPlane[1]);
|
||||
|
||||
// Helper methods
|
||||
assertTrue(eclPS.IsSOChar(0));
|
||||
assertFalse(eclPS.IsDBCSChar(0));
|
||||
|
||||
assertTrue(eclPS.IsDBCSChar(1));
|
||||
assertTrue(eclPS.IsDBCS1stChar(1));
|
||||
assertFalse(eclPS.IsDBCS2ndChar(1));
|
||||
|
||||
assertTrue(eclPS.IsDBCSChar(2));
|
||||
assertFalse(eclPS.IsDBCS1stChar(2));
|
||||
assertTrue(eclPS.IsDBCS2ndChar(2));
|
||||
|
||||
assertTrue(eclPS.IsSIChar(3));
|
||||
assertFalse(eclPS.IsDBCSChar(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLFieldDBCSMethods() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("930");
|
||||
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
|
||||
|
||||
// Field with SO + DBCS + SI
|
||||
byte[] stream = new byte[] {
|
||||
(byte) CMD_EW,
|
||||
(byte) 0xC3,
|
||||
(byte) ORDER_SBA,
|
||||
(byte) 0x40, (byte) 0x40,
|
||||
(byte) ORDER_SF,
|
||||
(byte) FA_PRINTABLE, // Field at 0
|
||||
(byte) 0x0E, // SO at 1
|
||||
(byte) 0x40, (byte) 0x40, // DBCS at 2, 3
|
||||
(byte) 0x0F // SI at 4
|
||||
};
|
||||
processor.processRecord(stream, 0, stream.length, true);
|
||||
|
||||
ECLPS eclPS = new ECLPS(screen, null, translator);
|
||||
ECLFieldList fieldList = new ECLFieldList(eclPS);
|
||||
fieldList.Refresh();
|
||||
assertEquals(1, fieldList.size());
|
||||
|
||||
ECLField field = fieldList.getField(0);
|
||||
assertTrue(field.isDBCS(), "Field should report isDBCS = true");
|
||||
assertTrue(field.isDBCSOnly(), "Field should report isDBCSOnly = true");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyBuilderDBCSCharsets() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("930");
|
||||
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
QueryReplyBuilder builder = new QueryReplyBuilder(screen);
|
||||
|
||||
byte[] charsets = builder.buildCharsets();
|
||||
assertNotNull(charsets);
|
||||
assertEquals(42, charsets.length, "DBCS Character Sets QR payload must be 42 bytes (matching HoD xxxqrcs)");
|
||||
|
||||
assertEquals((byte) 0x8E, charsets[0], "Flags 1 must be 0x8E");
|
||||
assertEquals((byte) 0x00, charsets[1], "Flags 2 must be 0x00");
|
||||
assertEquals(12, charsets[2], "SDW must be 12");
|
||||
assertEquals(24, charsets[3], "SDH must be 24");
|
||||
assertEquals(0x0B, charsets[8], "Descriptor length DL must be 11 (0x0B)");
|
||||
|
||||
// Descriptor 2 (DBCS): offset 9 + 11 = 20
|
||||
assertEquals(0x01, charsets[20], "Set ID must be 1");
|
||||
assertEquals((byte) 0x80, charsets[22], "Local ID must be 0x80 (DBCS)");
|
||||
assertEquals(24, charsets[23], "DBCS cell width must be double (24)");
|
||||
assertEquals(24, charsets[24], "DBCS cell height must be 24");
|
||||
assertEquals(0x41, charsets[25], "Section start must be 0x41");
|
||||
assertEquals(0x7F, charsets[26], "Section end for 930 must be 0x7F");
|
||||
|
||||
// DB CGCSGID = 370 (0x0172), DB CPGID = 300 (0x012C)
|
||||
int cgcsgid = ((charsets[27] & 0xFF) << 8) | (charsets[28] & 0xFF);
|
||||
int cpgid = ((charsets[29] & 0xFF) << 8) | (charsets[30] & 0xFF);
|
||||
assertEquals(370, cgcsgid);
|
||||
assertEquals(300, cpgid);
|
||||
|
||||
// Summary Query Reply contains Outlining (0x8C) and DBCS Asia (0x91)
|
||||
byte[] summary = builder.buildSummary();
|
||||
boolean hasOutlining = false;
|
||||
boolean hasDbcsAsia = false;
|
||||
for (byte b : summary) {
|
||||
if ((b & 0xFF) == QR_OUTLINING) hasOutlining = true;
|
||||
if ((b & 0xFF) == QR_DBCS_ASIA) hasDbcsAsia = true;
|
||||
}
|
||||
assertTrue(hasOutlining, "Summary must include QR_OUTLINING");
|
||||
assertTrue(hasDbcsAsia, "Summary must include QR_DBCS_ASIA");
|
||||
}
|
||||
}
|
||||
+152
@@ -271,4 +271,156 @@ public class DataStreamProcessorTest {
|
||||
assertEquals('8', screen.getCell(12).ucs4);
|
||||
assertEquals('9', screen.getCell(13).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPartitionReadModifiedReturnsAidQreply() {
|
||||
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
||||
processor.setOutputSender(out::writeBytes);
|
||||
|
||||
// Send WSF Read Partition with SNA_CMD_RM (0xF6)
|
||||
byte[] sfRm = new byte[] {
|
||||
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RM
|
||||
};
|
||||
out.reset();
|
||||
processor.processRecord(sfRm, 0, sfRm.length, true);
|
||||
|
||||
byte[] sent = out.toByteArray();
|
||||
assertTrue(sent.length >= 3, "Inbound data stream must contain at least AID and cursor address");
|
||||
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified AID must be AID_QREPLY (0x61)");
|
||||
|
||||
// Send WSF Read Partition with CMD_RM (0x06)
|
||||
byte[] sfRmNonSna = new byte[] {
|
||||
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) CMD_RM
|
||||
};
|
||||
out.reset();
|
||||
processor.processRecord(sfRmNonSna, 0, sfRmNonSna.length, true);
|
||||
|
||||
sent = out.toByteArray();
|
||||
assertTrue(sent.length >= 3);
|
||||
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified (non-SNA) AID must be AID_QREPLY (0x61)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPartitionReadModifiedAllReturnsAidQreply() {
|
||||
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
||||
processor.setOutputSender(out::writeBytes);
|
||||
|
||||
// Send WSF Read Partition with SNA_CMD_RMA (0x6E)
|
||||
byte[] sfRma = new byte[] {
|
||||
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RMA
|
||||
};
|
||||
out.reset();
|
||||
processor.processRecord(sfRma, 0, sfRma.length, true);
|
||||
|
||||
byte[] sent = out.toByteArray();
|
||||
assertTrue(sent.length >= 3);
|
||||
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified All AID must be AID_QREPLY (0x61)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPartitionReadBufferReturnsAidQreply() {
|
||||
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
||||
processor.setOutputSender(out::writeBytes);
|
||||
|
||||
// Send WSF Read Partition with SNA_CMD_RB (0xF2)
|
||||
byte[] sfRb = new byte[] {
|
||||
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RB
|
||||
};
|
||||
out.reset();
|
||||
processor.processRecord(sfRb, 0, sfRb.length, true);
|
||||
|
||||
byte[] sent = out.toByteArray();
|
||||
assertTrue(sent.length >= 3);
|
||||
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Buffer AID must be AID_QREPLY (0x61)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadModifiedAfterQueryRespondsWithQueryReply() {
|
||||
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
||||
processor.setOutputSender(out::writeBytes);
|
||||
|
||||
// 1. Send WSF Read Partition Query
|
||||
byte[] sfQuery = new byte[] {
|
||||
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, (byte) 0xFF, (byte) SF_RP_QUERY
|
||||
};
|
||||
out.reset();
|
||||
processor.processRecord(sfQuery, 0, sfQuery.length, true);
|
||||
|
||||
assertTrue(processor.isQreplyPending(), "qreplyPending should be true after query");
|
||||
|
||||
// 2. Mainframe issues channel Read Modified command (0x06) to poll the query reply
|
||||
byte[] rmCmd = new byte[] { (byte) CMD_RM };
|
||||
out.reset();
|
||||
processor.processRecord(rmCmd, 0, rmCmd.length, true);
|
||||
|
||||
byte[] sent = out.toByteArray();
|
||||
assertTrue(sent.length > 0, "Should have received query reply data");
|
||||
assertEquals((byte) AID_SF, sent[0], "Query reply header AID must be AID_SF (0x88)");
|
||||
assertFalse(processor.isQreplyPending(), "qreplyPending should be cleared after reply is sent");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStandaloneReadModifiedReturnsAidNo() {
|
||||
haus.nightmare.lib3270j.input.InputProcessor input = new haus.nightmare.lib3270j.input.InputProcessor(screen, translator, null);
|
||||
processor.setInputProcessor(input);
|
||||
|
||||
// Simulate operator previously pressing Enter
|
||||
input.setLastAid(AID_ENTER);
|
||||
input.setKeyboardLocked(true);
|
||||
|
||||
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
||||
processor.setOutputSender(out::writeBytes);
|
||||
|
||||
// Host responds with Erase/Write Alternate (0x7E) with WCC 0xC3 (kbdRestore=true)
|
||||
byte[] ewa = new byte[] { (byte) SNA_CMD_EWA, (byte) 0xC3 };
|
||||
processor.processRecord(ewa, 0, ewa.length, true);
|
||||
|
||||
assertFalse(input.isKeyboardLocked(), "Keyboard should be restored by WCC");
|
||||
assertEquals(AID_NO, input.getLastAid(), "lastAid should be reset to AID_NO on keyboard restore");
|
||||
|
||||
// Host subsequently polls terminal with standalone Read Modified (0xF6)
|
||||
byte[] rm = new byte[] { (byte) SNA_CMD_RM };
|
||||
out.reset();
|
||||
processor.processRecord(rm, 0, rm.length, true);
|
||||
|
||||
byte[] sent = out.toByteArray();
|
||||
assertTrue(sent.length >= 3, "Read Modified response must contain at least AID and 2-byte cursor address");
|
||||
assertEquals((byte) AID_NO, sent[0], "Standalone Read Modified from host must respond with AID_NO (0x60)");
|
||||
assertFalse(input.isKeyboardLocked(), "Keyboard must remain unlocked after host Read Modified poll");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStandaloneReadModifiedWithModifiedFieldSendsDataAndAidNo() {
|
||||
haus.nightmare.lib3270j.input.InputProcessor input = new haus.nightmare.lib3270j.input.InputProcessor(screen, translator, null);
|
||||
processor.setInputProcessor(input);
|
||||
|
||||
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
||||
processor.setOutputSender(out::writeBytes);
|
||||
|
||||
// Write a screen with an unprotected field at pos 10, WCC kbdRestore
|
||||
byte[] initStream = new byte[] {
|
||||
(byte) CMD_EW, (byte) 0xC3,
|
||||
(byte) ORDER_SBA, 0x00, 0x0A, // SBA 10
|
||||
(byte) ORDER_SF, (byte) FA_PRINTABLE, // unprotected
|
||||
(byte) 0xC1, (byte) 0xC2 // EBCDIC 'A', 'B'
|
||||
};
|
||||
processor.processRecord(initStream, 0, initStream.length, true);
|
||||
|
||||
// Modify the field by setting MDT (e.g. user typed into it)
|
||||
screen.getCell(10).fa = (byte) (screen.getCell(10).fa | FA_MODIFY);
|
||||
|
||||
// Host polls with 0xF6 (SNA_CMD_RM)
|
||||
out.reset();
|
||||
byte[] rm = new byte[] { (byte) SNA_CMD_RM };
|
||||
processor.processRecord(rm, 0, rm.length, true);
|
||||
|
||||
byte[] sent = out.toByteArray();
|
||||
assertTrue(sent.length > 3, "Should include SBA and field contents for modified field");
|
||||
assertEquals((byte) AID_NO, sent[0], "AID must be AID_NO (0x60)");
|
||||
assertEquals((byte) ORDER_SBA, sent[3], "Order must be SBA");
|
||||
assertEquals((byte) 0xC1, sent[6], "First char should be 'A'");
|
||||
assertEquals((byte) 0xC2, sent[7], "Second char should be 'B'");
|
||||
assertTrue((screen.getCell(10).fa & FA_MODIFY) != 0, "MDT must not be cleared by Read Modified command");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -49,9 +49,24 @@ public class QueryReplyBuilderTest {
|
||||
|
||||
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary
|
||||
int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
boolean hasB0 = false;
|
||||
boolean hasA8 = false;
|
||||
for (int i = 5; i < 1 + sumLen; i++) {
|
||||
assertNotEquals(QR_SEGMENT, replies[i] & 0xFF);
|
||||
if ((replies[i] & 0xFF) == QR_SEGMENT) hasB0 = true;
|
||||
if ((replies[i] & 0xFF) == QR_TRANSPARENCY) hasA8 = true;
|
||||
}
|
||||
assertFalse(hasB0);
|
||||
assertTrue(hasA8, "QR_TRANSPARENCY (0xA8) should be advertised even in GraphicsMode.NONE");
|
||||
|
||||
// And QR_TRANSPARENCY structured field payload must be present in replies
|
||||
boolean hasA8Payload = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_TRANSPARENCY) {
|
||||
hasA8Payload = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(hasA8Payload, "QR_TRANSPARENCY structured field should be included in buildAllQueryReplies");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -87,10 +87,10 @@ public class QueryReplyPhase2Test {
|
||||
assertNotNull(reply);
|
||||
|
||||
List<ParsedSF> sfs = parseQueryReplies(reply);
|
||||
// Base query reply must contain exactly 10 structured fields for SBCS:
|
||||
// Base query reply must contain exactly 11 structured fields for SBCS:
|
||||
// 0x80 (Summary), 0x81 (Usable Area), 0x84 (Alpha Partitions), 0x85 (Character Sets),
|
||||
// 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part)
|
||||
assertEquals(10, sfs.size());
|
||||
// 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part), 0xA8 (Transparency)
|
||||
assertEquals(11, sfs.size());
|
||||
|
||||
assertEquals(QR_SUMMARY, sfs.get(0).qcode);
|
||||
assertEquals(QR_USABLE_AREA, sfs.get(1).qcode);
|
||||
@@ -102,10 +102,10 @@ public class QueryReplyPhase2Test {
|
||||
assertEquals(QR_DDM, sfs.get(7).qcode);
|
||||
assertEquals(QR_AUXDA, sfs.get(8).qcode);
|
||||
assertEquals(QR_IMP_PART, sfs.get(9).qcode);
|
||||
assertEquals(QR_TRANSPARENCY, sfs.get(10).qcode);
|
||||
|
||||
// Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply
|
||||
// Verify that Vector Graphics SF payloads (0xB0..0xB6) are NOT sent in base reply
|
||||
for (ParsedSF sf : sfs) {
|
||||
assertNotEquals(QR_TRANSPARENCY, sf.qcode);
|
||||
assertNotEquals(QR_SEGMENT, sf.qcode);
|
||||
assertNotEquals(QR_PROCEDURE, sf.qcode);
|
||||
assertNotEquals(QR_LINETYPE, sf.qcode);
|
||||
@@ -246,7 +246,7 @@ public class QueryReplyPhase2Test {
|
||||
assertTrue(sent.length > 0);
|
||||
assertEquals((byte) AID_SF, sent[0]);
|
||||
List<ParsedSF> sfs = parseQueryReplies(sent);
|
||||
assertEquals(10, sfs.size());
|
||||
assertEquals(11, sfs.size());
|
||||
|
||||
// Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4
|
||||
byte[] queryList = new byte[] {
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TransparencyDataStreamTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private DataStreamProcessor processor;
|
||||
private ByteArrayOutputStream output;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
|
||||
processor = new DataStreamProcessor(screen, translator);
|
||||
output = new ByteArrayOutputStream();
|
||||
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetAttributeTransparency() {
|
||||
// Send SFE with XA_TRANSPARENCY = XAT_OPAQUE (0xFF)
|
||||
byte[] sfeRecord = new byte[] {
|
||||
(byte) CMD_EW, 0x00,
|
||||
(byte) ORDER_SFE, 0x02,
|
||||
(byte) XA_3270, (byte) FA_PRINTABLE,
|
||||
(byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE
|
||||
};
|
||||
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
|
||||
|
||||
ExtendedAttribute cell0 = screen.getCell(0);
|
||||
assertTrue(cell0.isFieldAttribute());
|
||||
assertEquals(ExtendedAttribute.TR_OPAQUE, cell0.tr);
|
||||
|
||||
// Send SA with XA_TRANSPARENCY = XAT_OPAQUE followed by character 'A' (0xC1)
|
||||
byte[] saRecord = new byte[] {
|
||||
(byte) CMD_W, 0x00,
|
||||
(byte) ORDER_SBA, 0x00, 0x01,
|
||||
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
|
||||
(byte) 0xC1 // 'A'
|
||||
};
|
||||
processor.processRecord(saRecord, 0, saRecord.length, true);
|
||||
|
||||
ExtendedAttribute charCell1 = screen.getCell(1);
|
||||
assertFalse(charCell1.isFieldAttribute());
|
||||
assertEquals((byte) 0xC1, charCell1.ec);
|
||||
assertEquals(ExtendedAttribute.TR_OPAQUE, charCell1.tr);
|
||||
|
||||
// Send SA with XA_ALL (0x00) followed by character 'B' (0xC2) -> should reset tr to 0
|
||||
byte[] resetRecord = new byte[] {
|
||||
(byte) CMD_W, 0x00,
|
||||
(byte) ORDER_SBA, 0x00, 0x02,
|
||||
(byte) ORDER_SA, (byte) XA_ALL, 0x00,
|
||||
(byte) 0xC2 // 'B'
|
||||
};
|
||||
processor.processRecord(resetRecord, 0, resetRecord.length, true);
|
||||
|
||||
ExtendedAttribute charCell2 = screen.getCell(2);
|
||||
assertFalse(charCell2.isFieldAttribute());
|
||||
assertEquals((byte) 0xC2, charCell2.ec);
|
||||
assertEquals(ExtendedAttribute.TR_DEFAULT, charCell2.tr);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatToAddressWithTransparency() {
|
||||
// Set SA Transparency = Opaque, then RA to address 5 with char 'C' (0xC3)
|
||||
byte[] raRecord = new byte[] {
|
||||
(byte) CMD_EW, 0x00,
|
||||
(byte) ORDER_SBA, 0x00, 0x00,
|
||||
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
|
||||
(byte) ORDER_RA, 0x00, 0x05, (byte) 0xC3
|
||||
};
|
||||
processor.processRecord(raRecord, 0, raRecord.length, true);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ExtendedAttribute cell = screen.getCell(i);
|
||||
assertEquals((byte) 0xC3, cell.ec);
|
||||
assertEquals(ExtendedAttribute.TR_OPAQUE, cell.tr, "Cell at " + i + " should have TR_OPAQUE");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBufferExtendedFieldIncludesTransparency() {
|
||||
screen.setReplyMode((byte) SF_SRM_XFIELD);
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
|
||||
|
||||
processor.processReadBuffer();
|
||||
|
||||
byte[] result = output.toByteArray();
|
||||
assertTrue(result.length > 3);
|
||||
assertEquals((byte) AID_NO, result[0]);
|
||||
|
||||
// Find ORDER_SFE
|
||||
boolean foundTr = false;
|
||||
for (int i = 3; i < result.length - 2; i++) {
|
||||
if ((result[i] & 0xFF) == ORDER_SFE) {
|
||||
int pairCount = result[i + 1] & 0xFF;
|
||||
int pos = i + 2;
|
||||
for (int p = 0; p < pairCount; p++) {
|
||||
int attrType = result[pos] & 0xFF;
|
||||
int attrVal = result[pos + 1] & 0xFF;
|
||||
if (attrType == XA_TRANSPARENCY && attrVal == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
|
||||
foundTr = true;
|
||||
}
|
||||
pos += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(foundTr, "Read Buffer in SF_SRM_XFIELD mode should report XA_TRANSPARENCY");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBufferCharacterModeIncludesTransparency() {
|
||||
screen.setReplyMode((byte) SF_SRM_CHAR);
|
||||
screen.getCell(0).ec = (byte) 0xC1;
|
||||
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
|
||||
|
||||
processor.processReadBuffer();
|
||||
|
||||
byte[] result = output.toByteArray();
|
||||
assertTrue(result.length > 3);
|
||||
|
||||
boolean foundSaTr = false;
|
||||
for (int i = 3; i < result.length - 2; i++) {
|
||||
if ((result[i] & 0xFF) == ORDER_SA && (result[i + 1] & 0xFF) == XA_TRANSPARENCY && (result[i + 2] & 0xFF) == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
|
||||
foundSaTr = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(foundSaTr, "Read Buffer in SF_SRM_CHAR mode should output ORDER_SA for XA_TRANSPARENCY");
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public class FTConfigTest {
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
|
||||
assertEquals("IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -25,7 +25,7 @@ public class FTConfigTest {
|
||||
config.setHostFilename("'USER.BIN.DATA'");
|
||||
config.setTransferMode(FTConfig.TransferMode.BINARY);
|
||||
|
||||
assertEquals("IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -40,7 +40,7 @@ public class FTConfigTest {
|
||||
config.setLrecl("80");
|
||||
config.setBlksize("3120");
|
||||
|
||||
assertEquals("IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
|
||||
assertEquals("TSO IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -125,4 +125,66 @@ public class FTConfigTest {
|
||||
String cmd = config.buildCommand();
|
||||
assertEquals("IND$FILE PUT FILE01 (BINARY NOCRLF REPLACE", cmd);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdsAllocationCommand() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.TSO);
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("'USER.PDS.DATA'");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
config.setRecfm("FB");
|
||||
config.setLrecl("80");
|
||||
config.setBlksize("3120");
|
||||
config.setPrimarySpace(15);
|
||||
config.setSecondarySpace(5);
|
||||
config.setDirectoryBlocks(10);
|
||||
config.setUnits(FTConfig.AllocationUnit.TRACKS);
|
||||
|
||||
String cmd = config.buildCommand();
|
||||
assertEquals("TSO IND$FILE PUT 'USER.PDS.DATA' ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(15,5,10) TRACKS", cmd);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdsParseOptionsThreeParameters() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("SYS1.PARMLIB");
|
||||
config.parseOptions("TSO ASCII CRLF SPACE(20,10,15) TRACKS");
|
||||
|
||||
assertEquals(20, config.getPrimarySpace());
|
||||
assertEquals(10, config.getSecondarySpace());
|
||||
assertEquals(15, config.getDirectoryBlocks());
|
||||
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
|
||||
assertEquals("TSO IND$FILE PUT SYS1.PARMLIB ASCII CRLF SPACE(20,10,15) TRACKS", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdsParseOptionsWithDirToken() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("SYS1.PROCLIB");
|
||||
config.parseOptions("TSO ASCII CRLF SPACE(10,5) DIR(20) CYLINDERS");
|
||||
|
||||
assertEquals(10, config.getPrimarySpace());
|
||||
assertEquals(5, config.getSecondarySpace());
|
||||
assertEquals(20, config.getDirectoryBlocks());
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, config.getUnits());
|
||||
assertEquals("TSO IND$FILE PUT SYS1.PROCLIB ASCII CRLF SPACE(10,5,20) CYLINDERS", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdsSetSpaceWithThreeArguments() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("MY.PDS");
|
||||
config.setSpace("12,4,8");
|
||||
|
||||
assertEquals(12, config.getPrimarySpace());
|
||||
assertEquals(4, config.getSecondarySpace());
|
||||
assertEquals(8, config.getDirectoryBlocks());
|
||||
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
|
||||
assertTrue(config.buildCommand().contains("SPACE(12,4,8) TRACKS"));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -443,7 +443,7 @@ public class FTProtocolHardeningPhase16Test {
|
||||
assertEquals("APVUFILE", optionsCfg.getProgramName());
|
||||
|
||||
String cmd = optionsCfg.buildCommand();
|
||||
assertTrue(cmd.startsWith("APVUFILE PUT 'TEST.FILE'"));
|
||||
assertTrue(cmd.startsWith("TSO APVUFILE PUT 'TEST.FILE'"));
|
||||
assertTrue(cmd.contains("NEW"));
|
||||
assertTrue(cmd.contains("NOSO"));
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ public class DirectoryParserTest {
|
||||
assertEquals(80, e0.getLrecl());
|
||||
assertEquals(3120, e0.getBlksize());
|
||||
assertEquals("PS", e0.getDsorg());
|
||||
assertEquals(15, e0.getTracksAllocated());
|
||||
assertEquals(1, e0.getExtents());
|
||||
|
||||
TSODirectoryEntry e1 = entries.get(1);
|
||||
assertEquals("TSOUSER.OTHER.DATA", e1.getName());
|
||||
@@ -121,4 +123,213 @@ public class DirectoryParserTest {
|
||||
assertEquals("GENPASS", entries.get(1).getFilename());
|
||||
assertEquals("REXX", entries.get(1).getFiletype());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContinuousScreenBufferParsingCms() {
|
||||
// Continuous 80-character rows without newlines (as produced by unformatted 3270 presentation space)
|
||||
String row1 = String.format("%-80s", "PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01");
|
||||
String row2 = String.format("%-80s", "TEST DATA A1 F 80 100 2 2026-08-01 09:15:00");
|
||||
String row3 = String.format("%-80s", "Ready; T=0.01/0.01 12:00:00");
|
||||
String continuousScreen = row1 + row2 + row3;
|
||||
|
||||
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(continuousScreen);
|
||||
assertEquals(2, entries.size(), "Should parse 2 CMS entries and ignore Ready;");
|
||||
assertEquals("PROFILE", entries.get(0).getFilename());
|
||||
assertEquals("TEST", entries.get(1).getFilename());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContinuousScreenBufferParsingTso() {
|
||||
// Continuous 80-character rows without newlines for ISPF DSLIST
|
||||
String row1 = String.format("%-80s", "Dslist - Data Sets Matching USER.*");
|
||||
String row2 = String.format("%-80s", "Command - Enter \"/\" to select action Tracks Dsorg Recfm Volume");
|
||||
String row3 = String.format("%-80s", " USER.TEST.CNTL 15 PO FB TSO001");
|
||||
String row4 = String.format("%-80s", " USER.TEST.DATA 10 PS FB TSO001");
|
||||
String row5 = String.format("%-80s", "***");
|
||||
String continuousScreen = row1 + row2 + row3 + row4 + row5;
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(continuousScreen);
|
||||
assertEquals(2, entries.size(), "Should parse 2 TSO entries and ignore ***");
|
||||
assertEquals("USER.TEST.CNTL", entries.get(0).getName());
|
||||
assertEquals("USER.TEST.DATA", entries.get(1).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCmsMoreRunningAndReadyHandling() {
|
||||
String cmsOutput =
|
||||
"PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01 MORE...\n" +
|
||||
"MORE...\n" +
|
||||
"RUNNING\n" +
|
||||
"HOLDING\n" +
|
||||
"TEST DATA A1 F 80 100 2 2026-08-01 09:15:00\n" +
|
||||
"OTHER FILE A1 V 80 50 1 2026-08-02 10:00:00 RUNNING\n" +
|
||||
"Ready(00000); T=0.01/0.01 12:00:00\n";
|
||||
|
||||
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(cmsOutput);
|
||||
assertEquals(3, entries.size(), "Should parse 3 CMS entries and strip/ignore MORE..., RUNNING, HOLDING, Ready(00000)");
|
||||
assertEquals("PROFILE", entries.get(0).getFilename());
|
||||
assertEquals("TEST", entries.get(1).getFilename());
|
||||
assertEquals("OTHER", entries.get(2).getFilename());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListcatWithClusterAndGdg() {
|
||||
String listcatOutput =
|
||||
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
|
||||
"NONVSAM ------- TSOUSER.NORMAL.DATA\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TSO001\n" +
|
||||
"CLUSTER ------- TSOUSER.VSAM.KSDS\n" +
|
||||
" IN-CAT --- CATALOG.USER\n" +
|
||||
"GDG ----------- TSOUSER.GDG.BASE\n" +
|
||||
"ALIAS --------- TSOUSER.MY.ALIAS\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
|
||||
assertEquals(4, entries.size(), "Should parse NONVSAM, CLUSTER, GDG, and ALIAS");
|
||||
assertEquals("TSOUSER.NORMAL.DATA", entries.get(0).getName());
|
||||
assertEquals("TSO001", entries.get(0).getVolume());
|
||||
assertEquals("TSOUSER.VSAM.KSDS", entries.get(1).getName());
|
||||
assertEquals("VSAM", entries.get(1).getDsorg());
|
||||
assertEquals("TSOUSER.GDG.BASE", entries.get(2).getName());
|
||||
assertEquals("TSOUSER.MY.ALIAS", entries.get(3).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListcatAllocDetails() {
|
||||
String allocOutput =
|
||||
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
|
||||
"NONVSAM ------- TSOUSER.PROJECT.DATA\n" +
|
||||
" IN-CAT --- CATALOG.USER\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TSOP01 DEVTYPE------X'3010200F'\n" +
|
||||
" ALLOCATION\n" +
|
||||
" SPACE-TYPE---------TRACK SPACE-PRI----------------15 SPACE-SEC----------------5\n" +
|
||||
" EXTENTS-----------------1\n" +
|
||||
"CLUSTER ------- TSOUSER.VSAM.TEST\n" +
|
||||
" IN-CAT --- CATALOG.USER\n" +
|
||||
" DATA ------- TSOUSER.VSAM.TEST.DATA\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TSOP02\n" +
|
||||
" ALLOCATION\n" +
|
||||
" SPACE-TYPE------CYLINDER SPACE-PRI----------------2\n" +
|
||||
" EXTENTS-----------------2\n" +
|
||||
" ATTRIBUTES\n" +
|
||||
" AVGLRECL--------------80 MAXLRECL----------------80 CISIZE----------------4096\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(allocOutput);
|
||||
assertEquals(2, entries.size());
|
||||
|
||||
TSODirectoryEntry nonvsam = entries.get(0);
|
||||
assertEquals("TSOUSER.PROJECT.DATA", nonvsam.getName());
|
||||
assertEquals("TSOP01", nonvsam.getVolume());
|
||||
assertEquals(15, nonvsam.getTracksAllocated());
|
||||
assertEquals(1, nonvsam.getExtents());
|
||||
|
||||
TSODirectoryEntry vsam = entries.get(1);
|
||||
assertEquals("TSOUSER.VSAM.TEST", vsam.getName());
|
||||
assertEquals("VSAM", vsam.getDsorg());
|
||||
assertEquals("TSOP02", vsam.getVolume());
|
||||
assertEquals(30, vsam.getTracksAllocated()); // 2 cylinders * 15 tracks
|
||||
assertEquals(2, vsam.getExtents());
|
||||
assertEquals(80, vsam.getLrecl());
|
||||
assertEquals(4096, vsam.getBlksize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListdsParsing() {
|
||||
String listdsOutput =
|
||||
"TSOUSER.MY.SOURCE\n" +
|
||||
"--RECFM-LRECL-BLKSIZE-DSORG\n" +
|
||||
" FB 80 3120 PO\n" +
|
||||
"--VOLUMES--\n" +
|
||||
" SRC001\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listdsOutput);
|
||||
assertEquals(1, entries.size());
|
||||
TSODirectoryEntry e = entries.get(0);
|
||||
assertEquals("TSOUSER.MY.SOURCE", e.getName());
|
||||
assertEquals("FB", e.getRecfm());
|
||||
assertEquals(80, e.getLrecl());
|
||||
assertEquals(3120, e.getBlksize());
|
||||
assertEquals("PO", e.getDsorg());
|
||||
assertEquals("SRC001", e.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListdsWithStatusAllocationParsing() {
|
||||
String listdsStatusOutput =
|
||||
"SYS1.ACMDLIB\n" +
|
||||
"--RECFM-LRECL-BLKSIZE-DSORG\n" +
|
||||
" U 0 32760 PO\n" +
|
||||
"--VOLUMES--\n" +
|
||||
" TK5DLB\n" +
|
||||
"--ALLOCATION---------------\n" +
|
||||
" SPACE-TYPE TRACK\n" +
|
||||
" SPACE-PRI 30\n" +
|
||||
" SPACE-SEC 5\n" +
|
||||
"--TRACKS-ALLOCATED-EXTENTS-\n" +
|
||||
" 30 1\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listdsStatusOutput);
|
||||
assertEquals(1, entries.size());
|
||||
TSODirectoryEntry e = entries.get(0);
|
||||
assertEquals("SYS1.ACMDLIB", e.getName());
|
||||
assertEquals("U", e.getRecfm());
|
||||
assertEquals(0, e.getLrecl());
|
||||
assertEquals(32760, e.getBlksize());
|
||||
assertEquals("PO", e.getDsorg());
|
||||
assertEquals("TK5DLB", e.getVolume());
|
||||
assertEquals(30, e.getTracksAllocated());
|
||||
assertEquals(1, e.getExtents());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoTabularDslispParsing() {
|
||||
String tabular = " USER.SAMPLE.COBOL MNT01 15 50 1 3390 PO FB 80 3120\n";
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(tabular);
|
||||
assertEquals(1, entries.size());
|
||||
TSODirectoryEntry e = entries.get(0);
|
||||
assertEquals("USER.SAMPLE.COBOL", e.getName());
|
||||
assertEquals("MNT01", e.getVolume());
|
||||
assertEquals(15, e.getTracksAllocated());
|
||||
assertEquals(50, e.getPercentUsed());
|
||||
assertEquals(1, e.getExtents());
|
||||
assertEquals("3390", e.getDevice());
|
||||
assertEquals("PO", e.getDsorg());
|
||||
assertEquals("FB", e.getRecfm());
|
||||
assertEquals(80, e.getLrecl());
|
||||
assertEquals(3120, e.getBlksize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoListcatWithSysresAsterisksVolume() {
|
||||
String listcatOutput =
|
||||
"IDCAMS SYSTEM SERVICES TIME: 02:32:00\n" +
|
||||
"NONVSAM ------- SYS1.ACMDLIB\n" +
|
||||
" IN-CAT --- SYS1.MCAT.TK5\n" +
|
||||
" HISTORY\n" +
|
||||
" OWNER-IDENT-------(NULL) CREATION----------23.146\n" +
|
||||
" RELEASE----------------2 EXPIRATION--------00.000\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------****** DEVTYPE------X'3010200F' FSEQN------------------0\n" +
|
||||
" ASSOCIATIONS--------(NULL)\n" +
|
||||
"NONVSAM ------- SYS1.SYSTEM.CMDSBSYS.CSCDATA\n" +
|
||||
" IN-CAT --- SYS1.MCAT.TK5\n" +
|
||||
" HISTORY\n" +
|
||||
" OWNER-IDENT-------(NULL) CREATION----------23.146\n" +
|
||||
" VOLUMES\n" +
|
||||
" VOLSER------------TK5002 DEVTYPE------X'3010200F' FSEQN------------------0\n" +
|
||||
" ASSOCIATIONS--------(NULL)\n";
|
||||
|
||||
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
|
||||
assertEquals(2, entries.size());
|
||||
|
||||
TSODirectoryEntry sysresEntry = entries.get(0);
|
||||
assertEquals("SYS1.ACMDLIB", sysresEntry.getName());
|
||||
assertEquals("******", sysresEntry.getVolume(), "SYSRES volume with asterisks should be preserved");
|
||||
|
||||
TSODirectoryEntry explicitEntry = entries.get(1);
|
||||
assertEquals("SYS1.SYSTEM.CMDSBSYS.CSCDATA", explicitEntry.getName());
|
||||
assertEquals("TK5002", explicitEntry.getVolume());
|
||||
}
|
||||
}
|
||||
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.FastByteBuffer;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 29:
|
||||
* Lock-Free Concurrency Architecture & Buffer De-Synchronization
|
||||
*/
|
||||
public class Phase29ConcurrencyStressTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.1: FastByteBuffer methods have synchronized modifier removed for thread-confined stream accumulation")
|
||||
public void testFastByteBufferMethodsAreUnsynchronized() throws Exception {
|
||||
Class<?> clazz = FastByteBuffer.class;
|
||||
String[] methodNames = {
|
||||
"write", "buffer", "size", "reset", "asByteBuffer", "slice", "toByteArray"
|
||||
};
|
||||
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
for (String name : methodNames) {
|
||||
if (method.getName().equals(name)) {
|
||||
int mod = method.getModifiers();
|
||||
assertFalse(Modifier.isSynchronized(mod),
|
||||
"FastByteBuffer method '" + method.getName() + "' must NOT have synchronized modifier");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Functional stream test
|
||||
FastByteBuffer fbb = new FastByteBuffer(128);
|
||||
for (int i = 0; i < 5000; i++) {
|
||||
fbb.write(i & 0xFF);
|
||||
}
|
||||
assertEquals(5000, fbb.size());
|
||||
assertEquals((byte) 0, fbb.buffer()[0]);
|
||||
assertEquals((byte) (4999 & 0xFF), fbb.buffer()[4999]);
|
||||
|
||||
ByteBuffer bb = fbb.asByteBuffer();
|
||||
assertEquals(5000, bb.remaining());
|
||||
ByteBuffer sl = fbb.slice(10, 20);
|
||||
assertEquals(20, sl.remaining());
|
||||
|
||||
fbb.reset();
|
||||
assertEquals(0, fbb.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.2: 10 worker threads reading getDisplayCell continuously while network thread writes 5,000 updates")
|
||||
public void testConcurrentDisplayCellReadsDuringHighFrequencyUpdates() throws Exception {
|
||||
ScreenBuffer sb = new ScreenBuffer(TerminalModel.IBM_3279_2, new EbcdicTranslator());
|
||||
int size = sb.getRows() * sb.getCols();
|
||||
|
||||
int workerThreadCount = 10;
|
||||
int totalUpdates = 5000;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(workerThreadCount + 1);
|
||||
AtomicBoolean running = new AtomicBoolean(true);
|
||||
AtomicInteger tornCellCount = new AtomicInteger(0);
|
||||
AtomicInteger exceptionCount = new AtomicInteger(0);
|
||||
AtomicLong totalReads = new AtomicLong(0);
|
||||
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
CountDownLatch writersDoneLatch = new CountDownLatch(1);
|
||||
|
||||
// Spin 10 worker threads reading getDisplayCell() continuously
|
||||
for (int w = 0; w < workerThreadCount; w++) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
while (running.get()) {
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
int curAddr = sb.getDisplayCursorAddress();
|
||||
long version = sb.getSnapshotVersion();
|
||||
|
||||
assertTrue(rows > 0, "Display rows must be positive");
|
||||
assertTrue(cols > 0, "Display cols must be positive");
|
||||
assertTrue(curAddr >= 0, "Display cursor must be non-negative");
|
||||
|
||||
int cellCount = rows * cols;
|
||||
for (int addr = 0; addr < cellCount; addr++) {
|
||||
ExtendedAttribute cell = sb.getDisplayCell(addr);
|
||||
if (cell == null) {
|
||||
exceptionCount.incrementAndGet();
|
||||
continue;
|
||||
}
|
||||
totalReads.incrementAndGet();
|
||||
|
||||
// Validate torn cell state:
|
||||
// In our patterned writer, every cell written in iteration N has:
|
||||
// ucs4 = (char) ('A' + (N % 26))
|
||||
// ec = (byte) (0x10 + (N % 26))
|
||||
// fg = (byte) (0x30 + (N % 26))
|
||||
// bg = (byte) (0x50 + (N % 26))
|
||||
// A torn read occurs if ucs4, ec, fg, or bg don't match the same iteration!
|
||||
if (cell.ucs4 >= 'A' && cell.ucs4 <= 'Z') {
|
||||
int ucsOffset = cell.ucs4 - 'A';
|
||||
int ecOffset = (cell.ec & 0xFF) - 0x10;
|
||||
int fgOffset = (cell.fg & 0xFF) - 0x30;
|
||||
int bgOffset = (cell.bg & 0xFF) - 0x50;
|
||||
|
||||
if (ucsOffset != ecOffset || ucsOffset != fgOffset || ucsOffset != bgOffset) {
|
||||
tornCellCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Writer task simulating network thread writing 5,000 screen updates
|
||||
Future<?> writerFuture = executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
for (int iter = 0; iter < totalUpdates; iter++) {
|
||||
int mod = iter % 26;
|
||||
char ch = (char) ('A' + mod);
|
||||
byte ec = (byte) (0x10 + mod);
|
||||
byte fg = (byte) (0x30 + mod);
|
||||
byte bg = (byte) (0x50 + mod);
|
||||
|
||||
// Mutate screen buffer
|
||||
ExtendedAttribute[] rawBuf = sb.getBuffer();
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute cell = rawBuf[i];
|
||||
cell.ucs4 = ch;
|
||||
cell.ec = ec;
|
||||
cell.fg = fg;
|
||||
cell.bg = bg;
|
||||
}
|
||||
sb.setCursorAddress(iter % size);
|
||||
sb.updateDisplaySnapshot();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
} finally {
|
||||
writersDoneLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger start
|
||||
startLatch.countDown();
|
||||
|
||||
// Await completion of 5,000 updates with timeout to verify 0 deadlocks
|
||||
boolean writerFinished = writersDoneLatch.await(15, TimeUnit.SECONDS);
|
||||
assertTrue(writerFinished, "Writer must complete 5,000 updates without deadlock");
|
||||
|
||||
// Stop workers
|
||||
running.set(false);
|
||||
executor.shutdown();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS), "Worker threads must terminate cleanly");
|
||||
|
||||
// Assert zero errors, zero torn reads, and high-volume lock-free reads verified
|
||||
assertEquals(0, exceptionCount.get(), "Zero exceptions (NPE/AIOOBE) must occur during concurrent execution");
|
||||
assertEquals(0, tornCellCount.get(), "Zero torn cell states must occur during atomic DisplaySnapshot swapping");
|
||||
assertTrue(totalReads.get() > 100_000, "Worker threads must execute high-throughput lock-free reads (actual: " + totalReads.get() + ")");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.2: Screen dimension resizing under concurrent reader load produces 0 exceptions")
|
||||
public void testConcurrentReadsDuringScreenResizing() throws Exception {
|
||||
ScreenBuffer sb = new ScreenBuffer(24, 80, 27, 132, new EbcdicTranslator());
|
||||
int workerCount = 8;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(workerCount + 1);
|
||||
AtomicBoolean running = new AtomicBoolean(true);
|
||||
AtomicInteger exceptionCount = new AtomicInteger(0);
|
||||
AtomicLong successfulReads = new AtomicLong(0);
|
||||
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
CountDownLatch writerLatch = new CountDownLatch(1);
|
||||
|
||||
for (int w = 0; w < workerCount; w++) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
while (running.get()) {
|
||||
ScreenBuffer.DisplaySnapshot snap = sb.getDisplaySnapshot();
|
||||
if (snap == null) {
|
||||
exceptionCount.incrementAndGet();
|
||||
continue;
|
||||
}
|
||||
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
int total = rows * cols;
|
||||
|
||||
for (int i = 0; i < total; i++) {
|
||||
ExtendedAttribute cell = sb.getDisplayCell(i);
|
||||
if (cell != null) {
|
||||
successfulReads.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Writer alternates dimensions between 24x80 (1920) and 27x132 (3564)
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
for (int iter = 0; iter < 1000; iter++) {
|
||||
sb.erase((iter % 2) != 0);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
} finally {
|
||||
writerLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
startLatch.countDown();
|
||||
assertTrue(writerLatch.await(10, TimeUnit.SECONDS), "Dimension resizing writer must complete without deadlock");
|
||||
|
||||
running.set(false);
|
||||
executor.shutdown();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
|
||||
assertEquals(0, exceptionCount.get(), "Screen resizing under reader load must not throw ArrayIndexOutOfBoundsException or NPE");
|
||||
assertTrue(successfulReads.get() > 50_000, "Concurrent readers must successfully read cells across dimension changes");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.2: setCursorAddress propagates cursor changes atomically to DisplaySnapshot")
|
||||
public void testCursorUpdatePropagatesToDisplaySnapshot() {
|
||||
ScreenBuffer sb = new ScreenBuffer(TerminalModel.IBM_3279_2, new EbcdicTranslator());
|
||||
sb.erase(false);
|
||||
|
||||
assertEquals(0, sb.getDisplayCursorAddress());
|
||||
|
||||
sb.setCursorAddress(125);
|
||||
assertEquals(125, sb.getDisplayCursorAddress());
|
||||
assertNotNull(sb.getDisplaySnapshot());
|
||||
assertEquals(125, sb.getDisplaySnapshot().cursorAddress);
|
||||
|
||||
sb.setCursorPosition(5, 10);
|
||||
int expected = 5 * sb.getCols() + 10;
|
||||
assertEquals(expected, sb.getDisplayCursorAddress());
|
||||
assertEquals(expected, sb.getDisplaySnapshot().cursorAddress);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.parallel.ResourceAccessMode;
|
||||
import org.junit.jupiter.api.parallel.ResourceLock;
|
||||
import org.junit.jupiter.api.parallel.Resources;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
|
||||
@@ -102,7 +105,7 @@ public class ProxyConnectionTest {
|
||||
connection.connect();
|
||||
assertTrue(connection.isConnected());
|
||||
|
||||
String req = receivedRequest.get(3, TimeUnit.SECONDS);
|
||||
String req = receivedRequest.get(5, TimeUnit.SECONDS);
|
||||
assertTrue(req.startsWith("CONNECT target.mainframe.org:23 HTTP/1.1"), "Expected CONNECT request");
|
||||
assertTrue(req.contains("Proxy-Authorization: Basic "), "Expected Basic auth in proxy request");
|
||||
|
||||
@@ -156,7 +159,7 @@ public class ProxyConnectionTest {
|
||||
|
||||
connection.connect();
|
||||
assertTrue(connection.isConnected());
|
||||
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
|
||||
assertTrue(handshakeDone.await(5, TimeUnit.SECONDS));
|
||||
|
||||
connection.disconnect();
|
||||
}
|
||||
@@ -236,13 +239,14 @@ public class ProxyConnectionTest {
|
||||
|
||||
connection.connect();
|
||||
assertTrue(connection.isConnected());
|
||||
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
|
||||
assertTrue(handshakeDone.await(5, TimeUnit.SECONDS));
|
||||
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE)
|
||||
public void testSystemPropertiesProxyFallback() throws Exception {
|
||||
try (ServerSocket proxyServer = new ServerSocket(0)) {
|
||||
int proxyPort = proxyServer.getLocalPort();
|
||||
@@ -259,6 +263,9 @@ public class ProxyConnectionTest {
|
||||
out.write("HTTP/1.1 200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
|
||||
out.flush();
|
||||
serverHandshakeDone.countDown();
|
||||
|
||||
// Keep socket open and drain data until client disconnects
|
||||
while (clientSock.getInputStream().read() != -1) {}
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.setDaemon(true);
|
||||
@@ -266,7 +273,11 @@ public class ProxyConnectionTest {
|
||||
|
||||
String origHost = System.getProperty("http.proxyHost");
|
||||
String origPort = System.getProperty("http.proxyPort");
|
||||
String origSocksHost = System.getProperty("socksProxyHost");
|
||||
String origSocksPort = System.getProperty("socksProxyPort");
|
||||
try {
|
||||
System.clearProperty("socksProxyHost");
|
||||
System.clearProperty("socksProxyPort");
|
||||
System.setProperty("http.proxyHost", "127.0.0.1");
|
||||
System.setProperty("http.proxyPort", String.valueOf(proxyPort));
|
||||
|
||||
@@ -278,13 +289,17 @@ public class ProxyConnectionTest {
|
||||
|
||||
connection.connect();
|
||||
assertTrue(connection.isConnected());
|
||||
assertTrue(serverHandshakeDone.await(3, TimeUnit.SECONDS));
|
||||
assertTrue(serverHandshakeDone.await(5, TimeUnit.SECONDS));
|
||||
connection.disconnect();
|
||||
} finally {
|
||||
if (origHost != null) System.setProperty("http.proxyHost", origHost);
|
||||
else System.clearProperty("http.proxyHost");
|
||||
if (origPort != null) System.setProperty("http.proxyPort", origPort);
|
||||
else System.clearProperty("http.proxyPort");
|
||||
if (origSocksHost != null) System.setProperty("socksProxyHost", origSocksHost);
|
||||
else System.clearProperty("socksProxyHost");
|
||||
if (origSocksPort != null) System.setProperty("socksProxyPort", origSocksPort);
|
||||
else System.clearProperty("socksProxyPort");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ public class Mock3270Server implements AutoCloseable {
|
||||
private int sequenceNumber = 0;
|
||||
|
||||
private final List<byte[]> receivedRecords = new CopyOnWriteArrayList<>();
|
||||
private final List<int[]> receivedCommands = new CopyOnWriteArrayList<>();
|
||||
private final AtomicInteger receivedNops = new AtomicInteger(0);
|
||||
private final AtomicInteger receivedTimingMarks = new AtomicInteger(0);
|
||||
|
||||
@@ -66,7 +67,9 @@ public class Mock3270Server implements AutoCloseable {
|
||||
}
|
||||
|
||||
public Mock3270Server(int requestedPort) throws IOException {
|
||||
this.serverSocket = new ServerSocket(requestedPort, 10, InetAddress.getByName("127.0.0.1"));
|
||||
this.serverSocket = new ServerSocket();
|
||||
this.serverSocket.setReuseAddress(true);
|
||||
this.serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), requestedPort), 10);
|
||||
this.port = serverSocket.getLocalPort();
|
||||
this.translator = new EbcdicTranslator();
|
||||
executor.submit(this::acceptLoop);
|
||||
@@ -126,6 +129,10 @@ public class Mock3270Server implements AutoCloseable {
|
||||
return Collections.unmodifiableList(receivedRecords);
|
||||
}
|
||||
|
||||
public List<int[]> getReceivedCommands() {
|
||||
return Collections.unmodifiableList(receivedCommands);
|
||||
}
|
||||
|
||||
public byte[] getLastReceivedRecord() {
|
||||
if (receivedRecords.isEmpty()) return null;
|
||||
return receivedRecords.get(receivedRecords.size() - 1);
|
||||
@@ -285,6 +292,7 @@ public class Mock3270Server implements AutoCloseable {
|
||||
|
||||
private void handleOptionCommand(int cmd, int opt) throws IOException {
|
||||
log.fine("Mock3270Server: RCVD IAC " + cmd + " opt=" + opt);
|
||||
receivedCommands.add(new int[] { cmd, opt });
|
||||
if (cmd == WILL && opt == TELOPT_TN3270E) {
|
||||
// Client accepted TN3270E -> Send SB TN3270E SEND DEVICE-TYPE IAC SE
|
||||
sendDeviceTypeSend();
|
||||
@@ -565,6 +573,11 @@ public class Mock3270Server implements AutoCloseable {
|
||||
} catch (Exception ignored) {}
|
||||
disconnectClient();
|
||||
executor.shutdownNow();
|
||||
try {
|
||||
executor.awaitTermination(200, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
log.info("Mock3270Server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
+68
-3
@@ -76,8 +76,8 @@ public class Mock3270ServerIntegrationTest {
|
||||
public void testSendScreenAndVerifyPresentationSpace() throws Exception {
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
|
||||
client = new Telnet3270Client(config);
|
||||
assertTrue(client.connect(8000));
|
||||
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
|
||||
assertTrue(client.connect(8000), "Client should connect to mock server");
|
||||
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS), "Server handshake should complete within 5s");
|
||||
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
|
||||
|
||||
// Server sends formatted greeting screen at row 2, col 10
|
||||
@@ -243,7 +243,10 @@ public class Mock3270ServerIntegrationTest {
|
||||
// Disconnect active client connection abruptly
|
||||
server.disconnectClient();
|
||||
|
||||
// Wait for client to detect disconnect and reconnect automatically
|
||||
// Wait for client to detect disconnect and enter RECONNECTING
|
||||
waitForState(client, ConnectionState.RECONNECTING, 5000);
|
||||
|
||||
// Wait for client to reconnect automatically
|
||||
waitForState(client, ConnectionState.CONNECTED_TN3270E, 8000);
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, client.getConnectionState());
|
||||
}
|
||||
@@ -290,4 +293,66 @@ public class Mock3270ServerIntegrationTest {
|
||||
|
||||
assertTrue(matched, "Fragmented stream must be reassembled and displayed correctly");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGracefulDisconnectSendsUnbindAndDeNegotiation() throws Exception {
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
|
||||
client = new Telnet3270Client(config);
|
||||
assertTrue(client.connect(8000));
|
||||
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
|
||||
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
|
||||
assertTrue(client.getTelnetFSM().isTn3270eBound());
|
||||
|
||||
// Perform graceful disconnect
|
||||
client.disconnect();
|
||||
|
||||
// Verify client transitioned to NOT_CONNECTED
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
assertFalse(client.isConnected());
|
||||
|
||||
// Wait briefly for server to process queued records
|
||||
Thread.sleep(150);
|
||||
|
||||
// Verify server received TN3270E UNBIND
|
||||
boolean receivedUnbind = false;
|
||||
for (byte[] rec : server.getReceivedRecords()) {
|
||||
if (rec != null && rec.length > 5 && (rec[0] & 0xFF) == DT_UNBIND) {
|
||||
int reason = rec[5] & 0xFF;
|
||||
assertEquals(UNBIND_NORMAL, reason, "UNBIND reason should be UNBIND_NORMAL (0x01)");
|
||||
receivedUnbind = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(receivedUnbind, "Server should have received TN3270E UNBIND record");
|
||||
|
||||
// Verify server received Telnet option de-negotiation (WONT TN3270E or DONT TN3270E)
|
||||
boolean receivedWontOrDontTn3270e = false;
|
||||
for (int[] cmd : server.getReceivedCommands()) {
|
||||
if ((cmd[0] == WONT || cmd[0] == DONT) && cmd[1] == TELOPT_TN3270E) {
|
||||
receivedWontOrDontTn3270e = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(receivedWontOrDontTn3270e, "Server should have received Telnet de-negotiation for TN3270E");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerSideDisconnectTransitionsClientToNotConnectedAndClosesSocket() throws Exception {
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
|
||||
client = new Telnet3270Client(config);
|
||||
assertTrue(client.connect(8000));
|
||||
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
|
||||
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
|
||||
assertTrue(client.isConnected());
|
||||
|
||||
// Server drops connection (host disconnect)
|
||||
server.disconnectClient();
|
||||
|
||||
// Wait for client to detect disconnect
|
||||
waitForState(client, ConnectionState.NOT_CONNECTED, 5000);
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
assertFalse(client.isConnected(), "Client must report isConnected() == false after host disconnect");
|
||||
assertNotNull(client.getConnection());
|
||||
assertFalse(client.getConnection().isConnected(), "TelnetConnection must report isConnected() == false after host disconnect");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Launch j3270 - Java TN3270 Terminal Emulator
|
||||
# Usage: ./run.sh [host] [port] [model]
|
||||
# Example: ./run.sh mainframe.example.com 23 4
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
|
||||
# Build if jar is missing or rebuild requested
|
||||
if [ ! -f "$BUILD_DIR/j3270.jar" ]; then
|
||||
"$SCRIPT_DIR/build_all.sh"
|
||||
fi
|
||||
|
||||
exec java -Dapple.laf.useScreenMenuBar=true \
|
||||
-Dapple.awt.application.name=j3270 \
|
||||
-jar "$BUILD_DIR/j3270.jar" "$@"
|
||||
@@ -1,31 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
j3270 Terminal Emulator - Setup and Build Script (PowerShell Entrypoint)
|
||||
Forwards execution directly to setup.ps1 with all supplied parameters.
|
||||
|
||||
.DESCRIPTION
|
||||
See setup.ps1 for full implementation, parameter definitions, and documentation.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[switch]$Run,
|
||||
[switch]$Test,
|
||||
[switch]$Clean,
|
||||
[switch]$Help,
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$RunArgs
|
||||
)
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
if (-not $scriptDir) {
|
||||
$scriptDir = Get-Location
|
||||
}
|
||||
|
||||
$ps1Script = Join-Path $scriptDir "setup.ps1"
|
||||
if (Test-Path $ps1Script) {
|
||||
& $ps1Script @PSBoundParameters @RunArgs
|
||||
} else {
|
||||
Write-Error "setup.ps1 not found in $scriptDir"
|
||||
exit 1
|
||||
}
|
||||
@@ -413,11 +413,19 @@ if (Test-Path $libResDir) {
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 7. Package Standalone Executable JAR
|
||||
# 7. Package Standalone JARs
|
||||
# ------------------------------------------------------------------------------
|
||||
Write-LogInfo "Packaging standalone lib3270j library JAR..."
|
||||
$libManifestFile = Join-Path $BuildDir "LIB_MANIFEST.MF"
|
||||
$libManifestText = "Manifest-Version: 1.0`r`nImplementation-Title: lib3270j`r`nImplementation-Version: 1.2.4`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
||||
[System.IO.File]::WriteAllText($libManifestFile, $libManifestText)
|
||||
|
||||
$libJarFile = Join-Path $BuildDir "lib3270j.jar"
|
||||
& $detectedJdk.Jar cvfm $libJarFile $libManifestFile -C $LibBuildDir . | Out-Null
|
||||
|
||||
Write-LogInfo "Packaging standalone executable JAR..."
|
||||
$manifestFile = Join-Path $BuildDir "MANIFEST.MF"
|
||||
$manifestText = "Manifest-Version: 1.0`r`nMain-Class: haus.nightmare.j3270.J3270App`r`nImplementation-Title: j3270`r`nImplementation-Version: 0.1.0`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
||||
$manifestText = "Manifest-Version: 1.0`r`nMain-Class: haus.nightmare.j3270.J3270App`r`nImplementation-Title: j3270`r`nImplementation-Version: 1.2.4`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
||||
[System.IO.File]::WriteAllText($manifestFile, $manifestText)
|
||||
|
||||
$jarFile = Join-Path $BuildDir "j3270.jar"
|
||||
@@ -428,10 +436,13 @@ if (-not (Test-Path $jarFile)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
$libJarItem = Get-Item $libJarFile
|
||||
$libJarSizeKB = [math]::Round($libJarItem.Length / 1KB, 1)
|
||||
$jarItem = Get-Item $jarFile
|
||||
$jarSizeKB = [math]::Round($jarItem.Length / 1KB, 1)
|
||||
|
||||
Write-Host "`n=== Build Successful ===`n" -ForegroundColor Green
|
||||
Write-LogSuccess "Library JAR created: $libJarFile (${libJarSizeKB} KB)"
|
||||
Write-LogSuccess "Executable JAR created: $jarFile (${jarSizeKB} KB)"
|
||||
|
||||
Write-Host @"
|
||||
|
||||
@@ -369,15 +369,28 @@ if [ -d "$SCRIPT_DIR/lib3270j/src/main/resources" ]; then
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 5. Package Standalone Executable JAR
|
||||
# 5. Package Standalone JARs
|
||||
# ------------------------------------------------------------------------------
|
||||
log_info "Packaging standalone lib3270j library JAR..."
|
||||
|
||||
cat << 'EOF' > "$BUILD_DIR/LIB_MANIFEST.MF"
|
||||
Manifest-Version: 1.0
|
||||
Implementation-Title: lib3270j
|
||||
Implementation-Version: 1.2.4
|
||||
Created-By: j3270 setup.sh
|
||||
|
||||
EOF
|
||||
|
||||
"$JAR_BIN" cvfm "$BUILD_DIR/lib3270j.jar" "$BUILD_DIR/LIB_MANIFEST.MF" \
|
||||
-C "$BUILD_DIR/lib3270j" . > /dev/null
|
||||
|
||||
log_info "Packaging standalone executable JAR..."
|
||||
|
||||
cat << 'EOF' > "$BUILD_DIR/MANIFEST.MF"
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: haus.nightmare.j3270.J3270App
|
||||
Implementation-Title: j3270
|
||||
Implementation-Version: 0.1.0
|
||||
Implementation-Version: 1.2.4
|
||||
Created-By: j3270 setup.sh
|
||||
|
||||
EOF
|
||||
@@ -391,14 +404,16 @@ if [ ! -f "$BUILD_DIR/j3270.jar" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_SIZE=$(ls -lh "$BUILD_DIR/j3270.jar" | awk '{print $5}')
|
||||
LIB_JAR_SIZE=$(du -h "$BUILD_DIR/lib3270j.jar" | awk '{print $1}')
|
||||
JAR_SIZE=$(du -h "$BUILD_DIR/j3270.jar" | awk '{print $1}')
|
||||
printf "\n%b=== Build Successful ===%b\n" "$BOLD" "$RESET"
|
||||
log_success "Library JAR: $BUILD_DIR/lib3270j.jar ($LIB_JAR_SIZE)"
|
||||
log_success "Executable JAR: $BUILD_DIR/j3270.jar ($JAR_SIZE)"
|
||||
|
||||
printf "\n%bTo run j3270:%b\n" "$BOLD" "$RESET"
|
||||
printf " ./run.sh\n"
|
||||
printf " # Or directly:\n"
|
||||
printf " %s -jar \"%s/build/j3270.jar\"\n\n" "$JAVA_BIN" "$SCRIPT_DIR"
|
||||
printf " java -jar \"%s/build/j3270.jar\"\n" "$SCRIPT_DIR"
|
||||
printf " # Or launch via setup script:\n"
|
||||
printf " ./setup.sh --run\n\n"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 6. Run Tests (if requested)
|
||||
@@ -407,8 +422,6 @@ if [ "$DO_TEST" -eq 1 ]; then
|
||||
printf "%b=== Running Unit Tests ===%b\n" "$BOLD" "$RESET"
|
||||
if [ -x "$SCRIPT_DIR/test_all.sh" ]; then
|
||||
sh "$SCRIPT_DIR/test_all.sh"
|
||||
elif [ -x "$SCRIPT_DIR/build_all.sh" ]; then
|
||||
sh "$SCRIPT_DIR/build_all.sh" test
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ cd "$SCRIPT_DIR"
|
||||
echo "=== Running j3270 Test Suite ==="
|
||||
|
||||
# 1. First build the core libraries
|
||||
sh "$SCRIPT_DIR/build_all.sh"
|
||||
sh "$SCRIPT_DIR/setup.sh"
|
||||
|
||||
# 2. Locate Java toolchain
|
||||
JAVAC_BIN=""
|
||||
@@ -83,7 +83,7 @@ if [ -f "$JUNIT_JAR" ] && [ -s "$JUNIT_JAR" ]; then
|
||||
rm -f "$BUILD_DIR/test_sources.txt"
|
||||
|
||||
echo "Executing JUnit tests..."
|
||||
"$JAVA_BIN" -Djava.awt.headless=true -jar "$JUNIT_JAR" \
|
||||
"$JAVA_BIN" -Djava.awt.headless=true -jar "$JUNIT_JAR" execute \
|
||||
--class-path "$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$TEST_BUILD_DIR" \
|
||||
--scan-class-path \
|
||||
--reports-dir="$REPORTS_DIR"
|
||||
|
||||
Reference in New Issue
Block a user