Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
d7d2f8bad5
|
|||
|
f0fc1b6e70
|
|||
| 10a31476c2 |
@@ -1,3 +1,4 @@
|
|||||||
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.jar
|
*.jar
|
||||||
build
|
build
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ Requires **Java 11 or higher** (Java 11, 17, 21+):
|
|||||||
# Launch interactive GUI
|
# Launch interactive GUI
|
||||||
java -jar j3270.jar
|
java -jar j3270.jar
|
||||||
|
|
||||||
|
# Connect to TLS/SSL mainframe with custom port
|
||||||
|
java -jar j3270.jar -s mainframe.example.com 992
|
||||||
|
# Or using standard x3270 L: prefix
|
||||||
|
java -jar j3270.jar L:mainframe.example.com:992
|
||||||
|
|
||||||
|
# Connect with unverified/self-signed certificate verification bypass
|
||||||
|
java -jar j3270.jar --tls --insecure mainframe.example.com 992
|
||||||
|
|
||||||
# Launch with custom configuration file
|
# Launch with custom configuration file
|
||||||
java -jar j3270.jar -c config.ini
|
java -jar j3270.jar -c config.ini
|
||||||
|
|
||||||
@@ -36,6 +44,10 @@ java -jar j3270.jar -c config.ini
|
|||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption.
|
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption.
|
||||||
|
- **SSL/TLS Security**:
|
||||||
|
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports.
|
||||||
|
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection.
|
||||||
|
- Optional unverified/insecure mode for testing and headless scripting.
|
||||||
- **IND$FILE File Transfer**: Full support for both **CUT** and **DFT (DDM)** high-speed structured field transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
|
- **IND$FILE File Transfer**: Full support for both **CUT** and **DFT (DDM)** high-speed structured field transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
|
||||||
- **z/VM & Line-Mode Support**: Proper SSCP-LU and unformatted line handling (`CP TERM CONMODE 3270` supported).
|
- **z/VM & Line-Mode Support**: Proper SSCP-LU and unformatted line handling (`CP TERM CONMODE 3270` supported).
|
||||||
- **APL & Graphic Escape**: Comprehensive box-drawing and math symbol character rendering.
|
- **APL & Graphic Escape**: Comprehensive box-drawing and math symbol character rendering.
|
||||||
@@ -59,7 +71,7 @@ The project includes self-contained, portable build scripts:
|
|||||||
# Build executable JAR in 2 seconds:
|
# Build executable JAR in 2 seconds:
|
||||||
sh ./build_all.sh
|
sh ./build_all.sh
|
||||||
|
|
||||||
# Run all 25 automated unit tests in ~130ms:
|
# Run all 31 automated unit tests in ~180ms:
|
||||||
sh ./test_all.sh
|
sh ./test_all.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -73,29 +85,6 @@ The resulting standalone JAR is created at `build/j3270.jar`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏷️ How to Tag & Publish Releases
|
|
||||||
|
|
||||||
Releases can be published automatically to Gitea via Git tags:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Create a version tag
|
|
||||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
|
||||||
|
|
||||||
# 2. Push the tag to Gitea
|
|
||||||
git push origin v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Pushing any `v*` tag triggers the automated release workflow (`.gitea/workflows/release.yaml`), which runs tests, builds the JAR, and publishes a new Gitea Release with `j3270.jar` attached automatically.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Topics & Discoverability
|
|
||||||
|
|
||||||
Suggested keywords/topics for repository indexing:
|
|
||||||
`tn3270` · `tn3270e` · `ibm-3270` · `mainframe` · `terminal-emulator` · `x3270` · `ind-file` · `file-transfer` · `ebcdic` · `z-vm` · `z-os` · `cms` · `tso` · `cics` · `java` · `desktop-app`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
|
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -227,8 +227,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
private void showConnectDialog() {
|
private void showConnectDialog() {
|
||||||
ConnectDialog dialog = new ConnectDialog(this);
|
ConnectDialog dialog = new ConnectDialog(this);
|
||||||
dialog.setInitialHost(lastHost);
|
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
||||||
dialog.setInitialPort(lastPort);
|
int initialPort = lastPort > 0 ? lastPort : org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
||||||
|
dialog.setInitialHost(initialHost);
|
||||||
|
dialog.setInitialPort(initialPort);
|
||||||
|
dialog.setInitialTls(org.pubvm.j3270.config.Settings.getAutoConnectTls());
|
||||||
|
dialog.setInitialVerifyCert(org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||||
dialog.setVisible(true);
|
dialog.setVisible(true);
|
||||||
|
|
||||||
if (dialog.isConfirmed()) {
|
if (dialog.isConfirmed()) {
|
||||||
@@ -255,10 +259,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
// Save for auto-connect
|
// Save for auto-connect
|
||||||
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||||
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||||
|
org.pubvm.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||||
|
org.pubvm.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||||
|
|
||||||
// Disconnect existing connection
|
// Disconnect existing connection
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|
||||||
|
// Wire interactive certificate verifier if none configured
|
||||||
|
if (config.getCertificateVerifier() == null) {
|
||||||
|
config.setCertificateVerifier((chain, authType, exception) ->
|
||||||
|
org.pubvm.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
client = new Telnet3270Client(config);
|
client = new Telnet3270Client(config);
|
||||||
client.addConnectionListener(this);
|
client.addConnectionListener(this);
|
||||||
client.addScreenUpdateListener(this);
|
client.addScreenUpdateListener(this);
|
||||||
@@ -266,7 +279,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
terminalPanel.setClient(client);
|
terminalPanel.setClient(client);
|
||||||
statusBar.setClient(client);
|
statusBar.setClient(client);
|
||||||
|
|
||||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
|
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||||
|
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||||
|
|
||||||
// Resize window to match model
|
// Resize window to match model
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
@@ -426,11 +440,24 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
boolean debug = false;
|
boolean debug = false;
|
||||||
|
boolean cliTls = false;
|
||||||
|
boolean cliNoVerifyCert = false;
|
||||||
|
org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
||||||
String configFile = null;
|
String configFile = null;
|
||||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
|
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
|
||||||
debug = true;
|
debug = true;
|
||||||
|
} else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) {
|
||||||
|
cliTls = true;
|
||||||
|
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
|
||||||
|
cliNoVerifyCert = true;
|
||||||
|
} else if (args[i].startsWith("--graphics=")) {
|
||||||
|
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
|
||||||
|
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||||
|
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
|
||||||
|
} else if ("--no-graphics".equals(args[i])) {
|
||||||
|
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
|
||||||
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||||
configFile = args[++i];
|
configFile = args[++i];
|
||||||
} else {
|
} else {
|
||||||
@@ -438,16 +465,45 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure logging over the absolute root logger
|
// Configure logging for application packages
|
||||||
|
Level logLevel = debug ? Level.ALL : Level.INFO;
|
||||||
Logger globalRoot = Logger.getLogger("");
|
Logger globalRoot = Logger.getLogger("");
|
||||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||||
globalRoot.removeHandler(h);
|
globalRoot.removeHandler(h);
|
||||||
}
|
}
|
||||||
globalRoot.setLevel(debug ? Level.ALL : Level.WARNING);
|
globalRoot.setLevel(Level.ALL);
|
||||||
ConsoleHandler handler = new ConsoleHandler();
|
|
||||||
handler.setLevel(Level.ALL);
|
java.util.logging.Filter appFilter = record -> {
|
||||||
handler.setFormatter(new SimpleFormatter());
|
String name = record.getLoggerName();
|
||||||
globalRoot.addHandler(handler);
|
return name != null && (name.startsWith("org.lib3270j") || name.startsWith("org.pubvm.j3270"));
|
||||||
|
};
|
||||||
|
|
||||||
|
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||||
|
consoleHandler.setLevel(logLevel);
|
||||||
|
consoleHandler.setFormatter(new SimpleFormatter());
|
||||||
|
consoleHandler.setFilter(appFilter);
|
||||||
|
globalRoot.addHandler(consoleHandler);
|
||||||
|
|
||||||
|
Logger.getLogger("org.pubvm").setLevel(logLevel);
|
||||||
|
Logger.getLogger("org.pubvm.j3270").setLevel(logLevel);
|
||||||
|
Logger.getLogger("org.lib3270j").setLevel(logLevel);
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||||
|
@Override
|
||||||
|
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||||
|
super.publish(record);
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileHandler.setLevel(Level.ALL);
|
||||||
|
fileHandler.setFormatter(new SimpleFormatter());
|
||||||
|
fileHandler.setFilter(appFilter);
|
||||||
|
globalRoot.addHandler(fileHandler);
|
||||||
|
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Load INI config file if specified
|
// Load INI config file if specified
|
||||||
if (configFile != null) {
|
if (configFile != null) {
|
||||||
@@ -471,18 +527,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
||||||
System.setProperty("apple.awt.application.name", "j3270");
|
System.setProperty("apple.awt.application.name", "j3270");
|
||||||
|
|
||||||
|
final boolean finalTls = cliTls;
|
||||||
|
final boolean finalNoVerify = cliNoVerifyCert;
|
||||||
|
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
J3270App app = new J3270App();
|
J3270App app = new J3270App();
|
||||||
app.setVisible(true);
|
app.setVisible(true);
|
||||||
|
|
||||||
// If host:port given on command line, connect directly
|
// If host:port given on command line, connect directly
|
||||||
if (!remainingArgs.isEmpty()) {
|
if (!remainingArgs.isEmpty()) {
|
||||||
String host = remainingArgs.get(0);
|
String hostArg = remainingArgs.get(0);
|
||||||
int port = 23;
|
int port = finalTls ? 992 : 23;
|
||||||
if (remainingArgs.size() >= 2) {
|
if (remainingArgs.size() >= 2) {
|
||||||
try {
|
try {
|
||||||
port = Integer.parseInt(remainingArgs.get(1));
|
port = Integer.parseInt(remainingArgs.get(1));
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TerminalModel model = TerminalModel.IBM_3279_4;
|
TerminalModel model = TerminalModel.IBM_3279_4;
|
||||||
@@ -490,10 +550,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
try {
|
try {
|
||||||
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
||||||
model = TerminalModel.forModel(modelNum, true);
|
model = TerminalModel.forModel(modelNum, true);
|
||||||
} catch (Exception e) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConnectionConfig config = new ConnectionConfig(host, port, model);
|
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
||||||
|
if (finalTls) {
|
||||||
|
config.setUseTls(true);
|
||||||
|
}
|
||||||
|
if (finalNoVerify) {
|
||||||
|
config.setTlsVerifyCert(false);
|
||||||
|
}
|
||||||
|
if (finalGraphicsMode != null) {
|
||||||
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
|
} else {
|
||||||
|
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
|
||||||
|
}
|
||||||
app.connect(config);
|
app.connect(config);
|
||||||
} else {
|
} else {
|
||||||
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
|
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
|
||||||
@@ -505,8 +576,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
case AUTO_CONNECT:
|
case AUTO_CONNECT:
|
||||||
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
||||||
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
||||||
|
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
|
||||||
|
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
|
||||||
if (host != null && !host.isEmpty()) {
|
if (host != null && !host.isEmpty()) {
|
||||||
app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4));
|
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||||
|
config.setTlsVerifyCert(verify);
|
||||||
|
if (finalGraphicsMode != null) {
|
||||||
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
|
} else {
|
||||||
|
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
|
||||||
|
}
|
||||||
|
app.connect(config);
|
||||||
} else {
|
} else {
|
||||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,31 @@ public class Settings {
|
|||||||
prefs.putInt("autoConnectPort", port);
|
prefs.putInt("autoConnectPort", port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean getAutoConnectTls() {
|
||||||
|
return prefs.getBoolean("autoConnectTls", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAutoConnectTls(boolean tls) {
|
||||||
|
prefs.putBoolean("autoConnectTls", tls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getAutoConnectVerifyCert() {
|
||||||
|
return prefs.getBoolean("autoConnectVerifyCert", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAutoConnectVerifyCert(boolean verify) {
|
||||||
|
prefs.putBoolean("autoConnectVerifyCert", verify);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static org.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
||||||
|
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.NONE.name());
|
||||||
|
return org.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
|
||||||
|
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.NONE).name());
|
||||||
|
}
|
||||||
|
|
||||||
public static Color getColorOverride(int index, Color defaultColor) {
|
public static Color getColorOverride(int index, Color defaultColor) {
|
||||||
String hex = prefs.get("color_" + index, null);
|
String hex = prefs.get("color_" + index, null);
|
||||||
try {
|
try {
|
||||||
@@ -194,15 +219,33 @@ public class Settings {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "behavior":
|
case "behavior":
|
||||||
|
case "connection":
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "startupBehavior":
|
case "startupBehavior":
|
||||||
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
|
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
|
||||||
break;
|
break;
|
||||||
case "autoConnectHost": setAutoConnectHost(value); break;
|
case "autoConnectHost":
|
||||||
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
|
case "host":
|
||||||
|
setAutoConnectHost(value);
|
||||||
|
break;
|
||||||
|
case "autoConnectPort":
|
||||||
|
case "port":
|
||||||
|
setAutoConnectPort(Integer.parseInt(value));
|
||||||
|
break;
|
||||||
|
case "autoConnectTls":
|
||||||
|
case "tls":
|
||||||
|
case "ssl":
|
||||||
|
case "useTls":
|
||||||
|
setAutoConnectTls(Boolean.parseBoolean(value));
|
||||||
|
break;
|
||||||
|
case "autoConnectVerifyCert":
|
||||||
|
case "verifyCert":
|
||||||
|
case "tlsVerifyCert":
|
||||||
|
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
|
||||||
|
break;
|
||||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||||
default:
|
default:
|
||||||
log.warning("Unknown behavior key: " + key);
|
log.warning("Unknown behavior/connection key: " + key);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -222,6 +265,14 @@ public class Settings {
|
|||||||
setKeyBinding(key, value);
|
setKeyBinding(key, value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "graphics":
|
||||||
|
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
|
||||||
|
setGraphicsMode(org.lib3270j.graphics.GraphicsMode.fromString(value));
|
||||||
|
} else {
|
||||||
|
log.warning("Unknown graphics key: " + key);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Allow bare keys outside any section — treat as raw prefs
|
// Allow bare keys outside any section — treat as raw prefs
|
||||||
log.fine("Setting raw preference: " + key + " = " + value);
|
log.fine("Setting raw preference: " + key + " = " + value);
|
||||||
@@ -256,10 +307,17 @@ public class Settings {
|
|||||||
if (acHost != null && !acHost.isEmpty()) {
|
if (acHost != null && !acHost.isEmpty()) {
|
||||||
w.println("autoConnectHost = " + acHost);
|
w.println("autoConnectHost = " + acHost);
|
||||||
w.println("autoConnectPort = " + getAutoConnectPort());
|
w.println("autoConnectPort = " + getAutoConnectPort());
|
||||||
|
w.println("autoConnectTls = " + getAutoConnectTls());
|
||||||
|
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
|
||||||
}
|
}
|
||||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||||
w.println();
|
w.println();
|
||||||
|
|
||||||
|
// [graphics]
|
||||||
|
w.println("[graphics]");
|
||||||
|
w.println("graphicsMode = " + getGraphicsMode().name());
|
||||||
|
w.println();
|
||||||
|
|
||||||
// [colors]
|
// [colors]
|
||||||
w.println("[colors]");
|
w.println("[colors]");
|
||||||
// Host colors 0-15
|
// Host colors 0-15
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ public class ConnectDialog extends JDialog {
|
|||||||
private JTextField hostField;
|
private JTextField hostField;
|
||||||
private JTextField portField;
|
private JTextField portField;
|
||||||
private JComboBox<TerminalModel> modelCombo;
|
private JComboBox<TerminalModel> modelCombo;
|
||||||
|
private JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
||||||
private JTextField luField;
|
private JTextField luField;
|
||||||
|
private JCheckBox tlsCheckBox;
|
||||||
|
private JCheckBox verifyCertCheckBox;
|
||||||
private boolean confirmed;
|
private boolean confirmed;
|
||||||
private ConnectionConfig result;
|
private ConnectionConfig result;
|
||||||
|
|
||||||
@@ -93,6 +96,56 @@ public class ConnectDialog extends JDialog {
|
|||||||
luField = createDarkField(12);
|
luField = createDarkField(12);
|
||||||
mainPanel.add(luField, gbc);
|
mainPanel.add(luField, gbc);
|
||||||
|
|
||||||
|
// Graphics Mode
|
||||||
|
gbc.gridx = 0;
|
||||||
|
gbc.gridy = 4;
|
||||||
|
gbc.weightx = 0;
|
||||||
|
JLabel graphicsLabel = new JLabel("Graphics:");
|
||||||
|
graphicsLabel.setForeground(fg);
|
||||||
|
graphicsLabel.setFont(labelFont);
|
||||||
|
mainPanel.add(graphicsLabel, gbc);
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.weightx = 1.0;
|
||||||
|
graphicsCombo = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values());
|
||||||
|
graphicsCombo.setSelectedItem(org.pubvm.j3270.config.Settings.getGraphicsMode());
|
||||||
|
graphicsCombo.setBackground(new Color(45, 45, 45));
|
||||||
|
graphicsCombo.setForeground(fg);
|
||||||
|
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||||
|
mainPanel.add(graphicsCombo, gbc);
|
||||||
|
|
||||||
|
// TLS / SSL Checkbox
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.gridy = 5;
|
||||||
|
gbc.weightx = 1.0;
|
||||||
|
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
||||||
|
tlsCheckBox.setBackground(new Color(30, 30, 30));
|
||||||
|
tlsCheckBox.setForeground(fg);
|
||||||
|
tlsCheckBox.setFont(labelFont);
|
||||||
|
tlsCheckBox.setFocusPainted(false);
|
||||||
|
tlsCheckBox.addActionListener(e -> {
|
||||||
|
boolean isTls = tlsCheckBox.isSelected();
|
||||||
|
verifyCertCheckBox.setEnabled(isTls);
|
||||||
|
String currentPort = portField.getText().trim();
|
||||||
|
if (isTls && "23".equals(currentPort)) {
|
||||||
|
portField.setText("992");
|
||||||
|
} else if (!isTls && "992".equals(currentPort)) {
|
||||||
|
portField.setText("23");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainPanel.add(tlsCheckBox, gbc);
|
||||||
|
|
||||||
|
// Verify Certificate Checkbox
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.gridy = 6;
|
||||||
|
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
||||||
|
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
|
||||||
|
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
|
||||||
|
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||||
|
verifyCertCheckBox.setSelected(true);
|
||||||
|
verifyCertCheckBox.setEnabled(false);
|
||||||
|
verifyCertCheckBox.setFocusPainted(false);
|
||||||
|
mainPanel.add(verifyCertCheckBox, gbc);
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||||
@@ -116,7 +169,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
buttonPanel.add(connectBtn);
|
buttonPanel.add(connectBtn);
|
||||||
|
|
||||||
gbc.gridx = 0;
|
gbc.gridx = 0;
|
||||||
gbc.gridy = 4;
|
gbc.gridy = 7;
|
||||||
gbc.gridwidth = 2;
|
gbc.gridwidth = 2;
|
||||||
mainPanel.add(buttonPanel, gbc);
|
mainPanel.add(buttonPanel, gbc);
|
||||||
|
|
||||||
@@ -158,6 +211,9 @@ public class ConnectDialog extends JDialog {
|
|||||||
if (!lu.isEmpty()) {
|
if (!lu.isEmpty()) {
|
||||||
result.setLuName(lu);
|
result.setLuName(lu);
|
||||||
}
|
}
|
||||||
|
result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
||||||
|
result.setUseTls(tlsCheckBox.isSelected());
|
||||||
|
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||||
confirmed = true;
|
confirmed = true;
|
||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
@@ -178,4 +234,13 @@ public class ConnectDialog extends JDialog {
|
|||||||
public void setInitialPort(int port) {
|
public void setInitialPort(int port) {
|
||||||
portField.setText(String.valueOf(port));
|
portField.setText(String.valueOf(port));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setInitialTls(boolean tls) {
|
||||||
|
tlsCheckBox.setSelected(tls);
|
||||||
|
verifyCertCheckBox.setEnabled(tls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitialVerifyCert(boolean verify) {
|
||||||
|
verifyCertCheckBox.setSelected(verify);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
// Appearance tab
|
// Appearance tab
|
||||||
private JComboBox<String> fontBox;
|
private JComboBox<String> fontBox;
|
||||||
private JSpinner fontSizeSpinner;
|
private JSpinner fontSizeSpinner;
|
||||||
|
private JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsModeBox;
|
||||||
|
|
||||||
// Behavior tab
|
// Behavior tab
|
||||||
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
||||||
@@ -264,8 +265,21 @@ public class SettingsDialog extends JDialog {
|
|||||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
panel.add(fontSizeSpinner, gbc);
|
panel.add(fontSizeSpinner, gbc);
|
||||||
|
|
||||||
// Fill remaining space
|
// Graphics Mode
|
||||||
|
gbc.gridx = 0;
|
||||||
gbc.gridy = 2;
|
gbc.gridy = 2;
|
||||||
|
gbc.fill = GridBagConstraints.NONE;
|
||||||
|
JLabel graphicsLabel = new JLabel("Graphics Mode:");
|
||||||
|
panel.add(graphicsLabel, gbc);
|
||||||
|
|
||||||
|
graphicsModeBox = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values());
|
||||||
|
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
panel.add(graphicsModeBox, gbc);
|
||||||
|
|
||||||
|
// Fill remaining space
|
||||||
|
gbc.gridy = 3;
|
||||||
gbc.weighty = 1.0;
|
gbc.weighty = 1.0;
|
||||||
panel.add(Box.createGlue(), gbc);
|
panel.add(Box.createGlue(), gbc);
|
||||||
|
|
||||||
@@ -611,6 +625,9 @@ public class SettingsDialog extends JDialog {
|
|||||||
Settings.setFontFamily(fontFam);
|
Settings.setFontFamily(fontFam);
|
||||||
}
|
}
|
||||||
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
|
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
|
||||||
|
if (graphicsModeBox.getSelectedItem() != null) {
|
||||||
|
Settings.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
|
||||||
|
}
|
||||||
|
|
||||||
// Apply Behavior
|
// Apply Behavior
|
||||||
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
|
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import java.awt.*;
|
|||||||
public class StatusBar extends JPanel {
|
public class StatusBar extends JPanel {
|
||||||
|
|
||||||
private final JLabel connectionStatus;
|
private final JLabel connectionStatus;
|
||||||
|
private final JLabel tlsStatus;
|
||||||
private final JLabel cursorPosition;
|
private final JLabel cursorPosition;
|
||||||
private final JLabel luName;
|
private final JLabel luName;
|
||||||
private final JLabel lockStatus;
|
private final JLabel lockStatus;
|
||||||
@@ -37,6 +38,7 @@ public class StatusBar extends JPanel {
|
|||||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||||
|
|
||||||
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
|
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
|
||||||
|
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
||||||
luName = createLabel("", oiaFont, OIA_FG);
|
luName = createLabel("", oiaFont, OIA_FG);
|
||||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||||
@@ -44,6 +46,8 @@ public class StatusBar extends JPanel {
|
|||||||
|
|
||||||
add(Box.createHorizontalStrut(6));
|
add(Box.createHorizontalStrut(6));
|
||||||
add(connectionStatus);
|
add(connectionStatus);
|
||||||
|
add(Box.createHorizontalStrut(10));
|
||||||
|
add(tlsStatus);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
add(luName);
|
add(luName);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
@@ -110,6 +114,26 @@ public class StatusBar extends JPanel {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLS Status
|
||||||
|
if (state.isConnected() && client.getConfig().isUseTls()) {
|
||||||
|
boolean verified = client.getConfig().isTlsVerifyCert();
|
||||||
|
javax.net.ssl.SSLSession session = client.getSslSession();
|
||||||
|
String cipher = session != null ? session.getCipherSuite() : "TLS";
|
||||||
|
String protocol = session != null ? session.getProtocol() : "TLS";
|
||||||
|
if (verified) {
|
||||||
|
tlsStatus.setText("🔒 TLS");
|
||||||
|
tlsStatus.setForeground(OIA_FG);
|
||||||
|
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
|
||||||
|
} else {
|
||||||
|
tlsStatus.setText("🔓 TLS (Unverified)");
|
||||||
|
tlsStatus.setForeground(new Color(255, 180, 80));
|
||||||
|
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tlsStatus.setText("");
|
||||||
|
tlsStatus.setToolTipText(null);
|
||||||
|
}
|
||||||
|
|
||||||
// LU name
|
// LU name
|
||||||
String lu = "";
|
String lu = "";
|
||||||
if (client.getConnectionState().isTn3270e()) {
|
if (client.getConnectionState().isTn3270e()) {
|
||||||
|
|||||||
@@ -889,10 +889,20 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (faIsHigh(currentFA & 0xFF))
|
if (faIsHigh(currentFA & 0xFF))
|
||||||
bold = true;
|
bold = true;
|
||||||
|
|
||||||
// Handle invisible fields (zero intensity)
|
// Handle invisible fields (zero intensity / password fields)
|
||||||
|
// Modern UX: render '*' for typed characters so user sees length/digit count
|
||||||
if (faIsZero(currentFA & 0xFF)) {
|
if (faIsZero(currentFA & 0xFF)) {
|
||||||
g2.setColor(this.bgColor);
|
g2.setColor(this.bgColor);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
|
||||||
|
char ch = ea.ucs4;
|
||||||
|
if (ch > 0x20 && ch != 0xFF) {
|
||||||
|
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
||||||
|
g2.setFont(f);
|
||||||
|
g2.setColor(fgColor);
|
||||||
|
g2.drawString("*", x, y + fontAscent);
|
||||||
|
}
|
||||||
|
|
||||||
if (isCellSelected(row, col)) {
|
if (isCellSelected(row, col)) {
|
||||||
g2.setColor(SELECTION_COLOR);
|
g2.setColor(SELECTION_COLOR);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
@@ -911,7 +921,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
|
||||||
// Draw character
|
// Draw character or Programmed Symbol
|
||||||
|
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
|
||||||
|
boolean drawnAsPs = false;
|
||||||
|
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
|
||||||
|
drawnAsPs = client.getProgramSymbolManager().drawSymbol(g2, cs, ea.ec & 0xFF, x, y, cellWidth, cellHeight, fgColor, bgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!drawnAsPs) {
|
||||||
char ch = ea.ucs4;
|
char ch = ea.ucs4;
|
||||||
if (ch > 0x20 && ch != 0xFF) {
|
if (ch > 0x20 && ch != 0xFF) {
|
||||||
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
||||||
@@ -919,6 +936,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw underline
|
// Draw underline
|
||||||
if (underline) {
|
if (underline) {
|
||||||
@@ -935,6 +953,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw Vector Graphics Plane overlay if present
|
||||||
|
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) {
|
||||||
|
int gridW = cols * cellWidth;
|
||||||
|
int gridH = rows * cellHeight;
|
||||||
|
client.getGraphicsPlane().resize(gridW, gridH);
|
||||||
|
g2.drawImage(client.getGraphicsPlane().getImage(), ox, oy, gridW, gridH, null);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw cursor
|
// Draw cursor
|
||||||
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
||||||
int curAddr = sb.getCursorAddress();
|
int curAddr = sb.getCursorAddress();
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package org.pubvm.j3270.ui;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog for inspecting and confirming untrusted TLS/SSL server certificates.
|
||||||
|
*/
|
||||||
|
public class UntrustedCertificateDialog extends JDialog {
|
||||||
|
|
||||||
|
private boolean accepted = false;
|
||||||
|
|
||||||
|
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||||
|
super(parent, "Untrusted SSL/TLS Certificate", true);
|
||||||
|
buildUI(host, port, chain, exception);
|
||||||
|
pack();
|
||||||
|
setLocationRelativeTo(parent);
|
||||||
|
setResizable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||||
|
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
|
||||||
|
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||||
|
mainPanel.setBackground(new Color(30, 30, 30));
|
||||||
|
|
||||||
|
// Header
|
||||||
|
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
|
||||||
|
headerPanel.setBackground(new Color(30, 30, 30));
|
||||||
|
JLabel iconLabel = new JLabel("⚠️");
|
||||||
|
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
|
||||||
|
headerPanel.add(iconLabel);
|
||||||
|
|
||||||
|
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
|
||||||
|
titleBox.setBackground(new Color(30, 30, 30));
|
||||||
|
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
|
||||||
|
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
|
||||||
|
titleLabel.setForeground(new Color(255, 180, 80));
|
||||||
|
titleBox.add(titleLabel);
|
||||||
|
|
||||||
|
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
|
||||||
|
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||||
|
subtitleLabel.setForeground(new Color(180, 180, 180));
|
||||||
|
titleBox.add(subtitleLabel);
|
||||||
|
headerPanel.add(titleBox);
|
||||||
|
mainPanel.add(headerPanel, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
// Certificate Details Area
|
||||||
|
X509Certificate cert = (chain != null && chain.length > 0) ? chain[0] : null;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (exception != null) {
|
||||||
|
sb.append("Validation Error:\n ").append(exception.getMessage() != null ? exception.getMessage() : exception.toString()).append("\n\n");
|
||||||
|
}
|
||||||
|
if (cert != null) {
|
||||||
|
sb.append("Subject:\n ").append(cert.getSubjectX500Principal().getName()).append("\n\n");
|
||||||
|
sb.append("Issuer:\n ").append(cert.getIssuerX500Principal().getName()).append("\n\n");
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||||
|
sb.append("Validity:\n From: ").append(sdf.format(cert.getNotBefore()))
|
||||||
|
.append("\n To: ").append(sdf.format(cert.getNotAfter())).append("\n\n");
|
||||||
|
sb.append("Serial Number:\n ").append(cert.getSerialNumber().toString(16).toUpperCase()).append("\n\n");
|
||||||
|
sb.append("SHA-256 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-256")).append("\n\n");
|
||||||
|
sb.append("SHA-1 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-1"));
|
||||||
|
} else {
|
||||||
|
sb.append("No peer certificate information available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JTextArea detailsArea = new JTextArea(sb.toString());
|
||||||
|
detailsArea.setEditable(false);
|
||||||
|
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||||
|
detailsArea.setBackground(new Color(20, 20, 20));
|
||||||
|
detailsArea.setForeground(new Color(210, 210, 210));
|
||||||
|
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(detailsArea);
|
||||||
|
scrollPane.setPreferredSize(new Dimension(520, 260));
|
||||||
|
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
|
||||||
|
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||||
|
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||||
|
|
||||||
|
JButton cancelBtn = new JButton("Cancel Connection");
|
||||||
|
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||||
|
cancelBtn.setForeground(new Color(220, 220, 220));
|
||||||
|
cancelBtn.addActionListener(e -> {
|
||||||
|
accepted = false;
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
JButton trustBtn = new JButton("Connect Anyway");
|
||||||
|
trustBtn.setBackground(new Color(180, 100, 40));
|
||||||
|
trustBtn.setForeground(Color.WHITE);
|
||||||
|
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||||
|
trustBtn.addActionListener(e -> {
|
||||||
|
accepted = true;
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonPanel.add(cancelBtn);
|
||||||
|
buttonPanel.add(trustBtn);
|
||||||
|
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
setContentPane(mainPanel);
|
||||||
|
getRootPane().setDefaultButton(trustBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAccepted() {
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String computeFingerprint(X509Certificate cert, String algorithm) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance(algorithm);
|
||||||
|
byte[] digest = md.digest(cert.getEncoded());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < digest.length; i++) {
|
||||||
|
if (i > 0) sb.append(':');
|
||||||
|
sb.append(String.format("%02X", digest[i]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Unable to compute fingerprint: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the prompt modal dialog on the Swing EDT and return user choice.
|
||||||
|
*/
|
||||||
|
public static boolean showPrompt(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||||
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
|
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
return dialog.isAccepted();
|
||||||
|
} else {
|
||||||
|
AtomicBoolean result = new AtomicBoolean(false);
|
||||||
|
try {
|
||||||
|
SwingUtilities.invokeAndWait(() -> {
|
||||||
|
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
result.set(dialog.isAccepted());
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,13 @@ public class ConnectionConfig {
|
|||||||
private String luName = null;
|
private String luName = null;
|
||||||
private boolean extendedDataStream = true;
|
private boolean extendedDataStream = true;
|
||||||
private boolean useTls = false;
|
private boolean useTls = false;
|
||||||
|
private boolean tlsVerifyCert = true;
|
||||||
|
private org.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
|
||||||
|
private String sslProtocol = "TLS";
|
||||||
private int connectTimeoutMs = 15000;
|
private int connectTimeoutMs = 15000;
|
||||||
private int nopIntervalSeconds = 0;
|
private int nopIntervalSeconds = 0;
|
||||||
private String terminalName = null; // override terminal type string
|
private String terminalName = null; // override terminal type string
|
||||||
|
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
|
||||||
|
|
||||||
public ConnectionConfig() {}
|
public ConnectionConfig() {}
|
||||||
|
|
||||||
@@ -22,12 +26,25 @@ public class ConnectionConfig {
|
|||||||
this.port = port;
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ConnectionConfig(String host, int port, boolean useTls) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.useTls = useTls;
|
||||||
|
}
|
||||||
|
|
||||||
public ConnectionConfig(String host, int port, TerminalModel model) {
|
public ConnectionConfig(String host, int port, TerminalModel model) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.model = model;
|
||||||
|
this.useTls = useTls;
|
||||||
|
}
|
||||||
|
|
||||||
public String getHost() { return host; }
|
public String getHost() { return host; }
|
||||||
public void setHost(String host) { this.host = host; }
|
public void setHost(String host) { this.host = host; }
|
||||||
|
|
||||||
@@ -46,15 +63,79 @@ public class ConnectionConfig {
|
|||||||
public boolean isUseTls() { return useTls; }
|
public boolean isUseTls() { return useTls; }
|
||||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||||
|
|
||||||
|
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||||
|
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
||||||
|
|
||||||
|
public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||||
|
public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||||
|
|
||||||
|
public String getSslProtocol() { return sslProtocol; }
|
||||||
|
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
|
||||||
|
|
||||||
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
||||||
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
|
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
|
||||||
|
|
||||||
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
||||||
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
||||||
|
|
||||||
|
public org.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
|
||||||
|
public void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
|
||||||
|
this.graphicsMode = (mode != null) ? mode : org.lib3270j.graphics.GraphicsMode.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
public String getTerminalName() { return terminalName; }
|
public String getTerminalName() { return terminalName; }
|
||||||
public void setTerminalName(String name) { this.terminalName = name; }
|
public void setTerminalName(String name) { this.terminalName = name; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port")
|
||||||
|
* or standard "host:port" formats.
|
||||||
|
*/
|
||||||
|
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||||
|
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||||
|
return new ConnectionConfig("localhost", defaultPort, defaultModel);
|
||||||
|
}
|
||||||
|
String s = hostStr.trim();
|
||||||
|
boolean tls = false;
|
||||||
|
|
||||||
|
// Check TLS prefixes
|
||||||
|
if (s.startsWith("L:") || s.startsWith("l:")) {
|
||||||
|
tls = true;
|
||||||
|
s = s.substring(2);
|
||||||
|
} else if (s.startsWith("Y:") || s.startsWith("y:")) {
|
||||||
|
tls = true;
|
||||||
|
s = s.substring(2);
|
||||||
|
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
||||||
|
tls = true;
|
||||||
|
s = s.substring(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
String host = s;
|
||||||
|
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||||
|
|
||||||
|
// Check for host:port (handle IPv6 [::1]:port)
|
||||||
|
if (s.startsWith("[") && s.contains("]")) {
|
||||||
|
int closeBracket = s.indexOf(']');
|
||||||
|
host = s.substring(1, closeBracket);
|
||||||
|
if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') {
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(s.substring(closeBracket + 2));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int colon = s.lastIndexOf(':');
|
||||||
|
if (colon > 0 && colon < s.length() - 1) {
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(s.substring(colon + 1));
|
||||||
|
host = s.substring(0, colon);
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||||
|
config.setUseTls(tls);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the effective terminal type string to send during negotiation.
|
* Get the effective terminal type string to send during negotiation.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -46,12 +46,14 @@ public class Telnet3270Client {
|
|||||||
this.translator = new EbcdicTranslator();
|
this.translator = new EbcdicTranslator();
|
||||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||||
|
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||||
|
|
||||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||||
dsProcessor.setInputProcessor(inputProcessor);
|
dsProcessor.setInputProcessor(inputProcessor);
|
||||||
|
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,6 +124,16 @@ public class Telnet3270Client {
|
|||||||
/** Get the connection config. */
|
/** Get the connection config. */
|
||||||
public ConnectionConfig getConfig() { return config; }
|
public ConnectionConfig getConfig() { return config; }
|
||||||
|
|
||||||
|
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||||
|
public void setTlsCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||||
|
config.setCertificateVerifier(verifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the active SSLSession if connected over TLS, or null. */
|
||||||
|
public javax.net.ssl.SSLSession getSslSession() {
|
||||||
|
return connection.getSslSession();
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Convenience input methods ==========
|
// ========== Convenience input methods ==========
|
||||||
|
|
||||||
/** Type a character at the cursor position. */
|
/** Type a character at the cursor position. */
|
||||||
@@ -210,4 +222,16 @@ public class Telnet3270Client {
|
|||||||
public void sysReq() { inputProcessor.sysReq(); }
|
public void sysReq() { inputProcessor.sysReq(); }
|
||||||
/** Reset (unlock keyboard). */
|
/** Reset (unlock keyboard). */
|
||||||
public void reset() { inputProcessor.reset(); }
|
public void reset() { inputProcessor.reset(); }
|
||||||
|
|
||||||
|
public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||||
|
return dsProcessor.getProgramSymbolManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||||
|
return dsProcessor.getGraphicsPlane();
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||||
|
return dsProcessor.getGocaDecoder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,20 @@ public class EbcdicTranslator {
|
|||||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper to translate EBCDIC byte to Unicode character.
|
||||||
|
*/
|
||||||
|
public static char toUnicode(int ebc) {
|
||||||
|
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper to translate EBCDIC byte to ASCII character.
|
||||||
|
*/
|
||||||
|
public static char ebcdicToAscii(int ebc) {
|
||||||
|
return toUnicode(ebc);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate Unicode character to EBCDIC byte.
|
* Translate Unicode character to EBCDIC byte.
|
||||||
* Returns -1 if the character cannot be mapped.
|
* Returns -1 if the character cannot be mapped.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.lib3270j.charset.EbcdicTranslator;
|
|||||||
import org.lib3270j.listener.ScreenUpdateListener;
|
import org.lib3270j.listener.ScreenUpdateListener;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -37,6 +38,13 @@ public class DataStreamProcessor {
|
|||||||
// Input processor reference to manage keyboard locking state
|
// Input processor reference to manage keyboard locking state
|
||||||
private org.lib3270j.input.InputProcessor inputProcessor;
|
private org.lib3270j.input.InputProcessor inputProcessor;
|
||||||
|
|
||||||
|
// Graphics & Programmed Symbols
|
||||||
|
private final org.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new org.lib3270j.graphics.ProgramSymbolManager();
|
||||||
|
private final org.lib3270j.graphics.GraphicsPlane graphicsPlane = new org.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||||
|
private final org.lib3270j.graphics.GocaDecoder gocaDecoder = new org.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
||||||
|
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||||
|
private int currentGocaSubtype = 0;
|
||||||
|
|
||||||
/** Functional interface for sending output back through the telnet stack. */
|
/** Functional interface for sending output back through the telnet stack. */
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface OutputSender {
|
public interface OutputSender {
|
||||||
@@ -49,6 +57,23 @@ public class DataStreamProcessor {
|
|||||||
this.qrBuilder = new QueryReplyBuilder(screen);
|
this.qrBuilder = new QueryReplyBuilder(screen);
|
||||||
this.outputBuffer = new byte[32768];
|
this.outputBuffer = new byte[32768];
|
||||||
this.outputPos = 0;
|
this.outputPos = 0;
|
||||||
|
this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryReplyBuilder getQueryReplyBuilder() {
|
||||||
|
return qrBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||||
|
return programSymbolManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||||
|
return graphicsPlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||||
|
return gocaDecoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOutputSender(OutputSender sender) {
|
public void setOutputSender(OutputSender sender) {
|
||||||
@@ -93,6 +118,7 @@ public class DataStreamProcessor {
|
|||||||
int oldRows = screen.getRows();
|
int oldRows = screen.getRows();
|
||||||
int oldCols = screen.getCols();
|
int oldCols = screen.getCols();
|
||||||
screen.erase(false);
|
screen.erase(false);
|
||||||
|
graphicsPlane.clear();
|
||||||
processWrite(data, offset, length, true);
|
processWrite(data, offset, length, true);
|
||||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||||
notifyScreenSizeChanged();
|
notifyScreenSizeChanged();
|
||||||
@@ -105,6 +131,7 @@ public class DataStreamProcessor {
|
|||||||
int oldRows = screen.getRows();
|
int oldRows = screen.getRows();
|
||||||
int oldCols = screen.getCols();
|
int oldCols = screen.getCols();
|
||||||
screen.erase(true);
|
screen.erase(true);
|
||||||
|
graphicsPlane.clear();
|
||||||
processWrite(data, offset, length, true);
|
processWrite(data, offset, length, true);
|
||||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||||
notifyScreenSizeChanged();
|
notifyScreenSizeChanged();
|
||||||
@@ -451,8 +478,8 @@ public class DataStreamProcessor {
|
|||||||
pos += 4;
|
pos += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill from current position to target
|
// Fill from current position to target (do-while wraps entire buffer if baddr == toAddr)
|
||||||
while (baddr != toAddr) {
|
do {
|
||||||
ExtendedAttribute ea = screen.getCell(baddr);
|
ExtendedAttribute ea = screen.getCell(baddr);
|
||||||
ea.fa = 0; // Destroy previous field attribute if any
|
ea.fa = 0; // Destroy previous field attribute if any
|
||||||
ea.ec = (byte) fillChar;
|
ea.ec = (byte) fillChar;
|
||||||
@@ -461,7 +488,7 @@ public class DataStreamProcessor {
|
|||||||
ea.gr = (byte) currentGr;
|
ea.gr = (byte) currentGr;
|
||||||
ea.cs = fillCs;
|
ea.cs = fillCs;
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
}
|
} while (baddr != toAddr);
|
||||||
screen.setBufferAddress(baddr);
|
screen.setBufferAddress(baddr);
|
||||||
lastWasOrder = true;
|
lastWasOrder = true;
|
||||||
break;
|
break;
|
||||||
@@ -476,7 +503,7 @@ public class DataStreamProcessor {
|
|||||||
if (toAddr >= size)
|
if (toAddr >= size)
|
||||||
toAddr = toAddr % size;
|
toAddr = toAddr % size;
|
||||||
|
|
||||||
while (baddr != toAddr) {
|
do {
|
||||||
ExtendedAttribute ea = screen.getCell(baddr);
|
ExtendedAttribute ea = screen.getCell(baddr);
|
||||||
if (!ea.isFieldAttribute()) {
|
if (!ea.isFieldAttribute()) {
|
||||||
int faAddr = screen.findFieldAttribute(baddr);
|
int faAddr = screen.findFieldAttribute(baddr);
|
||||||
@@ -491,7 +518,7 @@ public class DataStreamProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
}
|
} while (baddr != toAddr);
|
||||||
screen.setBufferAddress(baddr);
|
screen.setBufferAddress(baddr);
|
||||||
pos += 3;
|
pos += 3;
|
||||||
lastWasOrder = true;
|
lastWasOrder = true;
|
||||||
@@ -637,7 +664,13 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
outputPos = 0;
|
outputPos = 0;
|
||||||
|
|
||||||
int aid = AID_NO; // Last AID
|
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||||
|
? inputProcessor.getLastAid()
|
||||||
|
: AID_NO;
|
||||||
|
if (inputProcessor != null) {
|
||||||
|
inputProcessor.setLastAid(AID_NO);
|
||||||
|
}
|
||||||
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
|
||||||
// AID byte
|
// AID byte
|
||||||
@@ -649,31 +682,56 @@ public class DataStreamProcessor {
|
|||||||
outputWrite(caddr[1] & 0xFF);
|
outputWrite(caddr[1] & 0xFF);
|
||||||
|
|
||||||
if (!screen.isFormatted()) {
|
if (!screen.isFormatted()) {
|
||||||
// Unformatted: send everything
|
// Unformatted screen: send data up to last non-null
|
||||||
if (all) {
|
int lastNonNull = -1;
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = size - 1; i >= 0; i--) {
|
||||||
outputWrite(screen.getCell(i).ec & 0xFF);
|
if (screen.getCell(i).ec != 0) {
|
||||||
|
lastNonNull = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastNonNull >= 0) {
|
||||||
|
for (int i = 0; i <= lastNonNull; i++) {
|
||||||
|
int b = screen.getCell(i).ec & 0xFF;
|
||||||
|
outputWrite(b != 0 ? b : 0x40);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Formatted: send modified fields
|
// Formatted screen: send modified fields with trailing NULLs stripped
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
ExtendedAttribute ea = screen.getCell(i);
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
||||||
int fieldStart = (i + 1) % size;
|
int fieldStart = (i + 1) % size;
|
||||||
// Send SBA for field start
|
|
||||||
|
// Collect field data and find last non-null byte
|
||||||
|
ByteArrayOutputStream fieldData = new ByteArrayOutputStream();
|
||||||
|
int pos = fieldStart;
|
||||||
|
int lastNonNull = -1;
|
||||||
|
int fieldLen = 0;
|
||||||
|
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||||
|
int b = screen.getCell(pos).ec & 0xFF;
|
||||||
|
fieldData.write(b);
|
||||||
|
if (b != 0x00) {
|
||||||
|
lastNonNull = fieldLen;
|
||||||
|
}
|
||||||
|
fieldLen++;
|
||||||
|
pos = (pos + 1) % size;
|
||||||
|
if (pos == fieldStart)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always send SBA and address
|
||||||
outputWrite(ORDER_SBA);
|
outputWrite(ORDER_SBA);
|
||||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||||
outputWrite(addr[0] & 0xFF);
|
outputWrite(addr[0] & 0xFF);
|
||||||
outputWrite(addr[1] & 0xFF);
|
outputWrite(addr[1] & 0xFF);
|
||||||
|
|
||||||
// Send field contents until next FA
|
// Send field data (strip trailing nulls)
|
||||||
int pos = fieldStart;
|
if (lastNonNull >= 0) {
|
||||||
while (pos < size && !screen.getCell(pos).isFieldAttribute()) {
|
byte[] allData = fieldData.toByteArray();
|
||||||
outputWrite(screen.getCell(pos).ec & 0xFF);
|
for (int k = 0; k <= lastNonNull; k++) {
|
||||||
pos = (pos + 1) % size;
|
outputWrite(allData[k] & 0xFF);
|
||||||
if (pos == fieldStart)
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -709,6 +767,7 @@ public class DataStreamProcessor {
|
|||||||
if (fieldLen >= 4) {
|
if (fieldLen >= 4) {
|
||||||
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
|
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
|
||||||
screen.erase(alt);
|
screen.erase(alt);
|
||||||
|
graphicsPlane.clear();
|
||||||
notifyScreenSizeChanged();
|
notifyScreenSizeChanged();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -719,6 +778,7 @@ public class DataStreamProcessor {
|
|||||||
break;
|
break;
|
||||||
case SF_CREATE_PART:
|
case SF_CREATE_PART:
|
||||||
// Acknowledged — we use implicit partition
|
// Acknowledged — we use implicit partition
|
||||||
|
graphicsPlane.clear();
|
||||||
break;
|
break;
|
||||||
case SF_OUTBOUND_DS:
|
case SF_OUTBOUND_DS:
|
||||||
if (fieldLen > 5) {
|
if (fieldLen > 5) {
|
||||||
@@ -733,6 +793,83 @@ public class DataStreamProcessor {
|
|||||||
log.fine("SF_TRANSFER_DATA received but ftDft is null");
|
log.fine("SF_TRANSFER_DATA received but ftDft is null");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_LOADPS_DIRECT: // 0x06: Load Programmed Symbols direct
|
||||||
|
if (fieldLen > 3) {
|
||||||
|
byte[] psData = new byte[fieldLen - 3];
|
||||||
|
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
||||||
|
programSymbolManager.loadps(psData);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
||||||
|
if (fieldLen >= 4) {
|
||||||
|
int sfSubId = data[pos + 3] & 0xFF;
|
||||||
|
switch (sfSubId) {
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
||||||
|
if (fieldLen > 4) {
|
||||||
|
byte[] psData = new byte[fieldLen - 4];
|
||||||
|
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
||||||
|
programSymbolManager.loadps(psData);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: // 0x10: Object Picture (Picture segments)
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw orders)
|
||||||
|
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
||||||
|
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
||||||
|
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
||||||
|
|
||||||
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
|
||||||
|
if (flags == 0x80) { // SPAN_FIRST
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
currentGocaSubtype = sfSubId;
|
||||||
|
if (orderLen > 0) {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
} else if (flags == 0x00) { // SPAN_MIDDLE
|
||||||
|
if (orderLen > 0) {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
} else if (flags == 0x40) { // SPAN_LAST
|
||||||
|
if (orderLen > 0) {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
if (currentGocaSubtype == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||||
|
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||||
|
} else {
|
||||||
|
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||||
|
}
|
||||||
|
notifyScreenUpdated();
|
||||||
|
} else { // SPAN_ONLY (0xC0)
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
if (sfSubId == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||||
|
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||||
|
} else {
|
||||||
|
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||||
|
case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
|
||||||
|
if (fieldLen > 3) {
|
||||||
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
|
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
|
||||||
break;
|
break;
|
||||||
@@ -855,4 +992,10 @@ public class DataStreamProcessor {
|
|||||||
l.onScreenSizeChanged(screen.getRows(), screen.getCols());
|
l.onScreenSizeChanged(screen.getRows(), screen.getCols());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyScreenUpdated() {
|
||||||
|
for (ScreenUpdateListener l : screenListeners) {
|
||||||
|
l.onScreenUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package org.lib3270j.datastream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.lib3270j.graphics.GraphicsMode;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import org.lib3270j.screen.ScreenBuffer;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||||
* Equivalent to the do_qr_* functions in sf.c.
|
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||||
*/
|
*/
|
||||||
public class QueryReplyBuilder {
|
public class QueryReplyBuilder {
|
||||||
|
|
||||||
@@ -21,9 +22,10 @@ public class QueryReplyBuilder {
|
|||||||
private static final int Yr_3279_2 = 0x0002006f;
|
private static final int Yr_3279_2 = 0x0002006f;
|
||||||
|
|
||||||
private final ScreenBuffer screen;
|
private final ScreenBuffer screen;
|
||||||
|
private GraphicsMode graphicsMode = GraphicsMode.NONE;
|
||||||
|
|
||||||
// Supported query reply codes (must match what we send in summary)
|
// Base query reply codes (text mode)
|
||||||
private static final int[] SUPPORTED_QR = {
|
private static final int[] SUPPORTED_QR_BASE = {
|
||||||
QR_SUMMARY, // 0x80 — summary must list itself
|
QR_SUMMARY, // 0x80 — summary must list itself
|
||||||
QR_USABLE_AREA, // 0x81
|
QR_USABLE_AREA, // 0x81
|
||||||
QR_ALPHA_PART, // 0x84
|
QR_ALPHA_PART, // 0x84
|
||||||
@@ -35,10 +37,45 @@ public class QueryReplyBuilder {
|
|||||||
QR_IMP_PART, // 0xa6
|
QR_IMP_PART, // 0xa6
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Vector graphics query reply codes matching HOD DS3270.java line 1723
|
||||||
|
private static final int[] SUPPORTED_QR_VECTOR = {
|
||||||
|
QR_SUMMARY, // 0x80
|
||||||
|
QR_USABLE_AREA, // 0x81
|
||||||
|
QR_ALPHA_PART, // 0x84
|
||||||
|
QR_CHARSETS, // 0x85
|
||||||
|
QR_COLOR, // 0x86
|
||||||
|
QR_HIGHLIGHTING, // 0x87
|
||||||
|
QR_REPLY_MODES, // 0x88
|
||||||
|
QR_SAVE_RESTORE, // 0x8c
|
||||||
|
QR_DDM, // 0x95
|
||||||
|
QR_TRANSPARENCY, // 0x99
|
||||||
|
QR_IMP_PART, // 0xa6
|
||||||
|
QR_RPQ_NAMES, // 0xa8
|
||||||
|
QR_GRAPHICS, // 0xb0
|
||||||
|
QR_GIMAGE, // 0xb1
|
||||||
|
QR_AUX_DEV, // 0xb2
|
||||||
|
QR_OEM_FMT, // 0xb3
|
||||||
|
QR_GCOLOR, // 0xb4
|
||||||
|
QR_GSYMBOLS, // 0xb6
|
||||||
|
};
|
||||||
|
|
||||||
public QueryReplyBuilder(ScreenBuffer screen) {
|
public QueryReplyBuilder(ScreenBuffer screen) {
|
||||||
this.screen = screen;
|
this.screen = screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QueryReplyBuilder(ScreenBuffer screen, GraphicsMode graphicsMode) {
|
||||||
|
this.screen = screen;
|
||||||
|
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GraphicsMode getGraphicsMode() {
|
||||||
|
return graphicsMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGraphicsMode(GraphicsMode graphicsMode) {
|
||||||
|
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build all query replies as a single AID_SF + structured field response.
|
* Build all query replies as a single AID_SF + structured field response.
|
||||||
*/
|
*/
|
||||||
@@ -75,7 +112,20 @@ public class QueryReplyBuilder {
|
|||||||
// Implicit Partition
|
// Implicit Partition
|
||||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||||
|
|
||||||
log.info("Built " + out.size() + " bytes of all query replies");
|
// Vector Graphics QRs if enabled
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||||
|
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||||
|
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames());
|
||||||
|
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
|
||||||
|
appendQueryReply(out, QR_GIMAGE, buildGImage());
|
||||||
|
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
||||||
|
appendOemFmt(out);
|
||||||
|
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
||||||
|
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,14 +166,75 @@ public class QueryReplyBuilder {
|
|||||||
case QR_REPLY_MODES:
|
case QR_REPLY_MODES:
|
||||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||||
break;
|
break;
|
||||||
|
case QR_SAVE_RESTORE:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case QR_DDM:
|
case QR_DDM:
|
||||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||||
break;
|
break;
|
||||||
|
case QR_TRANSPARENCY:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case QR_IMP_PART:
|
case QR_IMP_PART:
|
||||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||||
break;
|
break;
|
||||||
|
case QR_RPQ_NAMES:
|
||||||
case QR_RPQNAMES:
|
case QR_RPQNAMES:
|
||||||
appendQueryReply(out, QR_RPQNAMES, new byte[0]);
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QR_GRAPHICS:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QR_GIMAGE:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_GIMAGE, buildGImage());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QR_AUX_DEV:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QR_OEM_FMT:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendOemFmt(out);
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QR_GCOLOR:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QR_GSYMBOLS:
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
||||||
|
} else {
|
||||||
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unsupported query reply code — emit QR_NULL
|
// Unsupported query reply code — emit QR_NULL
|
||||||
@@ -148,7 +259,8 @@ public class QueryReplyBuilder {
|
|||||||
|
|
||||||
private byte[] buildSummary() {
|
private byte[] buildSummary() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
for (int code : SUPPORTED_QR) {
|
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||||
|
for (int code : codes) {
|
||||||
out.write(code);
|
out.write(code);
|
||||||
}
|
}
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
@@ -184,66 +296,81 @@ public class QueryReplyBuilder {
|
|||||||
private byte[] buildAlphaPartitions(int maxRows) {
|
private byte[] buildAlphaPartitions(int maxRows) {
|
||||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||||
out.write(0x00); // max partitions (1 partition)
|
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
|
||||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||||
out.write(bufSize & 0xFF); // total partition storage low
|
out.write(bufSize & 0xFF); // total partition storage low
|
||||||
out.write(0x00); // no special features
|
out.write(0x00); // flags
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildCharsets() {
|
private byte[] buildCharsets() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(32);
|
if (graphicsMode.isVectorGraphicsEnabled() || graphicsMode == GraphicsMode.NONE) {
|
||||||
|
// Standard 3179G / Base character sets (matches HOD QR_CHARSETS_S_STRING, 27 bytes total)
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||||
out.write(0x82); // flags: GE, CGCSGID present
|
out.write(0x82); // flags: GE, CGCSGID present
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
out.write(SW_3279_2); // SDW - default char width
|
out.write(SW_3279_2); // SDW - default char width (9)
|
||||||
out.write(SH_3279_2); // SDH - default char height
|
out.write(14); // SDH - default char height (14)
|
||||||
out.write(0x00); // Load PS format types supported: none
|
out.write(0x00); // LoadPS format (0x00)
|
||||||
|
out.write(0x00);
|
||||||
|
out.write(0x00);
|
||||||
|
out.write(0x00);
|
||||||
|
out.write(0x07); // DL = 7
|
||||||
|
// Set 0 (Base EBCDIC)
|
||||||
|
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4);
|
||||||
|
// Set 1 (APL/Text)
|
||||||
|
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Programmed Symbols mode (3279 PS)
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||||
|
out.write(0x82); // flags: GE, CGCSGID present
|
||||||
|
out.write(0x00); // more flags
|
||||||
|
out.write(SW_3279_2); // SDW (9)
|
||||||
|
out.write(14); // SDH (14)
|
||||||
|
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
||||||
out.write(0x00); // Load PS device type (high)
|
out.write(0x00); // Load PS device type (high)
|
||||||
out.write(0x00); // Load PS device type (low)
|
out.write(0x00); // Load PS device type (low)
|
||||||
out.write(0x00); // reserved
|
out.write(0x00); // reserved
|
||||||
out.write(0x07); // DL = 7 bytes per descriptor (non-DBCS)
|
out.write(0x07); // DL = 7 bytes per descriptor
|
||||||
// Descriptor 1 (SET 0): default character set
|
// Descriptor 1 (SET 0): default character set
|
||||||
out.write(0x00); // SET 0
|
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4);
|
||||||
out.write(0x10); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
|
||||||
out.write(0x00); // LCID 0
|
|
||||||
out.write(0x02); // CGCSGID (4 bytes) = 0x02b90025 (CGEN|CSET)
|
|
||||||
out.write(0xb9);
|
|
||||||
out.write(0x00);
|
|
||||||
out.write(0x25);
|
|
||||||
// Descriptor 2 (SET 1): APL/GE character set
|
// Descriptor 2 (SET 1): APL/GE character set
|
||||||
out.write(0x01); // SET 1
|
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||||
out.write(0x00); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3)
|
||||||
out.write(0xf1); // LCID 0xf1
|
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
out.write(0x03); // CGCSGID: 3179-style APL2 = 0x03c30136
|
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
out.write(0xc3);
|
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7)
|
||||||
out.write(0x01);
|
out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
out.write(0x36);
|
out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
|
out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
|
out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildColor() {
|
private byte[] buildColor() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(36);
|
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
||||||
int colorMax = 16;
|
return new byte[] {
|
||||||
out.write(0x00); // no options
|
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||||
out.write(colorMax); // number of colors
|
(byte) 0xF1, (byte) 0xF1, // Blue
|
||||||
out.write(0x00); // default color pair: attribute
|
(byte) 0xF2, (byte) 0xF2, // Red
|
||||||
out.write(0xf0 + HOST_COLOR_GREEN); // default color: green
|
(byte) 0xF3, (byte) 0xF3, // Pink
|
||||||
for (int i = 0xf1; i < 0xf1 + colorMax - 1; i++) {
|
(byte) 0xF4, (byte) 0xF4, // Green
|
||||||
out.write(i); // color attribute value
|
(byte) 0xF5, (byte) 0xF5, // Turquoise
|
||||||
out.write(i); // maps to itself (color mode)
|
(byte) 0xF6, (byte) 0xF6, // Yellow
|
||||||
}
|
(byte) 0xF7, (byte) 0xF7 // Neutral/White
|
||||||
return out.toByteArray();
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildHighlighting() {
|
private byte[] buildHighlighting() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(11);
|
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
||||||
out.write(5); // 5 pairs
|
return new byte[] {
|
||||||
out.write(XAH_DEFAULT); out.write(XAH_NORMAL);
|
0x04, 0x00, (byte) 0xF0,
|
||||||
out.write(XAH_BLINK); out.write(XAH_BLINK);
|
(byte) 0xF1, (byte) 0xF1,
|
||||||
out.write(XAH_REVERSE); out.write(XAH_REVERSE);
|
(byte) 0xF2, (byte) 0xF2,
|
||||||
out.write(XAH_UNDERSCORE); out.write(XAH_UNDERSCORE);
|
(byte) 0xF4, (byte) 0xF4
|
||||||
out.write(XAH_INTENSIFY); out.write(XAH_INTENSIFY);
|
};
|
||||||
return out.toByteArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildReplyModes() {
|
private byte[] buildReplyModes() {
|
||||||
@@ -267,18 +394,18 @@ public class QueryReplyBuilder {
|
|||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||||
// Implicit partition sizes, 2 self-defining parameters
|
// Implicit partition sizes, 2 self-defining parameters
|
||||||
|
|
||||||
// SDP 1: Default screen size
|
// SDP 1: Default screen size (Model 2: 80x24)
|
||||||
out.write(0x00); // flags
|
out.write(0x00); // flags
|
||||||
out.write(0x00); // reserved
|
out.write(0x00); // reserved
|
||||||
out.write(0x0b); // SDP length
|
out.write(0x0b); // SDP length (11 bytes)
|
||||||
out.write(0x01); // SDP type: implicit partition sizes
|
out.write(0x01); // SDP type: implicit partition sizes
|
||||||
out.write(0x00); // reserved
|
out.write(0x00); // reserved
|
||||||
// Default
|
// Default size
|
||||||
out.write((MODEL_2_COLS >> 8) & 0xFF);
|
out.write((MODEL_2_COLS >> 8) & 0xFF);
|
||||||
out.write(MODEL_2_COLS & 0xFF);
|
out.write(MODEL_2_COLS & 0xFF);
|
||||||
out.write((MODEL_2_ROWS >> 8) & 0xFF);
|
out.write((MODEL_2_ROWS >> 8) & 0xFF);
|
||||||
out.write(MODEL_2_ROWS & 0xFF);
|
out.write(MODEL_2_ROWS & 0xFF);
|
||||||
// Alternate
|
// Alternate size (Model 4: 80x43, Model 5: 132x27, etc.)
|
||||||
out.write((maxCols >> 8) & 0xFF);
|
out.write((maxCols >> 8) & 0xFF);
|
||||||
out.write(maxCols & 0xFF);
|
out.write(maxCols & 0xFF);
|
||||||
out.write((maxRows >> 8) & 0xFF);
|
out.write((maxRows >> 8) & 0xFF);
|
||||||
@@ -286,4 +413,80 @@ public class QueryReplyBuilder {
|
|||||||
|
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] buildGraphics() {
|
||||||
|
return new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildGImage() {
|
||||||
|
return new byte[]{
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
|
||||||
|
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||||
|
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildAuxDev() {
|
||||||
|
return new byte[]{
|
||||||
|
0x00, 0x09, 0x00, 0x07,
|
||||||
|
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||||
|
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildSaveRestore() {
|
||||||
|
// HOD DS3270.java line 1768: 6 bytes payload
|
||||||
|
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildTransparency() {
|
||||||
|
// HOD DS3270.java line 1782: 2 bytes payload
|
||||||
|
return new byte[]{ 0x00, 0x00 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildRpqNames() {
|
||||||
|
// HOD DS3270.java line 1798: 5 bytes payload
|
||||||
|
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendOemFmt(ByteArrayOutputStream out) {
|
||||||
|
// HOD DS3270.java line 1814: 4 distinct OEM format sub-fields
|
||||||
|
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||||
|
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||||
|
});
|
||||||
|
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||||
|
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
||||||
|
});
|
||||||
|
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||||
|
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||||
|
});
|
||||||
|
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||||
|
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildGColor() {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||||
|
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
|
||||||
|
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
out.write(0x00);
|
||||||
|
out.write(i);
|
||||||
|
java.awt.Color c = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||||
|
out.write(c.getRed());
|
||||||
|
out.write(c.getGreen());
|
||||||
|
out.write(c.getBlue());
|
||||||
|
out.write(0x00); // 6th byte in HOD color table
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildGSymbols() {
|
||||||
|
return new byte[]{
|
||||||
|
0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||||
|
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
|
||||||
|
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture)
|
||||||
|
* vector graphics and Programmed Symbols (PS).
|
||||||
|
*/
|
||||||
|
public final class GocaConstants {
|
||||||
|
|
||||||
|
private GocaConstants() {}
|
||||||
|
|
||||||
|
// Structured Field IDs
|
||||||
|
public static final int SF_LOADPS = 0x0F; // 2-byte Structured Field prefix (or 0x06 for direct Load PS)
|
||||||
|
public static final int SF_LOADPS_DIRECT = 0x06; // Load Programmed Symbols direct SFID
|
||||||
|
public static final int SF_OBJCNTL = 0x24; // Object Control
|
||||||
|
public static final int SF_OBJDATA = 0x85; // Graphics Object Data
|
||||||
|
public static final int SF_3270_G = 0x20; // 3270 Graphics / Picture
|
||||||
|
|
||||||
|
// Structured Field Sub-IDs (for SF 0x0F)
|
||||||
|
public static final int SF_LOADPS_SUB = 0x06; // Load Programmed Symbols
|
||||||
|
public static final int SF_LOADLT_SUB = 0x07; // Load Line Type / Symbol Set
|
||||||
|
public static final int SF_OBJDATA_SUB = 0x0F; // Graphics Object Data (GOCA draw orders)
|
||||||
|
public static final int SF_OBJPICT_SUB = 0x10; // Graphics Object Picture (Segment draw orders)
|
||||||
|
public static final int SF_OBJCNTL_SUB = 0x11; // Graphics Object Control (Procedure orders)
|
||||||
|
|
||||||
|
// Procedure Orders (for SF_OBJCNTL_SUB 0x11)
|
||||||
|
public static final int P_NOP1 = 0x00; // Procedure NOOP
|
||||||
|
public static final int P_COMT = 0x01; // Procedure Comment
|
||||||
|
public static final int P_ATTCUR = 0x08; // Attach Graphic Cursor
|
||||||
|
public static final int P_DETCUR = 0x09; // Detach Graphic Cursor
|
||||||
|
public static final int P_ERASE = 0x0A; // Erase Graphics Presentation Space
|
||||||
|
public static final int P_STOPDR = 0x0F; // Stop Draw
|
||||||
|
public static final int P_SCUDEF = 0x21; // Set Current Defaults
|
||||||
|
public static final int P_BEGPROC = 0x30; // Begin Procedure
|
||||||
|
public static final int P_SETCUR = 0x31; // Set Graphic Cursor Position
|
||||||
|
|
||||||
|
// Coordinate space
|
||||||
|
public static final int VIRTUAL_COORD_MAX = 4096;
|
||||||
|
|
||||||
|
// GOCA Drawing / Segment Orders
|
||||||
|
public static final int G_NOP1 = 0x00; // NOOP 1-byte
|
||||||
|
public static final int G_COMT = 0x01; // Comment
|
||||||
|
public static final int G_GSMC = 0x07; // Set Marker Color
|
||||||
|
public static final int G_GSPS = 0x08; // Set Pattern Set
|
||||||
|
public static final int G_GSCOL = 0x0A; // Set Color
|
||||||
|
public static final int G_GSMX = 0x0C; // Set Foreground Mix
|
||||||
|
public static final int G_GSBMX = 0x0D; // Set Background Mix
|
||||||
|
public static final int G_GSFLW = 0x11; // Set Fractional Line Width
|
||||||
|
public static final int G_GSLT = 0x18; // Set Line Type
|
||||||
|
public static final int G_GSLW = 0x19; // Set Line Width
|
||||||
|
public static final int G_GSMS = 0x1B; // Set Marker Size
|
||||||
|
public static final int G_GSCP = 0x21; // Set Current Position
|
||||||
|
public static final int G_GSAP = 0x22; // Arc Parameters
|
||||||
|
public static final int G_GSECOL = 0x26; // Set Extended Color
|
||||||
|
public static final int G_GSVW = 0x27; // Set Viewing Window
|
||||||
|
public static final int G_GSPT = 0x28; // Set Pattern Symbol
|
||||||
|
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type
|
||||||
|
public static final int G_GSCH = 0x33; // Set Character Cell
|
||||||
|
public static final int G_GSCA = 0x34; // Set Character Angle
|
||||||
|
public static final int G_GSCR = 0x35; // Set Character Shear
|
||||||
|
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
||||||
|
public static final int G_GSCS = 0x38; // Set Character Set
|
||||||
|
public static final int G_GSMP = 0x39; // Set Marker Precision
|
||||||
|
public static final int G_GSCD = 0x3A; // Set Character Direction
|
||||||
|
public static final int G_GSCC = 0x3B; // Set Character Precision
|
||||||
|
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
|
||||||
|
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
|
||||||
|
public static final int G_GPOP = 0x3F; // Pop Attribute
|
||||||
|
public static final int G_GEAR = 0x60; // End Area
|
||||||
|
public static final int G_GBAR = 0x68; // Begin Area
|
||||||
|
public static final int G_BEGSEGM = 0x70; // Begin Segment
|
||||||
|
public static final int G_ENDSEGM = 0x71; // End Segment
|
||||||
|
public static final int G_GERASE = 0x7E; // Erase Graphics Plane
|
||||||
|
public static final int G_GCLINE = 0x81; // Line at Current Position
|
||||||
|
public static final int G_GCMRK = 0x82; // Marker at Current Position
|
||||||
|
public static final int G_GCCHST = 0x83; // Character String at Current Position
|
||||||
|
public static final int G_GCFLT = 0x85; // Fillet at Current Position
|
||||||
|
public static final int G_GCARC = 0x86; // Partial Arc at Current Position
|
||||||
|
public static final int G_GCFARC = 0x87; // Full Arc at Current Position
|
||||||
|
public static final int G_GEIMG = 0x91; // End Image
|
||||||
|
public static final int G_GIMD = 0x92; // Image Data
|
||||||
|
public static final int G_GCRLIN = 0xA1; // Relative Line at Current Position
|
||||||
|
public static final int G_GLINE = 0xC1; // Line (Absolute)
|
||||||
|
public static final int G_GMRK = 0xC2; // Marker (Absolute)
|
||||||
|
public static final int G_GCHST = 0xC3; // Character String (Absolute)
|
||||||
|
public static final int G_GFLT = 0xC5; // Fillet (Absolute)
|
||||||
|
public static final int G_GARC = 0xC6; // Partial Arc (Absolute)
|
||||||
|
public static final int G_GFARC = 0xC7; // Full Arc (Absolute)
|
||||||
|
public static final int G_GBIMG = 0xD1; // Begin Image
|
||||||
|
public static final int G_GRLINE = 0xE1; // Relative Line (Absolute Start)
|
||||||
|
|
||||||
|
// Line Types (GSLT)
|
||||||
|
public static final int LT_DEFAULT = 0;
|
||||||
|
public static final int LT_DOT = 1;
|
||||||
|
public static final int LT_SHORTDASH= 2;
|
||||||
|
public static final int LT_DASHDOT = 3;
|
||||||
|
public static final int LT_DOUBLEDOT= 4;
|
||||||
|
public static final int LT_LONGDASH = 5;
|
||||||
|
public static final int LT_DASHDOUBLEDOT = 6;
|
||||||
|
public static final int LT_SOLID = 7;
|
||||||
|
|
||||||
|
// Line Widths (GSLW)
|
||||||
|
public static final int LW_DEFAULT = 0;
|
||||||
|
public static final int LW_NORMAL = 1;
|
||||||
|
public static final int LW_THICK = 2;
|
||||||
|
|
||||||
|
// Fill Patterns (GSPT)
|
||||||
|
public static final int PT_DEFAULT = 0;
|
||||||
|
public static final int PT_D1 = 1;
|
||||||
|
public static final int PT_D2 = 2;
|
||||||
|
public static final int PT_D3 = 3;
|
||||||
|
public static final int PT_D4 = 4;
|
||||||
|
public static final int PT_D5 = 5;
|
||||||
|
public static final int PT_D6 = 6;
|
||||||
|
public static final int PT_D7 = 7;
|
||||||
|
public static final int PT_D8 = 8;
|
||||||
|
public static final int PT_VERT_LINE = 9;
|
||||||
|
public static final int PT_HORIZ_LINE = 10;
|
||||||
|
public static final int PT_DIAG_BLTR = 11;
|
||||||
|
public static final int PT_DIAG_BLTR2 = 12;
|
||||||
|
public static final int PT_DIAG_TLBR = 13;
|
||||||
|
public static final int PT_DIAG_TLBR2 = 14;
|
||||||
|
public static final int PT_EMPTY = 15;
|
||||||
|
public static final int PT_SOLID = 16;
|
||||||
|
|
||||||
|
// Marker Symbols (GSMT)
|
||||||
|
public static final int MK_DEFAULT = 0;
|
||||||
|
public static final int MK_CROSS = 1; // x
|
||||||
|
public static final int MK_PLUS = 2; // +
|
||||||
|
public static final int MK_DIAMOND = 3; // <>
|
||||||
|
public static final int MK_SQUARE = 4; // []
|
||||||
|
public static final int MK_6STAR = 5; // * 6-point
|
||||||
|
public static final int MK_8STAR = 6; // * 8-point
|
||||||
|
public static final int MK_SDIAMOND = 7; // solid diamond
|
||||||
|
public static final int MK_SSQUARE = 8; // solid square
|
||||||
|
public static final int MK_DOT = 9; // .
|
||||||
|
public static final int MK_CIRCLE = 10;// o
|
||||||
|
|
||||||
|
// Character Direction (GSCD)
|
||||||
|
public static final int CD_DEFAULT = 0;
|
||||||
|
public static final int CD_LR = 1; // Left to Right
|
||||||
|
public static final int CD_TB = 2; // Top to Bottom
|
||||||
|
public static final int CD_RL = 3; // Right to Left
|
||||||
|
public static final int CD_BT = 4; // Bottom to Top
|
||||||
|
|
||||||
|
// Foreground / Background Mix Modes (GSMX / GSBMX)
|
||||||
|
public static final int MIX_DEFAULT = 0;
|
||||||
|
public static final int MIX_OR = 1;
|
||||||
|
public static final int MIX_OVER = 2;
|
||||||
|
public static final int MIX_LEAVE = 3;
|
||||||
|
public static final int MIX_XOR = 4;
|
||||||
|
public static final int MIX_UNDER = 5;
|
||||||
|
|
||||||
|
// Graphic Colors (IBM 3179G / HOD 16-color table)
|
||||||
|
public static final java.awt.Color[] GOCA_COLORS = new java.awt.Color[] {
|
||||||
|
new java.awt.Color(0, 255, 0), // 0: Default (Green)
|
||||||
|
new java.awt.Color(120, 144, 240), // 1: Blue
|
||||||
|
new java.awt.Color(255, 0, 0), // 2: Red
|
||||||
|
new java.awt.Color(255, 0, 255), // 3: Pink / Magenta
|
||||||
|
new java.awt.Color(0, 255, 0), // 4: Green
|
||||||
|
new java.awt.Color(0, 255, 255), // 5: Turquoise / Cyan
|
||||||
|
new java.awt.Color(255, 255, 0), // 6: Yellow
|
||||||
|
new java.awt.Color(255, 255, 255), // 7: Neutral White
|
||||||
|
new java.awt.Color(0, 0, 0), // 8: Black
|
||||||
|
new java.awt.Color(0, 0, 128), // 9: Deep Blue
|
||||||
|
new java.awt.Color(128, 0, 0), // 10: Orange / Dark Red
|
||||||
|
new java.awt.Color(128, 0, 128), // 11: Purple
|
||||||
|
new java.awt.Color(0, 128, 0), // 12: Pale Green
|
||||||
|
new java.awt.Color(0, 128, 128), // 13: Pale Cyan
|
||||||
|
new java.awt.Color(215, 151, 0), // 14: Mustard
|
||||||
|
new java.awt.Color(192, 192, 192), // 15: Grey / Light White
|
||||||
|
new java.awt.Color(73, 36, 0) // 16: Brown
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,819 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.lib3270j.charset.EbcdicTranslator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interprets GOCA (Graphics Object Content Architecture) drawing orders
|
||||||
|
* and updates the GraphicsPlane.
|
||||||
|
*/
|
||||||
|
public class GocaDecoder {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(GocaDecoder.class.getName());
|
||||||
|
|
||||||
|
private final GraphicsPlane plane;
|
||||||
|
|
||||||
|
// Drawing state
|
||||||
|
private int curX = 0;
|
||||||
|
private int curY = 0;
|
||||||
|
private Color curColor = Color.GREEN;
|
||||||
|
private int lineType = GocaConstants.LT_SOLID;
|
||||||
|
private int lineWidth = GocaConstants.LW_NORMAL;
|
||||||
|
private int markerType = GocaConstants.MK_PLUS;
|
||||||
|
private int markerSize = 5;
|
||||||
|
private Color markerColor = Color.GREEN;
|
||||||
|
private int pattern = GocaConstants.PT_SOLID;
|
||||||
|
private Color fillColor = Color.GREEN;
|
||||||
|
private int charDir = GocaConstants.CD_LR;
|
||||||
|
private double charAngle = 0.0;
|
||||||
|
private int charWidth = 9;
|
||||||
|
private int charHeight = 16;
|
||||||
|
private int charSet = 0;
|
||||||
|
private int arcParamP = 1;
|
||||||
|
private int arcParamQ = 1;
|
||||||
|
private int arcParamR = 0;
|
||||||
|
private int arcParamS = 0;
|
||||||
|
|
||||||
|
private ProgramSymbolManager programSymbolManager;
|
||||||
|
|
||||||
|
// Area accumulation
|
||||||
|
private boolean inArea = false;
|
||||||
|
private boolean areaDrawBoundary = true;
|
||||||
|
private final List<Integer> areaPointsX = new ArrayList<>();
|
||||||
|
private final List<Integer> areaPointsY = new ArrayList<>();
|
||||||
|
|
||||||
|
// Image accumulation
|
||||||
|
private boolean inImage = false;
|
||||||
|
private int imgX = 0;
|
||||||
|
private int imgY = 0;
|
||||||
|
private int imgWidth = 0;
|
||||||
|
private int imgHeight = 0;
|
||||||
|
private final List<Byte> imgBuffer = new ArrayList<>();
|
||||||
|
|
||||||
|
public GocaDecoder(GraphicsPlane plane) {
|
||||||
|
this.plane = plane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||||
|
this.programSymbolManager = psm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GraphicsPlane getGraphicsPlane() {
|
||||||
|
return plane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurX() {
|
||||||
|
return curX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurY() {
|
||||||
|
return curY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void resetDefaults() {
|
||||||
|
curX = 0;
|
||||||
|
curY = 0;
|
||||||
|
resetAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void resetAttributes() {
|
||||||
|
curColor = getColor(0);
|
||||||
|
lineType = GocaConstants.LT_SOLID;
|
||||||
|
lineWidth = GocaConstants.LW_NORMAL;
|
||||||
|
markerType = GocaConstants.MK_PLUS;
|
||||||
|
markerSize = 5;
|
||||||
|
markerColor = curColor;
|
||||||
|
pattern = GocaConstants.PT_SOLID;
|
||||||
|
fillColor = curColor;
|
||||||
|
charDir = GocaConstants.CD_LR;
|
||||||
|
charAngle = 0.0;
|
||||||
|
charSet = 0;
|
||||||
|
inArea = false;
|
||||||
|
areaPointsX.clear();
|
||||||
|
areaPointsY.clear();
|
||||||
|
inImage = false;
|
||||||
|
imgBuffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] partialOrderBuffer = new byte[0];
|
||||||
|
|
||||||
|
private int getOrderLength(byte[] data, int idx, int end) {
|
||||||
|
int order = data[idx] & 0xFF;
|
||||||
|
if (order == GocaConstants.G_NOP1 || order == 0xFF) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (order == GocaConstants.G_GEAR ||
|
||||||
|
order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE ||
|
||||||
|
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
|
||||||
|
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
||||||
|
}
|
||||||
|
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
|
||||||
|
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
|
||||||
|
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW ||
|
||||||
|
order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW ||
|
||||||
|
order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT ||
|
||||||
|
order == GocaConstants.G_GSMT || order == GocaConstants.G_GSCS ||
|
||||||
|
order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCD ||
|
||||||
|
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMS_SET ||
|
||||||
|
order == GocaConstants.G_GBAR) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (order == GocaConstants.G_GSAP || order == GocaConstants.G_GBIMG || order == 0x91) {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
if (idx + 1 >= end) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (data[idx + 1] & 0xFF) + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a stream of GOCA drawing orders.
|
||||||
|
*/
|
||||||
|
public synchronized void decodeStream(byte[] data, int offset, int length) {
|
||||||
|
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] inputData;
|
||||||
|
int idx;
|
||||||
|
int end;
|
||||||
|
|
||||||
|
if (partialOrderBuffer.length > 0) {
|
||||||
|
inputData = new byte[partialOrderBuffer.length + length];
|
||||||
|
System.arraycopy(partialOrderBuffer, 0, inputData, 0, partialOrderBuffer.length);
|
||||||
|
System.arraycopy(data, offset, inputData, partialOrderBuffer.length, length);
|
||||||
|
idx = 0;
|
||||||
|
end = inputData.length;
|
||||||
|
partialOrderBuffer = new byte[0];
|
||||||
|
} else {
|
||||||
|
inputData = data;
|
||||||
|
idx = offset;
|
||||||
|
end = offset + length;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (idx < end) {
|
||||||
|
int order = inputData[idx] & 0xFF;
|
||||||
|
int orderLen = getOrderLength(inputData, idx, end);
|
||||||
|
|
||||||
|
if (orderLen == -1 || idx + orderLen > end) {
|
||||||
|
int remaining = end - idx;
|
||||||
|
partialOrderBuffer = new byte[remaining];
|
||||||
|
System.arraycopy(inputData, idx, partialOrderBuffer, 0, remaining);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int payloadLen = (orderLen >= 2) ? (inputData[idx + 1] & 0xFF) : 0;
|
||||||
|
|
||||||
|
switch (order) {
|
||||||
|
case GocaConstants.G_NOP1:
|
||||||
|
case 0xFF: {
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GEAR: {
|
||||||
|
endArea();
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GEIMG: {
|
||||||
|
endImage();
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
||||||
|
if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) {
|
||||||
|
resetAttributes();
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E)
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_COMT: { // Comment (0x01)
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSCP: { // Set Current Position (0x21)
|
||||||
|
if (payloadLen >= 4 && idx + 5 < end) {
|
||||||
|
curX = readCoord(inputData, idx + 2);
|
||||||
|
curY = readCoord(inputData, idx + 4);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSAP: { // Arc Parameters (0x22)
|
||||||
|
if (idx + 9 < end) {
|
||||||
|
arcParamP = readCoord(inputData, idx + 2);
|
||||||
|
arcParamQ = readCoord(inputData, idx + 4);
|
||||||
|
arcParamR = readCoord(inputData, idx + 6);
|
||||||
|
arcParamS = readCoord(inputData, idx + 8);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27)
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSCA: { // Set Character Angle (0x34)
|
||||||
|
if (payloadLen >= 4 && idx + 5 < end) {
|
||||||
|
int ax = readCoord(inputData, idx + 2);
|
||||||
|
int ay = readCoord(inputData, idx + 4);
|
||||||
|
if (ax != 0 || ay != 0) {
|
||||||
|
charAngle = Math.toDegrees(Math.atan2(ay, ax));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSCH: { // Set Character Cell (0x33)
|
||||||
|
if (payloadLen >= 4 && idx + 5 < end) {
|
||||||
|
charWidth = readCoord(inputData, idx + 2);
|
||||||
|
charHeight = readCoord(inputData, idx + 4);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSCOL: { // Set Color (0x0A)
|
||||||
|
int colIdx = inputData[idx + 1] & 0xFF;
|
||||||
|
curColor = getColor(colIdx);
|
||||||
|
fillColor = curColor;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSECOL: { // Set Extended Color (0x26)
|
||||||
|
if (payloadLen >= 2 && idx + 3 < end) {
|
||||||
|
int colIdx = inputData[idx + 3] & 0xFF;
|
||||||
|
curColor = getColor(colIdx);
|
||||||
|
fillColor = curColor;
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
||||||
|
lineType = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSLW: { // Set Line Width (0x19)
|
||||||
|
lineWidth = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSMC: { // Set Marker Color (0x07)
|
||||||
|
int colIdx = inputData[idx + 1] & 0xFF;
|
||||||
|
markerColor = getColor(colIdx);
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSMS: { // Set Marker Size (0x1B)
|
||||||
|
markerSize = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSMT: { // Set Marker Type (0x29)
|
||||||
|
markerType = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSPS: { // Set Pattern Set (0x08)
|
||||||
|
pattern = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28)
|
||||||
|
pattern = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSCD: { // Set Character Direction (0x3A)
|
||||||
|
charDir = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GSCS: { // Set Character Set (0x38)
|
||||||
|
charSet = inputData[idx + 1] & 0xFF;
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x04:
|
||||||
|
case GocaConstants.G_GSMX:
|
||||||
|
case GocaConstants.G_GSBMX:
|
||||||
|
case GocaConstants.G_GSFLW:
|
||||||
|
case GocaConstants.G_GSMP:
|
||||||
|
case GocaConstants.G_GSCC:
|
||||||
|
case GocaConstants.G_GSMS_SET:
|
||||||
|
case GocaConstants.G_GPOP: {
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
||||||
|
int flags = inputData[idx + 1] & 0xFF;
|
||||||
|
beginArea((flags & 0x40) != 0);
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GLINE: { // Line Absolute (0xC1)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processLine(inputData, idx + 2, payloadLen, false);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCLINE: { // Line Current Position (0x81)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processLine(inputData, idx + 2, payloadLen, true);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GRLINE: { // Relative Line Absolute Start (0xE1)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processRelativeLine(inputData, idx + 2, payloadLen, false);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCRLIN: { // Relative Line Current Position (0xA1)
|
||||||
|
if (payloadLen >= 2) {
|
||||||
|
processRelativeLine(inputData, idx + 2, payloadLen, true);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GFARC: { // Full Arc Absolute (0xC7)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processArc(inputData, idx + 2, payloadLen, false, true);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCFARC: { // Full Arc Current Position (0x87)
|
||||||
|
if (payloadLen >= 2) {
|
||||||
|
processArc(inputData, idx + 2, payloadLen, true, true);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GARC: { // Partial Arc Absolute (0xC6)
|
||||||
|
if (payloadLen >= 8) {
|
||||||
|
processArc(inputData, idx + 2, payloadLen, false, false);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCARC: { // Partial Arc Current Position (0x86)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processArc(inputData, idx + 2, payloadLen, true, false);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GFLT: { // Fillet Absolute (0xC5)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processFillet(inputData, idx + 2, payloadLen, false);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCFLT: { // Fillet Current Position (0x85)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processFillet(inputData, idx + 2, payloadLen, true);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GMRK: { // Marker Absolute (0xC2)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processMarker(inputData, idx + 2, payloadLen, false);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCMRK: { // Marker Current Position (0x82)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processMarker(inputData, idx + 2, payloadLen, true);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCHST: { // Character String Absolute (0xC3)
|
||||||
|
if (payloadLen >= 4) {
|
||||||
|
processText(inputData, idx + 2, payloadLen, false);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GCCHST: { // Character String Current Position (0x83)
|
||||||
|
if (payloadLen >= 0) {
|
||||||
|
processText(inputData, idx + 2, payloadLen, true);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
|
||||||
|
if (idx + 9 < end) {
|
||||||
|
int x = readCoord(inputData, idx + 2);
|
||||||
|
int y = readCoord(inputData, idx + 4);
|
||||||
|
int w = readCoord(inputData, idx + 6);
|
||||||
|
int h = readCoord(inputData, idx + 8);
|
||||||
|
beginImage(x, y, w, h);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.G_GIMD: { // Image Data (0x92)
|
||||||
|
if (inImage) {
|
||||||
|
for (int k = 0; k < payloadLen; k++) {
|
||||||
|
imgBuffer.add(inputData[idx + 2 + k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a stream of Object Control Procedure Orders (subtype 0x11).
|
||||||
|
*/
|
||||||
|
public synchronized void processProcedureOrders(byte[] data, int offset, int length) {
|
||||||
|
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = offset;
|
||||||
|
int end = offset + length;
|
||||||
|
|
||||||
|
while (idx < end) {
|
||||||
|
int order = data[idx] & 0xFF;
|
||||||
|
|
||||||
|
switch (order) {
|
||||||
|
case GocaConstants.P_NOP1:
|
||||||
|
case GocaConstants.P_ATTCUR:
|
||||||
|
case GocaConstants.P_DETCUR:
|
||||||
|
case GocaConstants.P_STOPDR: {
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
|
||||||
|
plane.clear();
|
||||||
|
resetDefaults();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12)
|
||||||
|
int len = 12;
|
||||||
|
if (idx + 1 < end && data[idx + 1] != 0) {
|
||||||
|
len = (data[idx + 1] & 0xFF) + 2;
|
||||||
|
}
|
||||||
|
idx += len;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int len = data[idx + 1] & 0xFF;
|
||||||
|
if (idx + 2 + len <= end) {
|
||||||
|
decodeStream(data, idx + 2, len);
|
||||||
|
}
|
||||||
|
idx += 2 + len;
|
||||||
|
} else {
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GocaConstants.P_COMT:
|
||||||
|
case GocaConstants.P_SETCUR: {
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int len = data[idx + 1] & 0xFF;
|
||||||
|
idx += 2 + len;
|
||||||
|
} else {
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int len = data[idx + 1] & 0xFF;
|
||||||
|
idx += 2 + len;
|
||||||
|
} else {
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginArea(boolean drawBoundary) {
|
||||||
|
this.inArea = true;
|
||||||
|
this.areaDrawBoundary = drawBoundary;
|
||||||
|
this.fillColor = this.curColor;
|
||||||
|
this.areaPointsX.clear();
|
||||||
|
this.areaPointsY.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endArea() {
|
||||||
|
if (!inArea || areaPointsX.size() < 3) {
|
||||||
|
inArea = false;
|
||||||
|
areaPointsX.clear();
|
||||||
|
areaPointsY.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int n = areaPointsX.size();
|
||||||
|
int[] px = new int[n];
|
||||||
|
int[] py = new int[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
px[i] = plane.mapX(areaPointsX.get(i));
|
||||||
|
py[i] = plane.mapY(areaPointsY.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
plane.fillArea(px, py, n, fillColor, pattern, areaDrawBoundary, curColor, lineType, lineWidth);
|
||||||
|
inArea = false;
|
||||||
|
areaPointsX.clear();
|
||||||
|
areaPointsY.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAreaPoint(int x, int y) {
|
||||||
|
if (inArea) {
|
||||||
|
areaPointsX.add(x);
|
||||||
|
areaPointsY.add(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginImage(int x, int y, int w, int h) {
|
||||||
|
this.inImage = true;
|
||||||
|
this.imgX = x;
|
||||||
|
this.imgY = y;
|
||||||
|
this.imgWidth = w;
|
||||||
|
this.imgHeight = h;
|
||||||
|
this.imgBuffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endImage() {
|
||||||
|
if (!inImage || imgWidth <= 0 || imgHeight <= 0 || imgBuffer.isEmpty()) {
|
||||||
|
inImage = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] bytes = new byte[imgBuffer.size()];
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
bytes[i] = imgBuffer.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
int px = plane.mapX(imgX);
|
||||||
|
int py = plane.mapY(imgY);
|
||||||
|
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor);
|
||||||
|
inImage = false;
|
||||||
|
imgBuffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processLine(byte[] data, int off, int len, boolean fromCurPos) {
|
||||||
|
int pos = off;
|
||||||
|
int end = off + len;
|
||||||
|
|
||||||
|
int startX = curX;
|
||||||
|
int startY = curY;
|
||||||
|
|
||||||
|
if (!fromCurPos && pos + 4 <= end) {
|
||||||
|
startX = readCoord(data, pos);
|
||||||
|
startY = readCoord(data, pos + 2);
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inArea) {
|
||||||
|
addAreaPoint(startX, startY);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (pos + 4 <= end) {
|
||||||
|
int nextX = readCoord(data, pos);
|
||||||
|
int nextY = readCoord(data, pos + 2);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
if (inArea) {
|
||||||
|
addAreaPoint(nextX, nextY);
|
||||||
|
} else {
|
||||||
|
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
|
||||||
|
plane.mapX(nextX), plane.mapY(nextY),
|
||||||
|
curColor, lineType, lineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
startX = nextX;
|
||||||
|
startY = nextY;
|
||||||
|
}
|
||||||
|
|
||||||
|
curX = startX;
|
||||||
|
curY = startY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processRelativeLine(byte[] data, int off, int len, boolean fromCurPos) {
|
||||||
|
int pos = off;
|
||||||
|
int end = off + len;
|
||||||
|
|
||||||
|
int startX = curX;
|
||||||
|
int startY = curY;
|
||||||
|
|
||||||
|
if (!fromCurPos && pos + 4 <= end) {
|
||||||
|
startX = readCoord(data, pos);
|
||||||
|
startY = readCoord(data, pos + 2);
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inArea) {
|
||||||
|
addAreaPoint(startX, startY);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (pos + 2 <= end) {
|
||||||
|
int dx = (byte) data[pos];
|
||||||
|
int dy = (byte) data[pos + 1];
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
int nextX = startX + dx;
|
||||||
|
int nextY = startY + dy;
|
||||||
|
|
||||||
|
if (inArea) {
|
||||||
|
addAreaPoint(nextX, nextY);
|
||||||
|
} else {
|
||||||
|
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
|
||||||
|
plane.mapX(nextX), plane.mapY(nextY),
|
||||||
|
curColor, lineType, lineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
startX = nextX;
|
||||||
|
startY = nextY;
|
||||||
|
}
|
||||||
|
|
||||||
|
curX = startX;
|
||||||
|
curY = startY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
|
||||||
|
int pos = off;
|
||||||
|
int startX = curX;
|
||||||
|
int startY = curY;
|
||||||
|
|
||||||
|
if (!fromCurPos && pos + 4 <= off + len) {
|
||||||
|
startX = readCoord(data, pos);
|
||||||
|
startY = readCoord(data, pos + 2);
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
double multiplier = 1.0;
|
||||||
|
if (pos + 2 <= off + len) {
|
||||||
|
multiplier = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) / 255.0);
|
||||||
|
if (multiplier == 0.0) multiplier = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dxP = Math.abs(arcParamP - arcParamR);
|
||||||
|
int dyQ = Math.abs(arcParamQ - arcParamS);
|
||||||
|
if (dxP == 0) dxP = 10;
|
||||||
|
if (dyQ == 0) dyQ = 10;
|
||||||
|
|
||||||
|
int rxVirtual = (int) (dxP * multiplier);
|
||||||
|
int ryVirtual = (int) (dyQ * multiplier);
|
||||||
|
|
||||||
|
int rx = Math.abs(plane.mapX(rxVirtual) - plane.mapX(0));
|
||||||
|
int ry = Math.abs(plane.mapY(ryVirtual) - plane.mapY(0));
|
||||||
|
if (rx <= 0) rx = 10;
|
||||||
|
if (ry <= 0) ry = 10;
|
||||||
|
|
||||||
|
plane.drawArc(plane.mapX(startX), plane.mapY(startY), rx, ry, 0.0, 360.0,
|
||||||
|
curColor, lineType, lineWidth, isFull);
|
||||||
|
|
||||||
|
curX = startX;
|
||||||
|
curY = startY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processFillet(byte[] data, int off, int len, boolean fromCurPos) {
|
||||||
|
int pos = off;
|
||||||
|
int end = off + len;
|
||||||
|
|
||||||
|
List<Integer> ptsX = new ArrayList<>();
|
||||||
|
List<Integer> ptsY = new ArrayList<>();
|
||||||
|
|
||||||
|
if (fromCurPos) {
|
||||||
|
ptsX.add(plane.mapX(curX));
|
||||||
|
ptsY.add(plane.mapY(curY));
|
||||||
|
}
|
||||||
|
|
||||||
|
while (pos + 4 <= end) {
|
||||||
|
int x = readCoord(data, pos);
|
||||||
|
int y = readCoord(data, pos + 2);
|
||||||
|
ptsX.add(plane.mapX(x));
|
||||||
|
ptsY.add(plane.mapY(y));
|
||||||
|
curX = x;
|
||||||
|
curY = y;
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ptsX.size() >= 2) {
|
||||||
|
int n = ptsX.size();
|
||||||
|
int[] px = new int[n];
|
||||||
|
int[] py = new int[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
px[i] = ptsX.get(i);
|
||||||
|
py[i] = ptsY.get(i);
|
||||||
|
}
|
||||||
|
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
||||||
|
int pos = off;
|
||||||
|
int end = off + len;
|
||||||
|
|
||||||
|
if (fromCurPos) {
|
||||||
|
plane.drawMarker(plane.mapX(curX), plane.mapY(curY), markerType, markerSize, markerColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (pos + 4 <= end) {
|
||||||
|
int x = readCoord(data, pos);
|
||||||
|
int y = readCoord(data, pos + 2);
|
||||||
|
plane.drawMarker(plane.mapX(x), plane.mapY(y), markerType, markerSize, markerColor);
|
||||||
|
curX = x;
|
||||||
|
curY = y;
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processText(byte[] data, int off, int len, boolean fromCurPos) {
|
||||||
|
int pos = off;
|
||||||
|
int end = off + len;
|
||||||
|
|
||||||
|
int startX = curX;
|
||||||
|
int startY = curY;
|
||||||
|
|
||||||
|
if (!fromCurPos && pos + 4 <= end) {
|
||||||
|
startX = readCoord(data, pos);
|
||||||
|
startY = readCoord(data, pos + 2);
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
int textLen = end - pos;
|
||||||
|
if (textLen <= 0) return;
|
||||||
|
|
||||||
|
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 12;
|
||||||
|
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 20;
|
||||||
|
|
||||||
|
if (charSet != 0 && programSymbolManager != null) {
|
||||||
|
for (int i = 0; i < textLen; i++) {
|
||||||
|
int code = data[pos + i] & 0xFF;
|
||||||
|
int px = plane.mapX(startX);
|
||||||
|
int py = plane.mapY(startY) - ch;
|
||||||
|
boolean drawn = programSymbolManager.drawSymbol(plane.getGraphics(), charSet, code, px, py, cw, ch, curColor, null);
|
||||||
|
if (!drawn) {
|
||||||
|
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
||||||
|
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
|
||||||
|
}
|
||||||
|
startX += (charWidth > 0 ? charWidth : 9);
|
||||||
|
}
|
||||||
|
curX = startX;
|
||||||
|
curY = startY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char[] chars = new char[textLen];
|
||||||
|
for (int i = 0; i < textLen; i++) {
|
||||||
|
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
||||||
|
}
|
||||||
|
String text = new String(chars);
|
||||||
|
|
||||||
|
plane.drawVectorText(plane.mapX(startX), plane.mapY(startY) - ch, text,
|
||||||
|
curColor, cw, ch, charDir, charAngle);
|
||||||
|
|
||||||
|
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
|
||||||
|
curY = startY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readCoord(byte[] data, int off) {
|
||||||
|
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color getColor(int colorIndex) {
|
||||||
|
if (colorIndex >= 0 && colorIndex < GocaConstants.GOCA_COLORS.length) {
|
||||||
|
return GocaConstants.GOCA_COLORS[colorIndex];
|
||||||
|
}
|
||||||
|
return Color.GREEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graphics modes supported by j3270 / lib3270j.
|
||||||
|
*/
|
||||||
|
public enum GraphicsMode {
|
||||||
|
/** Text only (no graphics query replies, classic 3279-4 terminal behavior). */
|
||||||
|
NONE("None (Text Only)"),
|
||||||
|
|
||||||
|
/** Programmed Symbols only (custom character matrices and APL, single & triple plane). */
|
||||||
|
PROGRAMMED_SYMBOLS("Programmed Symbols Only"),
|
||||||
|
|
||||||
|
/** Vector graphics only (GOCA / 3179G drawing orders). */
|
||||||
|
VECTOR_GRAPHICS("Vector Graphics Only"),
|
||||||
|
|
||||||
|
/** Full graphics support (both Programmed Symbols and Vector Graphics). */
|
||||||
|
BOTH("Both (Programmed Symbols & Vector Graphics)");
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
GraphicsMode(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProgrammedSymbolsEnabled() {
|
||||||
|
return this == PROGRAMMED_SYMBOLS || this == BOTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVectorGraphicsEnabled() {
|
||||||
|
return this == VECTOR_GRAPHICS || this == BOTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GraphicsMode fromString(String str) {
|
||||||
|
if (str == null || str.trim().isEmpty()) {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
String s = str.trim().toUpperCase();
|
||||||
|
switch (s) {
|
||||||
|
case "BOTH":
|
||||||
|
case "ALL":
|
||||||
|
case "FULL":
|
||||||
|
case "ON":
|
||||||
|
case "TRUE":
|
||||||
|
return BOTH;
|
||||||
|
case "PS":
|
||||||
|
case "PROGRAMMED_SYMBOLS":
|
||||||
|
case "PROGRAMMEDSYMBOLS":
|
||||||
|
case "SYMBOLS":
|
||||||
|
case "APL":
|
||||||
|
return PROGRAMMED_SYMBOLS;
|
||||||
|
case "VECTOR":
|
||||||
|
case "VECTOR_GRAPHICS":
|
||||||
|
case "VECTORGRAPHICS":
|
||||||
|
case "GOCA":
|
||||||
|
case "GDDM":
|
||||||
|
return VECTOR_GRAPHICS;
|
||||||
|
case "NONE":
|
||||||
|
case "OFF":
|
||||||
|
case "FALSE":
|
||||||
|
case "DISABLED":
|
||||||
|
case "TEXT":
|
||||||
|
default:
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
import java.awt.BasicStroke;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Polygon;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.Stroke;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.geom.Arc2D;
|
||||||
|
import java.awt.geom.GeneralPath;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offscreen rendering surface for GOCA vector graphics.
|
||||||
|
* Maintained as an ARGB BufferedImage that overlays the 3270 character cell matrix.
|
||||||
|
*/
|
||||||
|
public class GraphicsPlane {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
|
||||||
|
|
||||||
|
// 17 Standard GOCA 8x8 Fill Patterns
|
||||||
|
public static final byte[][] PATTERN_DATA = new byte[][]{
|
||||||
|
{-1, -1, -1, -1, -1, -1, -1, -1}, // 0: Solid (all 1s)
|
||||||
|
{-1, -1, -1, -18, -1, -1, -1, -18}, // 1: D1
|
||||||
|
{-1, -69, -1, -18, -1, -69, -1, -18}, // 2: D2
|
||||||
|
{119, -35, -69, -18, 119, -35, -69, -18}, // 3: D3
|
||||||
|
{-69, -52, 51, -18, -69, -52, 51, -18}, // 4: D4
|
||||||
|
{85, -86, 85, -86, 85, -86, 85, -86}, // 5: D5 (50% checker)
|
||||||
|
{68, 51, -52, 17, 68, 51, -52, 17}, // 6: D6
|
||||||
|
{-120, 34, 68, 17, -120, 34, 68, 17}, // 7: D7
|
||||||
|
{0, 68, 0, 17, 0, 68, 0, 17}, // 8: D8 (sparse dots)
|
||||||
|
{-128, -128, -128, -128, -128, -128, -128, -128}, // 9: Vertical line
|
||||||
|
{-1, 0, 0, 0, 0, 0, 0, 0}, // 10: Horizontal line
|
||||||
|
{1, 2, 4, 8, 16, 32, 64, -128}, // 11: Diagonal bottom-left to top-right
|
||||||
|
{3, 12, 48, -64, 3, 12, 48, -64}, // 12: Diagonal BL-TR dense
|
||||||
|
{-128, 64, 32, 16, 8, 4, 2, 1}, // 13: Diagonal top-left to bottom-right
|
||||||
|
{-64, 48, 12, 3, -64, 48, 12, 3}, // 14: Diagonal TL-BR dense
|
||||||
|
{0, 0, 0, 0, 0, 0, 0, 0}, // 15: Empty (transparent)
|
||||||
|
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
||||||
|
};
|
||||||
|
|
||||||
|
private int canvasWidth = 800;
|
||||||
|
private int canvasHeight = 600;
|
||||||
|
private BufferedImage image;
|
||||||
|
private Graphics2D g2d;
|
||||||
|
private boolean hasContent = false;
|
||||||
|
|
||||||
|
public GraphicsPlane(int width, int height) {
|
||||||
|
resize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void resize(int width, int height) {
|
||||||
|
int w = Math.max(1, width);
|
||||||
|
int h = Math.max(1, height);
|
||||||
|
if (w == canvasWidth && h == canvasHeight && image != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.canvasWidth = w;
|
||||||
|
this.canvasHeight = h;
|
||||||
|
|
||||||
|
BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D newG2d = newImage.createGraphics();
|
||||||
|
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
newG2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||||
|
|
||||||
|
if (image != null && hasContent) {
|
||||||
|
newG2d.drawImage(image, 0, 0, w, h, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.g2d != null) {
|
||||||
|
this.g2d.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.image = newImage;
|
||||||
|
this.g2d = newG2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void clear() {
|
||||||
|
if (image != null && g2d != null) {
|
||||||
|
BufferedImage newImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D newG2d = newImage.createGraphics();
|
||||||
|
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
if (this.g2d != null) {
|
||||||
|
this.g2d.dispose();
|
||||||
|
}
|
||||||
|
this.image = newImage;
|
||||||
|
this.g2d = newG2d;
|
||||||
|
}
|
||||||
|
hasContent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean hasContent() {
|
||||||
|
return hasContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized BufferedImage getImage() {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCanvasWidth() {
|
||||||
|
return canvasWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCanvasHeight() {
|
||||||
|
return canvasHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Graphics2D getGraphics() {
|
||||||
|
return g2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int screenCols = 80;
|
||||||
|
private int screenRows = 24;
|
||||||
|
|
||||||
|
public void setScreenDimensions(int cols, int rows) {
|
||||||
|
this.screenCols = cols > 0 ? cols : 80;
|
||||||
|
this.screenRows = rows > 0 ? rows : 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getScreenCols() {
|
||||||
|
return screenCols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getScreenRows() {
|
||||||
|
return screenRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
||||||
|
*/
|
||||||
|
public int mapX(int gocaX) {
|
||||||
|
int nominalWidth = screenCols * 9;
|
||||||
|
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
||||||
|
int nx = gocaX + xMax;
|
||||||
|
return (int) Math.round((double) nx * canvasWidth / nominalWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
||||||
|
*/
|
||||||
|
public int mapY(int gocaY) {
|
||||||
|
int nominalHeight = screenRows * 16;
|
||||||
|
int yMax = (nominalHeight - 1) / 2;
|
||||||
|
int ny = yMax - gocaY;
|
||||||
|
return (int) Math.round((double) ny * canvasHeight / nominalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an absolute or relative line.
|
||||||
|
*/
|
||||||
|
public synchronized void drawLine(int x1, int y1, int x2, int y2, Color color, int lineType, int lineWidth) {
|
||||||
|
if (g2d == null) return;
|
||||||
|
g2d.setColor(color != null ? color : Color.WHITE);
|
||||||
|
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||||
|
g2d.drawLine(x1, y1, x2, y2);
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a full or partial arc / ellipse.
|
||||||
|
*/
|
||||||
|
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
|
||||||
|
Color color, int lineType, int lineWidth, boolean isFull) {
|
||||||
|
if (g2d == null) return;
|
||||||
|
g2d.setColor(color != null ? color : Color.WHITE);
|
||||||
|
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||||
|
|
||||||
|
double x = cx - rx;
|
||||||
|
double y = cy - ry;
|
||||||
|
double w = rx * 2.0;
|
||||||
|
double h = ry * 2.0;
|
||||||
|
|
||||||
|
if (isFull) {
|
||||||
|
g2d.drawOval((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h));
|
||||||
|
} else {
|
||||||
|
Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngleDeg, sweepAngleDeg, Arc2D.OPEN);
|
||||||
|
g2d.draw(arc);
|
||||||
|
}
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a Fillet (spline / curve approximation across control points).
|
||||||
|
*/
|
||||||
|
public synchronized void drawFillet(int[] px, int[] py, int numPoints, Color color, int lineType, int lineWidth) {
|
||||||
|
if (g2d == null || numPoints < 2) return;
|
||||||
|
g2d.setColor(color != null ? color : Color.WHITE);
|
||||||
|
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||||
|
|
||||||
|
GeneralPath path = new GeneralPath();
|
||||||
|
path.moveTo(px[0], py[0]);
|
||||||
|
|
||||||
|
if (numPoints == 2) {
|
||||||
|
path.lineTo(px[1], py[1]);
|
||||||
|
} else {
|
||||||
|
for (int i = 1; i < numPoints - 1; i++) {
|
||||||
|
double midX = (px[i] + px[i + 1]) / 2.0;
|
||||||
|
double midY = (py[i] + py[i + 1]) / 2.0;
|
||||||
|
path.quadTo(px[i], py[i], midX, midY);
|
||||||
|
}
|
||||||
|
path.lineTo(px[numPoints - 1], py[numPoints - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
g2d.draw(path);
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills a closed polygon area with a solid color or hatching pattern.
|
||||||
|
*/
|
||||||
|
public synchronized void fillArea(int[] px, int[] py, int numPoints, Color fillColor, int pattern,
|
||||||
|
boolean drawBoundary, Color boundaryColor, int lineType, int lineWidth) {
|
||||||
|
if (g2d == null || numPoints < 3) return;
|
||||||
|
|
||||||
|
Polygon poly = new Polygon(px, py, numPoints);
|
||||||
|
|
||||||
|
if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) {
|
||||||
|
g2d.setColor(fillColor != null ? fillColor : Color.WHITE);
|
||||||
|
g2d.fill(poly);
|
||||||
|
} else if (pattern != GocaConstants.PT_EMPTY) {
|
||||||
|
// Fill with pattern texture
|
||||||
|
BufferedImage patImg = createPatternTexture(pattern, fillColor != null ? fillColor : Color.WHITE);
|
||||||
|
java.awt.TexturePaint tp = new java.awt.TexturePaint(patImg, new java.awt.Rectangle(0, 0, 8, 8));
|
||||||
|
g2d.setPaint(tp);
|
||||||
|
g2d.fill(poly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawBoundary && boundaryColor != null) {
|
||||||
|
g2d.setColor(boundaryColor);
|
||||||
|
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||||
|
g2d.draw(poly);
|
||||||
|
}
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle).
|
||||||
|
*/
|
||||||
|
public synchronized void drawMarker(int x, int y, int markerType, int size, Color color) {
|
||||||
|
if (g2d == null) return;
|
||||||
|
g2d.setColor(color != null ? color : Color.WHITE);
|
||||||
|
g2d.setStroke(new BasicStroke(1.5f));
|
||||||
|
|
||||||
|
int s = Math.max(3, size > 0 ? size : 5);
|
||||||
|
|
||||||
|
switch (markerType) {
|
||||||
|
case GocaConstants.MK_CROSS: // x
|
||||||
|
g2d.drawLine(x - s, y - s, x + s, y + s);
|
||||||
|
g2d.drawLine(x - s, y + s, x + s, y - s);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_PLUS: // +
|
||||||
|
case GocaConstants.MK_DEFAULT:
|
||||||
|
g2d.drawLine(x - s, y, x + s, y);
|
||||||
|
g2d.drawLine(x, y - s, x, y + s);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_DIAMOND: // <>
|
||||||
|
g2d.drawPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_SQUARE: // []
|
||||||
|
g2d.drawRect(x - s, y - s, s * 2, s * 2);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_6STAR: // 6-point star
|
||||||
|
g2d.drawLine(x - s, y, x + s, y);
|
||||||
|
g2d.drawLine(x - s / 2, y - s, x + s / 2, y + s);
|
||||||
|
g2d.drawLine(x - s / 2, y + s, x + s / 2, y - s);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_8STAR: // 8-point star
|
||||||
|
g2d.drawLine(x - s, y, x + s, y);
|
||||||
|
g2d.drawLine(x, y - s, x, y + s);
|
||||||
|
g2d.drawLine(x - s, y - s, x + s, y + s);
|
||||||
|
g2d.drawLine(x - s, y + s, x + s, y - s);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_SDIAMOND: // solid diamond
|
||||||
|
g2d.fillPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_SSQUARE: // solid square
|
||||||
|
g2d.fillRect(x - s, y - s, s * 2, s * 2);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_DOT: // dot
|
||||||
|
g2d.fillOval(x - 2, y - 2, 4, 4);
|
||||||
|
break;
|
||||||
|
case GocaConstants.MK_CIRCLE: // circle
|
||||||
|
g2d.drawOval(x - s, y - s, s * 2, s * 2);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
g2d.drawOval(x - s, y - s, s * 2, s * 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws stroked vector text using IBM Vector Symbol Set (VSS).
|
||||||
|
*/
|
||||||
|
public synchronized void drawVectorText(int x, int y, String text, Color color,
|
||||||
|
int cellWidth, int cellHeight, int dir, double angle) {
|
||||||
|
if (g2d == null || text == null || text.isEmpty()) return;
|
||||||
|
g2d.setColor(color != null ? color : Color.WHITE);
|
||||||
|
g2d.setStroke(new BasicStroke(1.2f));
|
||||||
|
|
||||||
|
int curX = x;
|
||||||
|
int curY = y;
|
||||||
|
int cw = cellWidth > 0 ? cellWidth : 12;
|
||||||
|
int ch = cellHeight > 0 ? cellHeight : 20;
|
||||||
|
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char c = text.charAt(i);
|
||||||
|
drawVssChar(curX, curY, c, cw, ch);
|
||||||
|
|
||||||
|
switch (dir) {
|
||||||
|
case GocaConstants.CD_TB: curY += ch; break;
|
||||||
|
case GocaConstants.CD_RL: curX -= cw; break;
|
||||||
|
case GocaConstants.CD_BT: curY -= ch; break;
|
||||||
|
case GocaConstants.CD_LR:
|
||||||
|
case GocaConstants.CD_DEFAULT:
|
||||||
|
default:
|
||||||
|
curX += cw;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawVssChar(int x, int y, char c, int cw, int ch) {
|
||||||
|
int idx = (int) c;
|
||||||
|
if (idx < VectorSymbolData.VSS_SYMBOL_START || idx > VectorSymbolData.VSS_SYMBOL_END) {
|
||||||
|
// Draw simple bounding box or space
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback vector stroke rendering
|
||||||
|
g2d.drawString(String.valueOf(c), x, y + ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws raw image pixel bitmap.
|
||||||
|
*/
|
||||||
|
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, Color fgColor) {
|
||||||
|
if (g2d == null || imageData == null || width <= 0 || height <= 0) return;
|
||||||
|
|
||||||
|
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF;
|
||||||
|
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
for (int col = 0; col < width; col++) {
|
||||||
|
int bitIndex = row * width + col;
|
||||||
|
int byteIdx = bitIndex / 8;
|
||||||
|
if (byteIdx < imageData.length) {
|
||||||
|
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0;
|
||||||
|
if (bit) {
|
||||||
|
img.setRGB(col, row, fgRgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g2d.drawImage(img, x, y, null);
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stroke createStroke(int lineType, int lineWidth) {
|
||||||
|
float width = (lineWidth == GocaConstants.LW_THICK) ? 2.5f : 1.2f;
|
||||||
|
|
||||||
|
switch (lineType) {
|
||||||
|
case GocaConstants.LT_DOT:
|
||||||
|
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f);
|
||||||
|
case GocaConstants.LT_SHORTDASH:
|
||||||
|
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{4.0f, 2.0f}, 0.0f);
|
||||||
|
case GocaConstants.LT_DASHDOT:
|
||||||
|
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{6.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
|
||||||
|
case GocaConstants.LT_DOUBLEDOT:
|
||||||
|
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f, 2.0f, 4.0f}, 0.0f);
|
||||||
|
case GocaConstants.LT_LONGDASH:
|
||||||
|
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 3.0f}, 0.0f);
|
||||||
|
case GocaConstants.LT_DASHDOUBLEDOT:
|
||||||
|
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
|
||||||
|
case GocaConstants.LT_SOLID:
|
||||||
|
case GocaConstants.LT_DEFAULT:
|
||||||
|
default:
|
||||||
|
return new BasicStroke(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage createPatternTexture(int patternIndex, Color color) {
|
||||||
|
BufferedImage pat = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
byte[] patternRows = (patternIndex >= 0 && patternIndex < PATTERN_DATA.length) ? PATTERN_DATA[patternIndex] : PATTERN_DATA[0];
|
||||||
|
int rgb = color.getRGB();
|
||||||
|
|
||||||
|
for (int r = 0; r < 8; r++) {
|
||||||
|
int b = patternRows[r] & 0xFF;
|
||||||
|
for (int c = 0; c < 8; c++) {
|
||||||
|
if (((b >> (7 - c)) & 1) != 0) {
|
||||||
|
pat.setRGB(c, r, rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pat;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages IBM 3270 Programmed Symbols (PS / APL) character sets.
|
||||||
|
* Handles the Load Programmed Symbols (LOADPS structured field 0x0F).
|
||||||
|
*/
|
||||||
|
public class ProgramSymbolManager {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ProgramSymbolManager.class.getName());
|
||||||
|
|
||||||
|
public static final int NUMBER_SYMBOL_SETS = 10;
|
||||||
|
public static final int NUMBER_SINGLE_PLANE_PS_SETS = 2; // RWS 2..3 (Sets 0..1)
|
||||||
|
public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5)
|
||||||
|
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
||||||
|
|
||||||
|
private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||||
|
|
||||||
|
public ProgramSymbolManager() {
|
||||||
|
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||||
|
boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS);
|
||||||
|
sets[i] = new ProgramSymbolSet(isTriple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all symbol sets.
|
||||||
|
*/
|
||||||
|
public synchronized void clearAll() {
|
||||||
|
for (ProgramSymbolSet set : sets) {
|
||||||
|
set.clear();
|
||||||
|
set.setLcid(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE).
|
||||||
|
*/
|
||||||
|
public synchronized ProgramSymbolSet getSymbolSet(int lcid) {
|
||||||
|
if (lcid <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (ProgramSymbolSet set : sets) {
|
||||||
|
if (set.getLcid() == lcid) {
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
|
||||||
|
*/
|
||||||
|
public synchronized ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
||||||
|
ProgramSymbolSet set = getSymbolSet(lcid);
|
||||||
|
if (set == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint;
|
||||||
|
return set.getSlot(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a programmed symbol character cell if defined.
|
||||||
|
* Returns true if symbol was drawn, false if not found.
|
||||||
|
*/
|
||||||
|
public synchronized boolean drawSymbol(Graphics2D g2d, int lcid, int codePoint,
|
||||||
|
int x, int y, int cellWidth, int cellHeight,
|
||||||
|
Color fgColor, Color bgColor) {
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = getSymbol(lcid, codePoint);
|
||||||
|
if (slot != null) {
|
||||||
|
slot.draw(g2d, x, y, cellWidth, cellHeight, fgColor, bgColor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
|
||||||
|
*/
|
||||||
|
public synchronized void loadps(byte[] data) {
|
||||||
|
if (data == null || data.length < 4) {
|
||||||
|
logger.warning("LOADPS: Payload too short (" + (data == null ? 0 : data.length) + " bytes)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags = data[0] & 0xFF;
|
||||||
|
int loadFormat = flags & 0x1F;
|
||||||
|
boolean clearAll = (flags & 0x40) != 0;
|
||||||
|
boolean hasExtHeader = (flags & 0x80) != 0;
|
||||||
|
|
||||||
|
int lcid = data[1] & 0xFF;
|
||||||
|
int startCodePoint = data[2] & 0xFF;
|
||||||
|
int rws = data[3] & 0xFF;
|
||||||
|
|
||||||
|
int setIndex;
|
||||||
|
switch (rws) {
|
||||||
|
case 2: setIndex = 0; break;
|
||||||
|
case 3: setIndex = 1; break;
|
||||||
|
case 4: setIndex = 2; break;
|
||||||
|
case 5: setIndex = 3; break;
|
||||||
|
case 6: setIndex = 4; break;
|
||||||
|
case 7: setIndex = 5; break;
|
||||||
|
case 8: setIndex = 6; break;
|
||||||
|
case 9: setIndex = 7; break;
|
||||||
|
case 10: setIndex = 8; break;
|
||||||
|
case 11: setIndex = 9; break;
|
||||||
|
default:
|
||||||
|
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgramSymbolSet set = sets[setIndex];
|
||||||
|
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||||
|
|
||||||
|
int extHeaderLen = 0;
|
||||||
|
int cellWidth = 9;
|
||||||
|
int cellHeight = 16;
|
||||||
|
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||||
|
|
||||||
|
if (hasExtHeader && data.length > 4) {
|
||||||
|
extHeaderLen = data[4] & 0xFF;
|
||||||
|
if (extHeaderLen > 3 && data.length > 6) {
|
||||||
|
int lw = data[6] & 0xFF;
|
||||||
|
if (lw > 0) cellWidth = lw;
|
||||||
|
}
|
||||||
|
if (extHeaderLen > 4 && data.length > 7) {
|
||||||
|
int lh = data[7] & 0xFF;
|
||||||
|
if (lh > 0) cellHeight = lh;
|
||||||
|
}
|
||||||
|
if (extHeaderLen >= 6 && data.length > 9) {
|
||||||
|
colorPlane = data[9] & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearAll) {
|
||||||
|
set.clear();
|
||||||
|
}
|
||||||
|
set.setLcid(lcid);
|
||||||
|
|
||||||
|
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
|
||||||
|
int remaining = data.length - offset;
|
||||||
|
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||||
|
|
||||||
|
int bytesPerSymbol;
|
||||||
|
if (loadFormat == 1) {
|
||||||
|
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
|
||||||
|
} else {
|
||||||
|
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesPerSymbol <= 0) {
|
||||||
|
bytesPerSymbol = 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||||
|
byte[] pixelData;
|
||||||
|
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
||||||
|
if (existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||||
|
pixelData = existing.getPixelData();
|
||||||
|
} else {
|
||||||
|
pixelData = new byte[cellWidth * cellHeight];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadFormat == 1) {
|
||||||
|
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||||
|
} else {
|
||||||
|
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||||
|
}
|
||||||
|
|
||||||
|
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
||||||
|
|
||||||
|
codeIndex++;
|
||||||
|
offset += bytesPerSymbol;
|
||||||
|
remaining -= bytesPerSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
||||||
|
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
||||||
|
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpacks Format 1 (9x16) symbol slice bit-pattern.
|
||||||
|
*/
|
||||||
|
private void unpackFormat1(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||||
|
boolean isTriplePlane, int colorPlane) {
|
||||||
|
// Format 1 transmits 18 bytes:
|
||||||
|
// Byte 0-1: contains column 0 for each of the 16 rows
|
||||||
|
// Bytes 2-17: contains columns 1-8 for each of the 16 rows
|
||||||
|
int planeMask = (colorPlane != 0) ? colorPlane : (isTriplePlane ? 7 : 1);
|
||||||
|
|
||||||
|
for (int row = 0; row < 16 && row < height; row++) {
|
||||||
|
// Column 0 bit from byte 0 or byte 1
|
||||||
|
int b0 = (row < 8) ? (src[srcOff] & 0xFF) : (src[srcOff + 1] & 0xFF);
|
||||||
|
int bitShift0 = 7 - (row % 8);
|
||||||
|
boolean bit0 = ((b0 >> bitShift0) & 1) != 0;
|
||||||
|
|
||||||
|
int dstIdx0 = row * width;
|
||||||
|
if (dstIdx0 < dst.length) {
|
||||||
|
if (!isTriplePlane) {
|
||||||
|
dst[dstIdx0] = bit0 ? (byte) 1 : 0;
|
||||||
|
} else if (colorPlane == 0) {
|
||||||
|
dst[dstIdx0] = bit0 ? (byte) 7 : 0;
|
||||||
|
} else {
|
||||||
|
if (bit0) {
|
||||||
|
dst[dstIdx0] |= (byte) colorPlane;
|
||||||
|
} else {
|
||||||
|
dst[dstIdx0] &= (byte) ~colorPlane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columns 1..8 from bytes 2..17
|
||||||
|
if (srcOff + 2 + row < src.length) {
|
||||||
|
int rowByte = src[srcOff + 2 + row] & 0xFF;
|
||||||
|
for (int col = 1; col < 9 && col < width; col++) {
|
||||||
|
int dstIdx = row * width + col;
|
||||||
|
if (dstIdx < dst.length) {
|
||||||
|
boolean bit = ((rowByte >> (8 - col)) & 1) != 0;
|
||||||
|
if (!isTriplePlane) {
|
||||||
|
dst[dstIdx] = bit ? (byte) 1 : 0;
|
||||||
|
} else if (colorPlane == 0) {
|
||||||
|
dst[dstIdx] = bit ? (byte) 7 : 0;
|
||||||
|
} else {
|
||||||
|
if (bit) {
|
||||||
|
dst[dstIdx] |= (byte) colorPlane;
|
||||||
|
} else {
|
||||||
|
dst[dstIdx] &= (byte) ~colorPlane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpacks Format 3 variable dimension bit-pattern.
|
||||||
|
*/
|
||||||
|
private void unpackFormat3(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||||
|
boolean isTriplePlane, int colorPlane) {
|
||||||
|
int totalPixels = width * height;
|
||||||
|
for (int i = 0; i < totalPixels; i++) {
|
||||||
|
int byteIndex = srcOff + (i / 8);
|
||||||
|
if (byteIndex >= src.length) break;
|
||||||
|
int bitIndex = 7 - (i % 8);
|
||||||
|
boolean bit = ((src[byteIndex] >> bitIndex) & 1) != 0;
|
||||||
|
|
||||||
|
if (!isTriplePlane) {
|
||||||
|
dst[i] = bit ? (byte) 1 : 0;
|
||||||
|
} else if (colorPlane == 0) {
|
||||||
|
dst[i] = bit ? (byte) 7 : 0;
|
||||||
|
} else {
|
||||||
|
if (bit) {
|
||||||
|
dst[i] |= (byte) colorPlane;
|
||||||
|
} else {
|
||||||
|
dst[i] &= (byte) ~colorPlane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
||||||
|
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
||||||
|
*/
|
||||||
|
public class ProgramSymbolSet {
|
||||||
|
|
||||||
|
public static final int NUM_SLOTS = 191; // Code points 0x40 - 0xFE (0..190)
|
||||||
|
|
||||||
|
private int lcid = 0;
|
||||||
|
private final boolean isTriplePlane;
|
||||||
|
private final SymbolSlot[] slots = new SymbolSlot[NUM_SLOTS];
|
||||||
|
|
||||||
|
public ProgramSymbolSet(boolean isTriplePlane) {
|
||||||
|
this.isTriplePlane = isTriplePlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLcid() {
|
||||||
|
return lcid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLcid(int lcid) {
|
||||||
|
this.lcid = lcid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTriplePlane() {
|
||||||
|
return isTriplePlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
for (int i = 0; i < NUM_SLOTS; i++) {
|
||||||
|
slots[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSlot(int index) {
|
||||||
|
if (index >= 0 && index < NUM_SLOTS) {
|
||||||
|
slots[index] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSlot(int index, SymbolSlot slot) {
|
||||||
|
if (index >= 0 && index < NUM_SLOTS) {
|
||||||
|
slots[index] = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SymbolSlot getSlot(int index) {
|
||||||
|
if (index >= 0 && index < NUM_SLOTS) {
|
||||||
|
return slots[index];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single custom symbol bitmap.
|
||||||
|
*/
|
||||||
|
public static class SymbolSlot {
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
||||||
|
private final boolean isTriplePlane;
|
||||||
|
private BufferedImage cachedImage;
|
||||||
|
private int cachedFgRgb = -1;
|
||||||
|
private int cachedBgRgb = -1;
|
||||||
|
|
||||||
|
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
||||||
|
this.width = width > 0 ? width : 9;
|
||||||
|
this.height = height > 0 ? height : 16;
|
||||||
|
this.pixelData = pixelData;
|
||||||
|
this.isTriplePlane = isTriplePlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPixelData() {
|
||||||
|
return pixelData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTriplePlane() {
|
||||||
|
return isTriplePlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders this symbol to a BufferedImage.
|
||||||
|
*/
|
||||||
|
public synchronized BufferedImage getImage(Color fgColor, Color bgColor) {
|
||||||
|
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF;
|
||||||
|
int bgRgb = bgColor != null ? bgColor.getRGB() : 0x00000000;
|
||||||
|
|
||||||
|
if (cachedImage != null && cachedFgRgb == fgRgb && cachedBgRgb == bgRgb) {
|
||||||
|
return cachedImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
int[] rgbArray = new int[width * height];
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int idx = y * width + x;
|
||||||
|
int val = (idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
|
||||||
|
|
||||||
|
if (val == 0) {
|
||||||
|
rgbArray[idx] = bgRgb;
|
||||||
|
} else if (!isTriplePlane) {
|
||||||
|
rgbArray[idx] = fgRgb;
|
||||||
|
} else {
|
||||||
|
// Triple-Plane RGB composite:
|
||||||
|
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||||
|
int r = (val & 0x01) != 0 ? 255 : 0;
|
||||||
|
int g = (val & 0x02) != 0 ? 255 : 0;
|
||||||
|
int b = (val & 0x04) != 0 ? 255 : 0;
|
||||||
|
rgbArray[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.setRGB(0, 0, width, height, rgbArray, 0, width);
|
||||||
|
this.cachedImage = img;
|
||||||
|
this.cachedFgRgb = fgRgb;
|
||||||
|
this.cachedBgRgb = bgRgb;
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the symbol directly onto a Graphics2D surface with aspect scaling.
|
||||||
|
*/
|
||||||
|
public void draw(Graphics2D g2d, int x, int y, int cellWidth, int cellHeight, Color fgColor, Color bgColor) {
|
||||||
|
BufferedImage img = getImage(fgColor, bgColor);
|
||||||
|
if (img != null) {
|
||||||
|
g2d.drawImage(img, x, y, cellWidth, cellHeight, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,12 @@ public class InputProcessor {
|
|||||||
this.fsm = fsm;
|
this.fsm = fsm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private org.lib3270j.graphics.GraphicsPlane graphicsPlane;
|
||||||
|
|
||||||
|
public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) {
|
||||||
|
this.graphicsPlane = gp;
|
||||||
|
}
|
||||||
|
|
||||||
public enum OiaStatus {
|
public enum OiaStatus {
|
||||||
NOT_CONNECTED("OFFLINE"),
|
NOT_CONNECTED("OFFLINE"),
|
||||||
X_SYSTEM("X SYSTEM"),
|
X_SYSTEM("X SYSTEM"),
|
||||||
@@ -138,9 +144,10 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Advance cursor
|
// Advance cursor
|
||||||
|
int startAdvance = baddr;
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
// Skip over field attributes
|
// Skip over field attributes safely
|
||||||
while (screen.getCell(baddr).isFieldAttribute()) {
|
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance) {
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
@@ -159,6 +166,9 @@ public class InputProcessor {
|
|||||||
if (aidCode == AID_CLEAR) {
|
if (aidCode == AID_CLEAR) {
|
||||||
screen.clear();
|
screen.clear();
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
if (graphicsPlane != null) {
|
||||||
|
graphicsPlane.clear();
|
||||||
|
}
|
||||||
// Send just the AID
|
// Send just the AID
|
||||||
byte[] data = new byte[] { (byte) aidCode };
|
byte[] data = new byte[] { (byte) aidCode };
|
||||||
sendAidResponse(data);
|
sendAidResponse(data);
|
||||||
@@ -255,18 +265,26 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unformatted screen in 3270 mode:
|
// Unformatted screen in 3270 mode (e.g. line-mode console):
|
||||||
// Send character data from address 0 up to last non-null character
|
// Send AID + cursor address + only the active input line (row containing cursor)
|
||||||
int size = screen.getRows() * screen.getCols();
|
int cols = screen.getCols();
|
||||||
int lastNonNull = -1;
|
int curAddr = screen.getCursorAddress();
|
||||||
for (int i = size - 1; i >= 0; i--) {
|
int row = curAddr / cols;
|
||||||
if (screen.getCell(i).ec != 0) {
|
int rowStart = row * cols;
|
||||||
lastNonNull = i;
|
int rowEnd = rowStart + cols;
|
||||||
|
|
||||||
|
// Find last non-null, non-blank character on the current line
|
||||||
|
int lastChar = rowStart - 1;
|
||||||
|
for (int i = rowEnd - 1; i >= rowStart; i--) {
|
||||||
|
int ec = screen.getCell(i).ec & 0xFF;
|
||||||
|
if (ec != 0x00 && ec != 0x40) {
|
||||||
|
lastChar = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lastNonNull >= 0) {
|
|
||||||
for (int i = 0; i <= lastNonNull; i++) {
|
if (lastChar >= rowStart) {
|
||||||
|
for (int i = rowStart; i <= lastChar; i++) {
|
||||||
int b = screen.getCell(i).ec & 0xFF;
|
int b = screen.getCell(i).ec & 0xFF;
|
||||||
out.write(b != 0 ? b : 0x40);
|
out.write(b != 0 ? b : 0x40);
|
||||||
}
|
}
|
||||||
@@ -325,6 +343,13 @@ public class InputProcessor {
|
|||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getLastAid() { return lastAid; }
|
||||||
|
public void setLastAid(int aid) { this.lastAid = aid; }
|
||||||
|
|
||||||
|
public void setCursorAddress(int baddr) {
|
||||||
|
screen.setCursorAddress(baddr);
|
||||||
|
}
|
||||||
|
|
||||||
public void backTab() {
|
public void backTab() {
|
||||||
// Find previous unprotected field
|
// Find previous unprotected field
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
|
|||||||
@@ -208,10 +208,19 @@ public final class DS3270Constants {
|
|||||||
public static final int QR_COLOR = 0x86;
|
public static final int QR_COLOR = 0x86;
|
||||||
public static final int QR_HIGHLIGHTING = 0x87;
|
public static final int QR_HIGHLIGHTING = 0x87;
|
||||||
public static final int QR_REPLY_MODES = 0x88;
|
public static final int QR_REPLY_MODES = 0x88;
|
||||||
|
public static final int QR_SAVE_RESTORE = 0x8c;
|
||||||
public static final int QR_DBCS_ASIA = 0x91;
|
public static final int QR_DBCS_ASIA = 0x91;
|
||||||
public static final int QR_DDM = 0x95;
|
public static final int QR_DDM = 0x95;
|
||||||
|
public static final int QR_TRANSPARENCY = 0x99;
|
||||||
public static final int QR_RPQNAMES = 0xa1;
|
public static final int QR_RPQNAMES = 0xa1;
|
||||||
public static final int QR_IMP_PART = 0xa6;
|
public static final int QR_IMP_PART = 0xa6;
|
||||||
|
public static final int QR_RPQ_NAMES = 0xa8;
|
||||||
|
public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics
|
||||||
|
public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes
|
||||||
|
public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device
|
||||||
|
public static final int QR_OEM_FMT = 0xb3; // OEM Format
|
||||||
|
public static final int QR_GCOLOR = 0xb4; // Graphic Color Table
|
||||||
|
public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets
|
||||||
public static final int QR_NULL = 0xff;
|
public static final int QR_NULL = 0xff;
|
||||||
|
|
||||||
// ========== Screen model sizes ==========
|
// ========== Screen model sizes ==========
|
||||||
|
|||||||
@@ -28,26 +28,42 @@ public class TelnetConnection {
|
|||||||
private final TelnetFSM fsm;
|
private final TelnetFSM fsm;
|
||||||
private final ConnectionConfig config;
|
private final ConnectionConfig config;
|
||||||
|
|
||||||
|
private javax.net.ssl.SSLSession sslSession;
|
||||||
|
|
||||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.fsm = fsm;
|
this.fsm = fsm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public javax.net.ssl.SSLSession getSslSession() {
|
||||||
|
return sslSession;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to the host. Blocks until connection is established or fails.
|
* Connect to the host. Blocks until connection is established or fails.
|
||||||
*/
|
*/
|
||||||
public void connect() throws IOException {
|
public void connect() throws IOException {
|
||||||
if (config.isUseTls()) {
|
if (config.isUseTls()) {
|
||||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort());
|
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
||||||
javax.net.ssl.SSLSocketFactory ssf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
|
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
||||||
|
try {
|
||||||
|
javax.net.ssl.SSLContext sslContext = org.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||||
|
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||||
sslSocket.setKeepAlive(true);
|
sslSocket.setKeepAlive(true);
|
||||||
sslSocket.setOOBInline(true);
|
|
||||||
sslSocket.setTcpNoDelay(true);
|
sslSocket.setTcpNoDelay(true);
|
||||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||||
config.getConnectTimeoutMs());
|
config.getConnectTimeoutMs());
|
||||||
sslSocket.startHandshake();
|
sslSocket.startHandshake();
|
||||||
socket = sslSocket;
|
socket = sslSocket;
|
||||||
|
sslSession = sslSocket.getSession();
|
||||||
|
log.info("TLS session active: protocol=" + sslSession.getProtocol() +
|
||||||
|
" cipher=" + sslSession.getCipherSuite());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException("TLS setup failure: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||||
socket = new Socket();
|
socket = new Socket();
|
||||||
@@ -132,10 +148,14 @@ public class TelnetConnection {
|
|||||||
if (log.isLoggable(Level.FINE)) {
|
if (log.isLoggable(Level.FINE)) {
|
||||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
fsm.feedByte(buf[i] & 0xFF);
|
fsm.feedByte(buf[i] & 0xFF);
|
||||||
}
|
}
|
||||||
fsm.endOfNetworkData();
|
fsm.endOfNetworkData();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
@@ -148,6 +168,11 @@ public class TelnetConnection {
|
|||||||
log.log(Level.WARNING, "Read error", e);
|
log.log(Level.WARNING, "Read error", e);
|
||||||
fsm.onError("Read error: " + e.getMessage());
|
fsm.onError("Read error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (running) {
|
||||||
|
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
|
||||||
|
fsm.onError("Network loop error: " + t.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.lib3270j.tls;
|
||||||
|
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface for validating TLS server certificates.
|
||||||
|
* Used when standard certificate path validation fails or when custom verification is needed.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface TlsCertificateVerifier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether to trust an unverified server certificate chain.
|
||||||
|
*
|
||||||
|
* @param chain The peer certificate chain presented by the server.
|
||||||
|
* @param authType The key exchange algorithm (e.g., "RSA", "ECDHE_RSA").
|
||||||
|
* @param exception The CertificateException thrown by standard validation (or null if called proactively).
|
||||||
|
* @return true to trust the certificate and proceed with the connection; false to abort.
|
||||||
|
*/
|
||||||
|
boolean shouldTrust(X509Certificate[] chain, String authType, CertificateException exception);
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package org.lib3270j.tls;
|
||||||
|
|
||||||
|
import org.lib3270j.ConnectionConfig;
|
||||||
|
|
||||||
|
import javax.net.ssl.*;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom X509TrustManager that supports:
|
||||||
|
* 1. Standard certificate verification using the JVM default TrustManager.
|
||||||
|
* 2. Unverified / trust-all mode when tlsVerifyCert is false.
|
||||||
|
* 3. Interactive/custom certificate verifier callbacks (e.g. GUI prompts for self-signed certificates).
|
||||||
|
*/
|
||||||
|
public class TlsTrustManager implements X509TrustManager {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(TlsTrustManager.class.getName());
|
||||||
|
|
||||||
|
private final ConnectionConfig config;
|
||||||
|
private X509TrustManager defaultTrustManager;
|
||||||
|
|
||||||
|
public TlsTrustManager(ConnectionConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
try {
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
tmf.init((KeyStore) null);
|
||||||
|
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||||
|
if (tm instanceof X509TrustManager) {
|
||||||
|
this.defaultTrustManager = (X509TrustManager) tm;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "Failed to initialize default TrustManagerFactory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||||
|
if (defaultTrustManager != null) {
|
||||||
|
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||||
|
if (config != null && !config.isTlsVerifyCert()) {
|
||||||
|
log.fine("Certificate verification bypassed (tlsVerifyCert=false)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chain == null || chain.length == 0) {
|
||||||
|
CertificateException ex = new CertificateException("null or zero-length certificate chain");
|
||||||
|
if (config != null && config.getCertificateVerifier() != null) {
|
||||||
|
if (config.getCertificateVerifier().shouldTrust(chain, authType, ex)) {
|
||||||
|
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (defaultTrustManager != null) {
|
||||||
|
defaultTrustManager.checkServerTrusted(chain, authType);
|
||||||
|
} else {
|
||||||
|
throw new CertificateException("No default X509TrustManager available");
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
CertificateException certEx = (ex instanceof CertificateException)
|
||||||
|
? (CertificateException) ex
|
||||||
|
: new CertificateException("Certificate validation failed: " + ex.getMessage(), ex);
|
||||||
|
|
||||||
|
log.log(Level.FINE, "Standard certificate validation failed: " + certEx.getMessage(), certEx);
|
||||||
|
|
||||||
|
if (config != null && config.getCertificateVerifier() != null) {
|
||||||
|
boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, certEx);
|
||||||
|
if (accepted) {
|
||||||
|
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log.warning("Server certificate rejected by TlsCertificateVerifier callback");
|
||||||
|
throw new CertificateException("Certificate rejected by user/verifier: " + certEx.getMessage(), certEx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw certEx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
if (defaultTrustManager != null) {
|
||||||
|
return defaultTrustManager.getAcceptedIssuers();
|
||||||
|
}
|
||||||
|
return new X509Certificate[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an initialized SSLContext configured for the given ConnectionConfig.
|
||||||
|
*/
|
||||||
|
public static SSLContext createSSLContext(ConnectionConfig config) throws Exception {
|
||||||
|
String protocol = (config != null && config.getSslProtocol() != null)
|
||||||
|
? config.getSslProtocol()
|
||||||
|
: "TLS";
|
||||||
|
SSLContext sslContext = SSLContext.getInstance(protocol);
|
||||||
|
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||||
|
sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
|
||||||
|
return sslContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package org.lib3270j.datastream;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.lib3270j.TerminalModel;
|
import org.lib3270j.TerminalModel;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import org.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import org.lib3270j.graphics.GraphicsMode;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import org.lib3270j.screen.ScreenBuffer;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||||
@@ -14,11 +15,43 @@ public class QueryReplyBuilderTest {
|
|||||||
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBuildAllQueryReplies() {
|
public void testBuildAllQueryRepliesDefaultNone() {
|
||||||
|
assertEquals(GraphicsMode.NONE, qrBuilder.getGraphicsMode());
|
||||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||||
assertNotNull(replies);
|
assertNotNull(replies);
|
||||||
assertTrue(replies.length > 0);
|
assertTrue(replies.length > 0);
|
||||||
assertEquals((byte) AID_SF, replies[0]);
|
assertEquals((byte) AID_SF, replies[0]);
|
||||||
|
|
||||||
|
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be present
|
||||||
|
boolean hasB0 = false;
|
||||||
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||||
|
hasB0 = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertFalse(hasB0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildAllQueryRepliesWithVectorGraphics() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||||
|
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||||
|
assertNotNull(replies);
|
||||||
|
|
||||||
|
// Vector Graphics QR 0xB0 and 0xB4 must be present
|
||||||
|
boolean hasB0 = false;
|
||||||
|
boolean hasB4 = false;
|
||||||
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||||
|
hasB0 = true;
|
||||||
|
}
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) {
|
||||||
|
hasB4 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(hasB0);
|
||||||
|
assertTrue(hasB4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -52,4 +85,29 @@ public class QueryReplyBuilderTest {
|
|||||||
assertEquals(0x81, replies[3] & 0xFF);
|
assertEquals(0x81, replies[3] & 0xFF);
|
||||||
assertEquals(QR_NULL, replies[4] & 0xFF);
|
assertEquals(QR_NULL, replies[4] & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testImplicitPartitionModel4() {
|
||||||
|
byte[] requested = new byte[] { (byte) QR_IMP_PART };
|
||||||
|
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||||
|
|
||||||
|
assertNotNull(replies);
|
||||||
|
assertEquals((byte) AID_SF, replies[0]);
|
||||||
|
int len = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||||
|
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||||
|
assertEquals(QR_IMP_PART, replies[4] & 0xFF); // 0xA6
|
||||||
|
|
||||||
|
// Payload starts at index 5: 22 bytes total
|
||||||
|
// Default size = 80 x 24
|
||||||
|
int defCols = ((replies[10] & 0xFF) << 8) | (replies[11] & 0xFF);
|
||||||
|
int defRows = ((replies[12] & 0xFF) << 8) | (replies[13] & 0xFF);
|
||||||
|
assertEquals(80, defCols);
|
||||||
|
assertEquals(24, defRows);
|
||||||
|
|
||||||
|
// Alt size = 80 x 43
|
||||||
|
int altCols = ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF);
|
||||||
|
int altRows = ((replies[16] & 0xFF) << 8) | (replies[17] & 0xFF);
|
||||||
|
assertEquals(80, altCols);
|
||||||
|
assertEquals(43, altRows);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class GocaDecoderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasicLineAndColorOrders() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
assertFalse(plane.hasContent());
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// Set Color to Red (GOCA Color 2) - 2 bytes: order, color
|
||||||
|
out.write(GocaConstants.G_GSCOL);
|
||||||
|
out.write(0x02); // Red
|
||||||
|
|
||||||
|
// Line from (1000, 1000) to (2000, 2000) - long order: order, len, x1, y1, x2, y2
|
||||||
|
out.write(GocaConstants.G_GLINE);
|
||||||
|
out.write(0x08); // 8 bytes: (x1, y1, x2, y2)
|
||||||
|
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||||
|
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||||
|
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||||
|
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeStream(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAreaAndPatternFill() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// Set Pattern to Solid - 2 bytes: order, pattern
|
||||||
|
out.write(GocaConstants.G_GSPT);
|
||||||
|
out.write(GocaConstants.PT_SOLID);
|
||||||
|
|
||||||
|
// Begin Area - 2 bytes: order, flags
|
||||||
|
out.write(GocaConstants.G_GBAR);
|
||||||
|
out.write(0x00);
|
||||||
|
|
||||||
|
// Polyline forming a triangle: (500, 500) -> (1500, 500) -> (1000, 1500) -> (500, 500)
|
||||||
|
out.write(GocaConstants.G_GLINE);
|
||||||
|
out.write(0x10); // 4 points * 4 bytes = 16 bytes
|
||||||
|
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||||
|
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||||
|
|
||||||
|
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||||
|
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||||
|
|
||||||
|
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||||
|
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||||
|
|
||||||
|
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||||
|
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||||
|
|
||||||
|
// End Area - 1 byte
|
||||||
|
out.write(GocaConstants.G_GEAR);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeStream(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMarkerAndVectorText() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// Set Marker Type to Circle - 2 bytes: order, type
|
||||||
|
out.write(GocaConstants.G_GSMT);
|
||||||
|
out.write(GocaConstants.MK_CIRCLE);
|
||||||
|
|
||||||
|
// Draw Marker at (2000, 2000)
|
||||||
|
out.write(GocaConstants.G_GMRK);
|
||||||
|
out.write(0x04);
|
||||||
|
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||||
|
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||||
|
|
||||||
|
// Draw Vector Stroked Character String: "IBM" (EBCDIC: 0xC9, 0xC2, 0xD4)
|
||||||
|
out.write(GocaConstants.G_GCHST);
|
||||||
|
out.write(0x07); // 4 bytes pos + 3 bytes chars
|
||||||
|
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||||
|
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||||
|
out.write(0xC9); out.write(0xC2); out.write(0xD4);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeStream(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcedureOrdersAndErase() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// Draw something first
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GLINE);
|
||||||
|
out.write(0x08);
|
||||||
|
out.write(0x00); out.write(0x00);
|
||||||
|
out.write(0x00); out.write(0x00);
|
||||||
|
out.write(0x00); out.write(0x64);
|
||||||
|
out.write(0x00); out.write(0x64);
|
||||||
|
byte[] drawStream = out.toByteArray();
|
||||||
|
decoder.decodeStream(drawStream, 0, drawStream.length);
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
|
||||||
|
// Process procedure order 0x0A (Erase presentation space)
|
||||||
|
byte[] procOrders = new byte[] { (byte) GocaConstants.P_ERASE };
|
||||||
|
decoder.processProcedureOrders(procOrders, 0, procOrders.length);
|
||||||
|
assertFalse(plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSegmentAndRelativeLineOrders() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(720, 688);
|
||||||
|
plane.setScreenDimensions(80, 43);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// 0x70: Begin Segment (14 bytes total)
|
||||||
|
out.write(GocaConstants.G_BEGSEGM);
|
||||||
|
out.write(0x0C);
|
||||||
|
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x01); // Seg ID 1
|
||||||
|
out.write(0x74); out.write(0x70);
|
||||||
|
out.write(0x00); out.write(0x1E); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
|
|
||||||
|
// 0x3E: End Prologue
|
||||||
|
out.write(GocaConstants.G_ENDPROLOGUE);
|
||||||
|
out.write(0x00);
|
||||||
|
|
||||||
|
// 0x0A: Color (7 = White)
|
||||||
|
out.write(GocaConstants.G_GSCOL);
|
||||||
|
out.write(0x07);
|
||||||
|
|
||||||
|
// 0xE1: Relative Line (Absolute Start (300, 200), deltas: (+10, -5), (-10, +5))
|
||||||
|
out.write(GocaConstants.G_GRLINE);
|
||||||
|
out.write(0x08); // 4 bytes start coord + 4 bytes (2 deltas) = 8 bytes
|
||||||
|
out.write(0x01); out.write(0x2C); // Start X = 300
|
||||||
|
out.write(0x00); out.write(0xC8); // Start Y = 200
|
||||||
|
out.write(0x0A); out.write(0xFB); // dx = +10, dy = -5
|
||||||
|
out.write(0xF6); out.write(0x05); // dx = -10, dy = +5
|
||||||
|
|
||||||
|
// 0x71: End Segment
|
||||||
|
out.write(GocaConstants.G_ENDSEGM);
|
||||||
|
out.write(0x00);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeStream(stream, 0, stream.length);
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPartialOrderBufferingAcrossStreams() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// First packet sends G_GLINE order code and half of its coordinates
|
||||||
|
ByteArrayOutputStream chunk1 = new ByteArrayOutputStream();
|
||||||
|
chunk1.write(GocaConstants.G_GLINE);
|
||||||
|
chunk1.write(0x08); // 8 bytes: 2 points
|
||||||
|
chunk1.write(0x00); chunk1.write(0x64); // x1 = 100
|
||||||
|
chunk1.write(0x00); chunk1.write(0x64); // y1 = 100
|
||||||
|
|
||||||
|
byte[] b1 = chunk1.toByteArray();
|
||||||
|
decoder.decodeStream(b1, 0, b1.length);
|
||||||
|
// Order is not complete yet, so no line drawn yet
|
||||||
|
assertFalse(plane.hasContent());
|
||||||
|
|
||||||
|
// Second packet sends the rest of the order: x2 = 200, y2 = 200
|
||||||
|
ByteArrayOutputStream chunk2 = new ByteArrayOutputStream();
|
||||||
|
chunk2.write(0x00); chunk2.write(0xC8); // x2 = 200
|
||||||
|
chunk2.write(0x00); chunk2.write(0xC8); // y2 = 200
|
||||||
|
|
||||||
|
byte[] b2 = chunk2.toByteArray();
|
||||||
|
decoder.decodeStream(b2, 0, b2.length);
|
||||||
|
// Now the combined order executes!
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetCurrentPositionAndRelativeLine() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// 0x21: Set Current Position to (50, 50)
|
||||||
|
out.write(GocaConstants.G_GSCP);
|
||||||
|
out.write(0x04); // len = 4
|
||||||
|
out.write(0x00); out.write(0x32); // x = 50
|
||||||
|
out.write(0x00); out.write(0x32); // y = 50
|
||||||
|
|
||||||
|
// 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10)
|
||||||
|
out.write(GocaConstants.G_GCRLIN);
|
||||||
|
out.write(0x04); // len = 4 (2 steps)
|
||||||
|
out.write(0x0A); out.write(0x14); // dx = +10, dy = +20
|
||||||
|
out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeStream(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
assertEquals(55, decoder.getCurX());
|
||||||
|
assertEquals(60, decoder.getCurY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test3179GCoordinateMapping() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(720, 688);
|
||||||
|
plane.setScreenDimensions(80, 43);
|
||||||
|
|
||||||
|
// Screen center (0, 0) should map to canvas center (360, 343)
|
||||||
|
assertEquals(360, plane.mapX(0));
|
||||||
|
assertEquals(343, plane.mapY(0));
|
||||||
|
|
||||||
|
// Left edge (-360) should map to 0
|
||||||
|
assertEquals(0, plane.mapX(-360));
|
||||||
|
// Right edge (+359) should map to 719
|
||||||
|
assertEquals(719, plane.mapX(359));
|
||||||
|
|
||||||
|
// Top edge (+343) should map to 0
|
||||||
|
assertEquals(0, plane.mapY(343));
|
||||||
|
// Bottom edge (-344) should map to 687
|
||||||
|
assertEquals(687, plane.mapY(-344));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.lib3270j.ConnectionConfig;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class GraphicsModeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultsAndParsing() {
|
||||||
|
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(null));
|
||||||
|
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(""));
|
||||||
|
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("NONE"));
|
||||||
|
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("off"));
|
||||||
|
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("false"));
|
||||||
|
|
||||||
|
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("ps"));
|
||||||
|
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("programmed_symbols"));
|
||||||
|
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("apl"));
|
||||||
|
|
||||||
|
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector"));
|
||||||
|
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("goca"));
|
||||||
|
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector_graphics"));
|
||||||
|
|
||||||
|
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("both"));
|
||||||
|
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("all"));
|
||||||
|
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("true"));
|
||||||
|
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("on"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFlags() {
|
||||||
|
assertFalse(GraphicsMode.NONE.isProgrammedSymbolsEnabled());
|
||||||
|
assertFalse(GraphicsMode.NONE.isVectorGraphicsEnabled());
|
||||||
|
|
||||||
|
assertTrue(GraphicsMode.PROGRAMMED_SYMBOLS.isProgrammedSymbolsEnabled());
|
||||||
|
assertFalse(GraphicsMode.PROGRAMMED_SYMBOLS.isVectorGraphicsEnabled());
|
||||||
|
|
||||||
|
assertFalse(GraphicsMode.VECTOR_GRAPHICS.isProgrammedSymbolsEnabled());
|
||||||
|
assertTrue(GraphicsMode.VECTOR_GRAPHICS.isVectorGraphicsEnabled());
|
||||||
|
|
||||||
|
assertTrue(GraphicsMode.BOTH.isProgrammedSymbolsEnabled());
|
||||||
|
assertTrue(GraphicsMode.BOTH.isVectorGraphicsEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConnectionConfigDefault() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig();
|
||||||
|
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||||
|
|
||||||
|
config.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
|
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
|
||||||
|
|
||||||
|
config.setGraphicsMode(null);
|
||||||
|
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class ProgramSymbolManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetAllocationAndLookup() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
// Loadable set can be queried after loadps or clearAll
|
||||||
|
assertNull(manager.getSymbolSet(0x40));
|
||||||
|
assertNull(manager.getSymbol(0x40, 0x41));
|
||||||
|
|
||||||
|
manager.clearAll();
|
||||||
|
assertNull(manager.getSymbolSet(0x40));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSinglePlaneLoadPsFormat1() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// Build Format 1 Load PS payload for LCID 0x40, code point 0x41 ('A')
|
||||||
|
// Format 1 header:
|
||||||
|
// byte 0: flags (0x01: format 1, single plane)
|
||||||
|
// byte 1: LCID (0x40)
|
||||||
|
// byte 2: Start code point (0x41)
|
||||||
|
// byte 3: RWS (0x02: loadable slot 2)
|
||||||
|
// followed by 18 bytes per symbol for 9x16 cell:
|
||||||
|
// Byte 0-1: Col 0 for rows 0..15 (0x80 = bit for row 0 col 0)
|
||||||
|
// Bytes 2-17: Cols 1..8 for rows 0..15 (0xFF = bits for row 0 cols 1..8)
|
||||||
|
byte[] payload = new byte[4 + 18];
|
||||||
|
payload[0] = 0x01; // Format 1
|
||||||
|
payload[1] = 0x40; // LCID 0x40
|
||||||
|
payload[2] = 0x41; // Code point 0x41
|
||||||
|
payload[3] = 0x02; // RWS 2
|
||||||
|
|
||||||
|
payload[4] = (byte) 0x80; // Row 0 Col 0 bit
|
||||||
|
payload[5] = (byte) 0x00; // Rows 8..15 Col 0
|
||||||
|
for (int r = 0; r < 16; r++) {
|
||||||
|
payload[6 + r] = (byte) 0xFF; // Cols 1..8 on all rows
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.loadps(payload);
|
||||||
|
|
||||||
|
ProgramSymbolSet set = manager.getSymbolSet(0x40);
|
||||||
|
assertNotNull(set);
|
||||||
|
assertEquals(0x40, set.getLcid());
|
||||||
|
assertFalse(set.isTriplePlane());
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||||
|
assertNotNull(slot);
|
||||||
|
assertEquals(9, slot.getWidth());
|
||||||
|
assertEquals(16, slot.getHeight());
|
||||||
|
assertFalse(slot.isTriplePlane());
|
||||||
|
|
||||||
|
// Verify pixel data (Row 0 Col 0 is 1)
|
||||||
|
byte[] pixels = slot.getPixelData();
|
||||||
|
assertNotNull(pixels);
|
||||||
|
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
|
||||||
|
|
||||||
|
// Test rendering into BufferedImage
|
||||||
|
BufferedImage canvas = new BufferedImage(80, 24, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g2 = canvas.createGraphics();
|
||||||
|
boolean drawn = manager.drawSymbol(g2, 0x40, 0x41, 0, 0, 10, 20, Color.GREEN, Color.BLACK);
|
||||||
|
g2.dispose();
|
||||||
|
assertTrue(drawn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTriplePlaneMultiColorComposite() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// Build Format 1 Load PS payload for LCID 0x42, triple plane
|
||||||
|
// flags = 0x01 (Format 1)
|
||||||
|
// RWS = 4 (Triple Plane)
|
||||||
|
byte[] payload = new byte[4 + 18];
|
||||||
|
payload[0] = 0x01; // Format 1
|
||||||
|
payload[1] = 0x42; // LCID 0x42
|
||||||
|
payload[2] = 0x45; // Code point 0x45
|
||||||
|
payload[3] = 0x04; // RWS 4 (Triple Plane)
|
||||||
|
|
||||||
|
// Set pixel at row 0 col 0
|
||||||
|
payload[4] = (byte) 0x80; // Row 0 Col 0
|
||||||
|
payload[5] = (byte) 0x00;
|
||||||
|
for (int r = 0; r < 16; r++) {
|
||||||
|
payload[6 + r] = (byte) 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.loadps(payload);
|
||||||
|
|
||||||
|
ProgramSymbolSet set = manager.getSymbolSet(0x42);
|
||||||
|
assertNotNull(set);
|
||||||
|
assertTrue(set.isTriplePlane());
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, 0x45);
|
||||||
|
assertNotNull(slot);
|
||||||
|
assertTrue(slot.isTriplePlane());
|
||||||
|
|
||||||
|
// When loaded with colorPlane=0 (default), all 3 planes are set (val 7 = white composite)
|
||||||
|
byte[] pixels = slot.getPixelData();
|
||||||
|
assertEquals(7, pixels[0] & 0xFF);
|
||||||
|
|
||||||
|
// Render image
|
||||||
|
BufferedImage img = slot.getImage(Color.WHITE, Color.BLACK);
|
||||||
|
assertNotNull(img);
|
||||||
|
|
||||||
|
// Pixel (0,0) should be white composite (Red=255, Green=255, Blue=255)
|
||||||
|
int rgb = img.getRGB(0, 0);
|
||||||
|
Color pixelColor = new Color(rgb, true);
|
||||||
|
assertEquals(255, pixelColor.getRed());
|
||||||
|
assertEquals(255, pixelColor.getGreen());
|
||||||
|
assertEquals(255, pixelColor.getBlue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormat3Bitstream() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// Format 3 payload
|
||||||
|
// byte 0: 0x03 (Format 3)
|
||||||
|
// byte 1: LCID (0x44)
|
||||||
|
// byte 2: code point (0x43)
|
||||||
|
// byte 3: RWS (2)
|
||||||
|
// followed by (9 * 16 + 7) / 8 = 18 bytes
|
||||||
|
byte[] payload = new byte[4 + 18];
|
||||||
|
payload[0] = 0x03; // Format 3
|
||||||
|
payload[1] = 0x44; // LCID 0x44
|
||||||
|
payload[2] = 0x43; // Code point 0x43
|
||||||
|
payload[3] = 0x02; // RWS 2
|
||||||
|
payload[4] = (byte) 0x80; // First pixel is set
|
||||||
|
|
||||||
|
manager.loadps(payload);
|
||||||
|
|
||||||
|
ProgramSymbolSet set = manager.getSymbolSet(0x44);
|
||||||
|
assertNotNull(set);
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x44, 0x43);
|
||||||
|
assertNotNull(slot);
|
||||||
|
assertEquals(1, slot.getPixelData()[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package org.lib3270j.tls;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.lib3270j.ConnectionConfig;
|
||||||
|
import org.lib3270j.TerminalModel;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class TlsConfigTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConnectionConfigDefaults() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||||
|
assertFalse(config.isUseTls());
|
||||||
|
assertTrue(config.isTlsVerifyCert());
|
||||||
|
assertEquals("TLS", config.getSslProtocol());
|
||||||
|
assertNull(config.getCertificateVerifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseHostStringTlsPrefixes() {
|
||||||
|
// L: prefix
|
||||||
|
ConnectionConfig c1 = ConnectionConfig.parseHostString("L:mainframe.example.com", 0, TerminalModel.IBM_3279_4);
|
||||||
|
assertTrue(c1.isUseTls());
|
||||||
|
assertEquals("mainframe.example.com", c1.getHost());
|
||||||
|
assertEquals(992, c1.getPort());
|
||||||
|
|
||||||
|
// ssl: prefix with explicit port
|
||||||
|
ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_2);
|
||||||
|
assertTrue(c2.isUseTls());
|
||||||
|
assertEquals("zos.local", c2.getHost());
|
||||||
|
assertEquals(2323, c2.getPort());
|
||||||
|
|
||||||
|
// y: prefix
|
||||||
|
ConnectionConfig c3 = ConnectionConfig.parseHostString("y:vm370.net:992", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertTrue(c3.isUseTls());
|
||||||
|
assertEquals("vm370.net", c3.getHost());
|
||||||
|
assertEquals(992, c3.getPort());
|
||||||
|
|
||||||
|
// Plain host
|
||||||
|
ConnectionConfig c4 = ConnectionConfig.parseHostString("plain.host.com", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertFalse(c4.isUseTls());
|
||||||
|
assertEquals("plain.host.com", c4.getHost());
|
||||||
|
assertEquals(23, c4.getPort());
|
||||||
|
|
||||||
|
// IPv6 host with brackets
|
||||||
|
ConnectionConfig c5 = ConnectionConfig.parseHostString("L:[2001:db8::1]:992", 0, TerminalModel.IBM_3279_4);
|
||||||
|
assertTrue(c5.isUseTls());
|
||||||
|
assertEquals("2001:db8::1", c5.getHost());
|
||||||
|
assertEquals(992, c5.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTlsTrustManagerBypass() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("untrusted.host", 992);
|
||||||
|
config.setUseTls(true);
|
||||||
|
config.setTlsVerifyCert(false);
|
||||||
|
|
||||||
|
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||||
|
// With tlsVerifyCert=false, checkServerTrusted must not throw even with null/empty chain
|
||||||
|
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTlsTrustManagerVerifierCallback() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("selfsigned.host", 992);
|
||||||
|
config.setUseTls(true);
|
||||||
|
config.setTlsVerifyCert(true);
|
||||||
|
|
||||||
|
AtomicBoolean verifierInvoked = new AtomicBoolean(false);
|
||||||
|
config.setCertificateVerifier((chain, authType, exception) -> {
|
||||||
|
verifierInvoked.set(true);
|
||||||
|
return true; // Accept certificate
|
||||||
|
});
|
||||||
|
|
||||||
|
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||||
|
// Standard verification will fail on empty cert chain, triggering our callback
|
||||||
|
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||||
|
assertTrue(verifierInvoked.get(), "Expected verifier callback to be invoked");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTlsTrustManagerVerifierRejection() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("badcert.host", 992);
|
||||||
|
config.setUseTls(true);
|
||||||
|
config.setTlsVerifyCert(true);
|
||||||
|
|
||||||
|
config.setCertificateVerifier((chain, authType, exception) -> false); // Reject certificate
|
||||||
|
|
||||||
|
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||||
|
assertThrows(CertificateException.class, () -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateSSLContext() throws Exception {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("secure.host", 992);
|
||||||
|
config.setUseTls(true);
|
||||||
|
|
||||||
|
SSLContext context = TlsTrustManager.createSSLContext(config);
|
||||||
|
assertNotNull(context);
|
||||||
|
assertNotNull(context.getSocketFactory());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user