Clear cruft
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 26s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Failing after 1m1s

This commit is contained in:
2026-09-07 20:16:00 -04:00
parent 50a4279446
commit dbcb0aa1b6
66 changed files with 5579 additions and 443 deletions
@@ -70,13 +70,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
setLocationRelativeTo(null); setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400)); setMinimumSize(new Dimension(640, 400));
// Status refresh timer — also ensures focus stays on terminal // Status refresh timer — also ensures focus stays on terminal (guarding against focus stealing)
refreshTimer = new Timer(100, e -> { refreshTimer = new Timer(100, e -> {
if (client != null) { if (client != null) {
statusBar.updateStatus(); statusBar.updateStatus();
syncModeMenuItems(); syncModeMenuItems();
if (isActive() && !terminalPanel.hasFocus()) { if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
terminalPanel.requestFocusInWindow(); if (!terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
} }
} }
}); });
@@ -96,11 +98,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
@Override @Override
public void windowActivated(WindowEvent e) { public void windowActivated(WindowEvent e) {
terminalPanel.requestFocusInWindow(); if (!isAnyChildDialogActive()) {
terminalPanel.requestFocusInWindow();
}
} }
}); });
} }
private boolean isAnyChildDialogActive() {
Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
if (focusedWindow != null && focusedWindow != this) {
return true;
}
if (findDialog != null && findDialog.isVisible()) return true;
if (scriptDialog != null && scriptDialog.isVisible()) return true;
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
for (Window owned : getOwnedWindows()) {
if (owned != null && owned.isVisible()) {
return true;
}
}
return false;
}
private void buildUI() { private void buildUI() {
terminalPanel = new TerminalPanel(); terminalPanel = new TerminalPanel();
statusBar = new StatusBar(); statusBar = new StatusBar();
@@ -743,13 +764,26 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
fileTransfer.cancel(); fileTransfer.cancel();
fileTransfer = null; fileTransfer = null;
} }
client.disconnect(); Telnet3270Client c = client;
client = null; client = null;
terminalPanel.setClient(null); new Thread(() -> {
statusBar.setClient(null, terminalPanel); try {
syncModeMenuItems(); c.disconnect();
terminalPanel.repaint(); } catch (Exception ignored) {}
setTitle("j3270 — Java TN3270 Terminal Emulator"); }, "Disconnect-Thread").start();
Runnable uiReset = () -> {
terminalPanel.setClient(null);
statusBar.setClient(null, terminalPanel);
syncModeMenuItems();
terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator");
};
if (SwingUtilities.isEventDispatchThread()) {
uiReset.run();
} else {
SwingUtilities.invokeLater(uiReset);
}
} }
} }
@@ -790,7 +824,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
statusBar.updateStatus(); statusBar.updateStatus();
terminalPanel.repaint(); terminalPanel.repaint();
if (newState.isFullSession() && !oldState.isFullSession()) { if (newState == ConnectionState.RECONNECTING) {
setTitle("j3270 — " + lastHost + ":" + lastPort + " [Reconnecting...]");
} else if (newState == ConnectionState.NOT_CONNECTED) {
setTitle("j3270 — Java TN3270 Terminal Emulator");
} else if (newState.isFullSession() && !oldState.isFullSession()) {
String tlsIndicator = (client != null && client.getConfig().isUseTls()) ?
(client.getConfig().isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ?
" [" + client.getTelnetFSM().getConnectedLu() + "]" : "";
setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev);
terminalPanel.guardedPack(); terminalPanel.guardedPack();
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
@@ -847,6 +890,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}); });
} }
@Override
public void onCursorMoved(int oldAddress, int newAddress) {
SwingUtilities.invokeLater(() -> {
terminalPanel.repaint();
statusBar.updateStatus();
});
}
@Override
public void onKeyboardUnlocked() {
SwingUtilities.invokeLater(statusBar::updateStatus);
}
// ========== Help dialogs ========== // ========== Help dialogs ==========
private void showKeyMappings() { private void showKeyMappings() {
@@ -949,6 +1005,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
boolean cliTls = false; boolean cliTls = false;
boolean cliNoVerifyCert = false; boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null; Boolean cliTn3270e = null;
Boolean cliAutoSysUnlock = null;
GraphicsMode cliGraphicsMode = null; GraphicsMode cliGraphicsMode = null;
String configFile = null; String configFile = null;
@@ -965,6 +1022,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliTn3270e = false; cliTn3270e = false;
} else if ("--tn3270e".equals(arg)) { } else if ("--tn3270e".equals(arg)) {
cliTn3270e = true; cliTn3270e = true;
} else if ("--auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false;
} else if (arg.startsWith("--graphics=")) { } else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11)); cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) { } else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -1041,6 +1102,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final boolean finalTls = cliTls; final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert; final boolean finalNoVerify = cliNoVerifyCert;
final Boolean finalTn3270e = cliTn3270e; final Boolean finalTn3270e = cliTn3270e;
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
final GraphicsMode finalGraphicsMode = cliGraphicsMode; final GraphicsMode finalGraphicsMode = cliGraphicsMode;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
@@ -1077,6 +1139,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalTn3270e != null) { if (finalTn3270e != null) {
config.setTn3270eEnabled(finalTn3270e); config.setTn3270eEnabled(finalTn3270e);
} }
if (finalAutoSysUnlock != null) {
config.setAutoSysUnlock(finalAutoSysUnlock);
} else {
config.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
}
if (finalGraphicsMode != null) { if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode); config.setGraphicsMode(finalGraphicsMode);
} else { } else {
@@ -1100,6 +1167,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify); config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e); config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) { if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode); config.setGraphicsMode(finalGraphicsMode);
} else { } else {
@@ -105,6 +105,51 @@ public class Settings {
prefs.putBoolean("autoConnectTn3270e", tn3270e); prefs.putBoolean("autoConnectTn3270e", tn3270e);
} }
public static boolean getAutoSysUnlock() {
return prefs.getBoolean("autoSysUnlock", true);
}
public static void setAutoSysUnlock(boolean autoSysUnlock) {
prefs.putBoolean("autoSysUnlock", autoSysUnlock);
flushPrefs();
}
public static boolean getAutoConnectKeepAlive() {
return prefs.getBoolean("autoConnectKeepAlive", true);
}
public static void setAutoConnectKeepAlive(boolean keepAlive) {
prefs.putBoolean("autoConnectKeepAlive", keepAlive);
flushPrefs();
}
public static int getAutoConnectKeepAliveInterval() {
return prefs.getInt("autoConnectKeepAliveInterval", 120);
}
public static void setAutoConnectKeepAliveInterval(int interval) {
prefs.putInt("autoConnectKeepAliveInterval", interval);
flushPrefs();
}
public static boolean getAutoConnectAutoReconnect() {
return prefs.getBoolean("autoConnectAutoReconnect", false);
}
public static void setAutoConnectAutoReconnect(boolean autoReconnect) {
prefs.putBoolean("autoConnectAutoReconnect", autoReconnect);
flushPrefs();
}
public static int getAutoConnectReconnectMaxRetries() {
return prefs.getInt("autoConnectReconnectMaxRetries", 5);
}
public static void setAutoConnectReconnectMaxRetries(int retries) {
prefs.putInt("autoConnectReconnectMaxRetries", retries);
flushPrefs();
}
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name()); String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr); return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
@@ -311,6 +356,26 @@ public class Settings {
prefs.putBoolean("blockSelectMode", block); prefs.putBoolean("blockSelectMode", block);
} }
// ========== Clipboard & Tabular Paste ==========
public static boolean getEnablePasteFromExcel() {
return prefs.getBoolean("enablePasteFromExcel", true);
}
public static void setEnablePasteFromExcel(boolean val) {
prefs.putBoolean("enablePasteFromExcel", val);
flushPrefs();
}
public static boolean getPasteStopAtProtectedLine() {
return prefs.getBoolean("pasteStopAtProtectedLine", false);
}
public static void setPasteStopAtProtectedLine(boolean val) {
prefs.putBoolean("pasteStopAtProtectedLine", val);
flushPrefs();
}
// ========== Crosshair Ruler ========== // ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() { public static boolean getCrosshairRuler() {
@@ -509,11 +574,42 @@ public class Settings {
setDynamicCols(Integer.parseInt(value)); setDynamicCols(Integer.parseInt(value));
break; break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "autoSysUnlock":
case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
setEnablePasteFromExcel(Boolean.parseBoolean(value));
break;
case "pasteStopAtProtectedLine":
case "stopAtProtected":
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
default: default:
log.warning("Unknown behavior/connection key: " + key); log.warning("Unknown behavior/connection key: " + key);
} }
break; break;
case "clipboard":
switch (key) {
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
setEnablePasteFromExcel(Boolean.parseBoolean(value));
break;
case "pasteStopAtProtectedLine":
case "stopAtProtected":
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown clipboard key: " + key);
}
break;
case "colors": case "colors":
if (key.startsWith("color_")) { if (key.startsWith("color_")) {
int index = Integer.parseInt(key.substring(6)); int index = Integer.parseInt(key.substring(6));
@@ -633,6 +729,9 @@ public class Settings {
w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols()); w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
w.println(); w.println();
// [entryassist] // [entryassist]
@@ -0,0 +1,88 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
import haus.nightmare.lib3270j.graphics.PixelBuffer;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
/**
* Desktop Swing/AWT bridge for converting between lib3270j PixelBuffer and java.awt BufferedImage.
*/
public final class AwtPixelBufferBridge {
private AwtPixelBufferBridge() {}
/**
* Converts a PixelBuffer to a BufferedImage.
* Uses TYPE_INT_ARGB with backed raster for optimal Java2D hardware acceleration.
*/
public static BufferedImage toBufferedImage(PixelBuffer buffer) {
if (buffer == null) return null;
int w = buffer.getWidth();
int h = buffer.getHeight();
if (w <= 0 || h <= 0) {
return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
}
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
copyIntoBufferedImage(buffer, img);
return img;
}
/**
* Copies pixels from a PixelBuffer directly into an existing BufferedImage.
*/
public static void copyIntoBufferedImage(PixelBuffer buffer, BufferedImage target) {
if (buffer == null || target == null) return;
int w = Math.min(buffer.getWidth(), target.getWidth());
int h = Math.min(buffer.getHeight(), target.getHeight());
if (w <= 0 || h <= 0) return;
int[] srcPixels = buffer.getPixels();
if (srcPixels == null) return;
if (target.getType() == BufferedImage.TYPE_INT_ARGB || target.getType() == BufferedImage.TYPE_INT_RGB) {
if (target.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] dstPixels = ((DataBufferInt) target.getRaster().getDataBuffer()).getData();
int srcW = buffer.getWidth();
int dstW = target.getWidth();
if (srcW == dstW && srcW == w && srcPixels.length >= w * h && dstPixels.length >= w * h) {
System.arraycopy(srcPixels, 0, dstPixels, 0, w * h);
return;
}
for (int y = 0; y < h; y++) {
System.arraycopy(srcPixels, y * srcW, dstPixels, y * dstW, w);
}
return;
}
}
// Fallback for custom or incompatible image formats
target.setRGB(0, 0, w, h, srcPixels, 0, buffer.getWidth());
}
/**
* Wraps or converts a java.awt BufferedImage into a PixelBuffer.
*/
public static PixelBuffer toPixelBuffer(BufferedImage img) {
if (img == null) return null;
int w = img.getWidth();
int h = img.getHeight();
int[] pixels;
if ((img.getType() == BufferedImage.TYPE_INT_ARGB || img.getType() == BufferedImage.TYPE_INT_RGB)
&& img.getRaster().getDataBuffer() instanceof DataBufferInt) {
pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
} else {
pixels = new int[w * h];
img.getRGB(0, 0, w, h, pixels, 0, w);
}
return new DefaultPixelBuffer(w, h, pixels);
}
}
@@ -24,6 +24,8 @@ public class ConnectDialog extends JDialog {
private JCheckBox tlsCheckBox; private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox; private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox; private JCheckBox tn3270eCheckBox;
private JCheckBox keepAliveCheckBox;
private JCheckBox autoReconnectCheckBox;
private boolean confirmed; private boolean confirmed;
private ConnectionConfig result; private ConnectionConfig result;
@@ -246,6 +248,24 @@ public class ConnectDialog extends JDialog {
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e()); tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
mainPanel.add(tn3270eCheckBox, gbc); mainPanel.add(tn3270eCheckBox, gbc);
// Keep-Alive Checkbox
gbc.gridx = 1;
gbc.gridy = 10;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox);
keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
mainPanel.add(keepAliveCheckBox, gbc);
// Auto-Reconnect Checkbox
gbc.gridx = 1;
gbc.gridy = 11;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox);
autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
mainPanel.add(autoReconnectCheckBox, gbc);
// Buttons // Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
@@ -266,7 +286,7 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(connectBtn); buttonPanel.add(connectBtn);
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 10; gbc.gridy = 12;
gbc.gridwidth = 2; gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc); mainPanel.add(buttonPanel, gbc);
@@ -328,6 +348,18 @@ public class ConnectDialog extends JDialog {
result.setUseTls(tlsCheckBox.isSelected()); result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.isSelected()); result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
result.setTn3270eEnabled(tn3270eCheckBox.isSelected()); result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
result.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
boolean ka = keepAliveCheckBox.isSelected();
result.setKeepAliveEnabled(ka);
result.setKeepAliveIntervalSeconds(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAliveInterval());
haus.nightmare.j3270.config.Settings.setAutoConnectKeepAlive(ka);
boolean ar = autoReconnectCheckBox.isSelected();
result.setAutoReconnect(ar);
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
confirmed = true; confirmed = true;
dispose(); dispose();
} }
@@ -0,0 +1,427 @@
package haus.nightmare.j3270.ui;
import java.io.*;
import java.util.*;
import java.util.logging.Logger;
/**
* Keyboard mapping profile (.kmp) file importer and keybinding utility.
* Parses keyboard mapping profile files, translates key scan codes and mnemonics
* to j3270 action handlers.
*/
public class KeyBindings {
private static final Logger log = Logger.getLogger(KeyBindings.class.getName());
// IBM scan code to standard Java KeyStroke key name mapping
private static final Map<Integer, String> SCAN_CODE_MAP = new HashMap<>();
// Mnemonic to j3270 action mapping
private static final Map<String, String> MNEMONIC_MAP = new HashMap<>();
static {
// Alphanumeric and symbol keys
SCAN_CODE_MAP.put(1, "BACK_QUOTE");
SCAN_CODE_MAP.put(2, "1");
SCAN_CODE_MAP.put(3, "2");
SCAN_CODE_MAP.put(4, "3");
SCAN_CODE_MAP.put(5, "4");
SCAN_CODE_MAP.put(6, "5");
SCAN_CODE_MAP.put(7, "6");
SCAN_CODE_MAP.put(8, "7");
SCAN_CODE_MAP.put(9, "8");
SCAN_CODE_MAP.put(10, "9");
SCAN_CODE_MAP.put(11, "0");
SCAN_CODE_MAP.put(12, "MINUS");
SCAN_CODE_MAP.put(13, "EQUALS");
SCAN_CODE_MAP.put(14, "BACK_SPACE");
SCAN_CODE_MAP.put(15, "TAB");
SCAN_CODE_MAP.put(16, "Q");
SCAN_CODE_MAP.put(17, "W");
SCAN_CODE_MAP.put(18, "E");
SCAN_CODE_MAP.put(19, "R");
SCAN_CODE_MAP.put(20, "T");
SCAN_CODE_MAP.put(21, "Y");
SCAN_CODE_MAP.put(22, "U");
SCAN_CODE_MAP.put(23, "I");
SCAN_CODE_MAP.put(24, "O");
SCAN_CODE_MAP.put(25, "P");
SCAN_CODE_MAP.put(26, "OPEN_BRACKET");
SCAN_CODE_MAP.put(27, "CLOSE_BRACKET");
SCAN_CODE_MAP.put(28, "BACK_SLASH");
SCAN_CODE_MAP.put(29, "CAPS_LOCK");
SCAN_CODE_MAP.put(30, "A");
SCAN_CODE_MAP.put(31, "S");
SCAN_CODE_MAP.put(32, "D");
SCAN_CODE_MAP.put(33, "F");
SCAN_CODE_MAP.put(34, "G");
SCAN_CODE_MAP.put(35, "H");
SCAN_CODE_MAP.put(36, "J");
SCAN_CODE_MAP.put(37, "K");
SCAN_CODE_MAP.put(38, "L");
SCAN_CODE_MAP.put(39, "SEMICOLON");
SCAN_CODE_MAP.put(40, "QUOTE");
SCAN_CODE_MAP.put(41, "BACK_QUOTE");
SCAN_CODE_MAP.put(42, "ENTER");
SCAN_CODE_MAP.put(43, "ENTER");
SCAN_CODE_MAP.put(45, "Z");
SCAN_CODE_MAP.put(46, "X");
SCAN_CODE_MAP.put(47, "C");
SCAN_CODE_MAP.put(48, "V");
SCAN_CODE_MAP.put(49, "B");
SCAN_CODE_MAP.put(50, "N");
SCAN_CODE_MAP.put(51, "M");
SCAN_CODE_MAP.put(52, "COMMA");
SCAN_CODE_MAP.put(53, "PERIOD");
SCAN_CODE_MAP.put(54, "SLASH");
SCAN_CODE_MAP.put(57, "SPACE");
// Editing & Navigation keys
SCAN_CODE_MAP.put(75, "INSERT");
SCAN_CODE_MAP.put(76, "DELETE");
SCAN_CODE_MAP.put(79, "LEFT");
SCAN_CODE_MAP.put(80, "HOME");
SCAN_CODE_MAP.put(81, "END");
SCAN_CODE_MAP.put(83, "UP");
SCAN_CODE_MAP.put(84, "DOWN");
SCAN_CODE_MAP.put(85, "PAGE_UP");
SCAN_CODE_MAP.put(86, "PAGE_DOWN");
SCAN_CODE_MAP.put(89, "RIGHT");
// Numeric Keypad
SCAN_CODE_MAP.put(90, "NUM_LOCK");
SCAN_CODE_MAP.put(91, "NUMPAD7");
SCAN_CODE_MAP.put(92, "NUMPAD4");
SCAN_CODE_MAP.put(93, "NUMPAD1");
SCAN_CODE_MAP.put(95, "DIVIDE");
SCAN_CODE_MAP.put(96, "NUMPAD8");
SCAN_CODE_MAP.put(97, "NUMPAD5");
SCAN_CODE_MAP.put(98, "NUMPAD2");
SCAN_CODE_MAP.put(99, "NUMPAD0");
SCAN_CODE_MAP.put(100, "MULTIPLY");
SCAN_CODE_MAP.put(101, "NUMPAD9");
SCAN_CODE_MAP.put(102, "NUMPAD6");
SCAN_CODE_MAP.put(103, "NUMPAD3");
SCAN_CODE_MAP.put(104, "DECIMAL");
SCAN_CODE_MAP.put(105, "SUBTRACT");
SCAN_CODE_MAP.put(106, "ADD");
SCAN_CODE_MAP.put(108, "ENTER");
// Function keys & Escape
SCAN_CODE_MAP.put(110, "ESCAPE");
SCAN_CODE_MAP.put(112, "F1");
SCAN_CODE_MAP.put(113, "F2");
SCAN_CODE_MAP.put(114, "F3");
SCAN_CODE_MAP.put(115, "F4");
SCAN_CODE_MAP.put(116, "F5");
SCAN_CODE_MAP.put(117, "F6");
SCAN_CODE_MAP.put(118, "F7");
SCAN_CODE_MAP.put(119, "F8");
SCAN_CODE_MAP.put(120, "F9");
SCAN_CODE_MAP.put(121, "F10");
SCAN_CODE_MAP.put(122, "F11");
SCAN_CODE_MAP.put(123, "F12");
// Mnemonics mapping
MNEMONIC_MAP.put("enter", "ENTER");
MNEMONIC_MAP.put("enterreset", "ENTER");
MNEMONIC_MAP.put("newline", "NEWLINE");
MNEMONIC_MAP.put("tab", "TAB");
MNEMONIC_MAP.put("backtab", "shift TAB");
MNEMONIC_MAP.put("reset", "ESCAPE");
MNEMONIC_MAP.put("clear", "CLEAR");
MNEMONIC_MAP.put("eraseeof", "END");
MNEMONIC_MAP.put("eof", "END");
MNEMONIC_MAP.put("erasefld", "ERASE_INPUT");
MNEMONIC_MAP.put("erinp", "ERASE_INPUT");
MNEMONIC_MAP.put("eraseinput", "ERASE_INPUT");
MNEMONIC_MAP.put("dup", "DUP");
MNEMONIC_MAP.put("fieldmark", "FIELD_MARK");
MNEMONIC_MAP.put("fldmark", "FIELD_MARK");
MNEMONIC_MAP.put("fldext", "NEWLINE");
MNEMONIC_MAP.put("field-exit", "NEWLINE");
MNEMONIC_MAP.put("field+", "TAB");
MNEMONIC_MAP.put("field-", "TAB");
MNEMONIC_MAP.put("attn", "ATTN");
MNEMONIC_MAP.put("sysreq", "SYSREQ");
MNEMONIC_MAP.put("cursel", "CURSEL");
MNEMONIC_MAP.put("copy", "COPY");
MNEMONIC_MAP.put("paste", "PASTE");
MNEMONIC_MAP.put("selectall", "SELECTALL");
MNEMONIC_MAP.put("insert", "INSERT");
MNEMONIC_MAP.put("delete", "DELETE");
MNEMONIC_MAP.put("backspace", "BACK_SPACE");
MNEMONIC_MAP.put("home", "HOME");
MNEMONIC_MAP.put("end", "END");
MNEMONIC_MAP.put("up", "UP");
MNEMONIC_MAP.put("down", "DOWN");
MNEMONIC_MAP.put("left", "LEFT");
MNEMONIC_MAP.put("right", "RIGHT");
MNEMONIC_MAP.put("pageup", "PAGE_UP");
MNEMONIC_MAP.put("pagedn", "PAGE_DOWN");
MNEMONIC_MAP.put("pagedown", "PAGE_DOWN");
MNEMONIC_MAP.put("docmode", "DOCMODE");
MNEMONIC_MAP.put("wordwrap", "WORDWRAP");
MNEMONIC_MAP.put("apl", "APL");
MNEMONIC_MAP.put("rule", "CROSSHAIR_RULER");
MNEMONIC_MAP.put("statusbar", "STATUS_BAR");
for (int i = 1; i <= 24; i++) {
MNEMONIC_MAP.put("pf" + i, "PF" + i);
}
for (int i = 1; i <= 3; i++) {
MNEMONIC_MAP.put("pa" + i, "PA" + i);
}
// Shifted PF keys 1-12 = PF13-PF24
for (int i = 1; i <= 12; i++) {
MNEMONIC_MAP.put("spf" + i, "PF" + (i + 12));
}
}
/**
* Parse a .kmp file from a File object.
*
* @param file the .kmp file to parse
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(File file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return parseKmp(reader);
}
}
/**
* Parse a .kmp file from an InputStream.
*
* @param in the InputStream to parse
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(InputStream in) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
return parseKmp(reader);
}
}
/**
* Parse a .kmp file from a Reader.
* Supports both INI-style KEY<n>=action formats and bind statements.
*
* @param reader the reader to parse from
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(Reader reader) throws IOException {
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
// Strip comments
int commentIdx = line.indexOf(';');
if (commentIdx >= 0) {
line = line.substring(0, commentIdx).trim();
}
commentIdx = line.indexOf('#');
if (commentIdx >= 0) {
line = line.substring(0, commentIdx).trim();
}
if (line.isEmpty() || line.startsWith("[")) {
continue; // Skip section headers and blank lines
}
// Syntax 1: bind [mnemonic] KeyCombination OR bind action KeyCombination
if (line.toLowerCase().startsWith("bind ")) {
parseBindStatement(line.substring(5).trim(), actionToBindings);
continue;
}
// Syntax 2: KEY<n>=action OR Modifier-KEY<n>=action OR KeyStroke=[mnemonic]
int eq = line.indexOf('=');
if (eq > 0) {
String left = line.substring(0, eq).trim();
String right = line.substring(eq + 1).trim();
parseKeyAssignment(left, right, actionToBindings);
}
}
// Format into combined comma-separated bindings map
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : actionToBindings.entrySet()) {
String action = entry.getKey();
List<String> bindings = entry.getValue();
if (!bindings.isEmpty()) {
result.put(action, String.join(", ", bindings));
}
}
return result;
}
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
String[] parts = stmt.split("\\s+", 2);
if (parts.length < 2) return;
String target = parts[0].trim();
String keyStrokeStr = parts[1].trim();
String action = resolveMnemonic(target);
if (action == null) {
// Check if parts[1] was the mnemonic instead (e.g. "bind KeyCombination [mnemonic]")
String altAction = resolveMnemonic(keyStrokeStr);
if (altAction != null) {
action = altAction;
keyStrokeStr = target;
}
}
if (action != null) {
String normKeyStroke = normalizeKeyStroke(keyStrokeStr);
if (normKeyStroke != null && !normKeyStroke.isEmpty()) {
addBinding(actionToBindings, action, normKeyStroke);
}
}
}
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
String action = resolveMnemonic(right);
String keyStroke = null;
if (action != null) {
keyStroke = parseScanCodeEntry(left);
} else {
// Case B: left is mnemonic, right is KeyStroke
action = resolveMnemonic(left);
if (action != null) {
keyStroke = normalizeKeyStroke(right);
} else {
// Case C: left is KeyStroke, right is mnemonic
action = resolveMnemonic(right);
if (action != null) {
keyStroke = normalizeKeyStroke(left);
}
}
}
if (action != null && keyStroke != null && !keyStroke.isEmpty()) {
addBinding(actionToBindings, action, keyStroke);
}
}
/**
* Parse scan code entry like "KEY43", "S-KEY112", "C-KEY43", "CS-KEY85", etc.
*/
public static String parseScanCodeEntry(String entry) {
if (entry == null || entry.isEmpty()) return null;
String upper = entry.toUpperCase().trim();
int keyPos = upper.indexOf("KEY");
if (keyPos < 0) {
return normalizeKeyStroke(entry);
}
String prefix = upper.substring(0, keyPos);
if (prefix.endsWith("-")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
String numPart = upper.substring(keyPos + 3);
int scanCode;
try {
scanCode = Integer.parseInt(numPart.trim());
} catch (NumberFormatException e) {
return null;
}
String baseKey = SCAN_CODE_MAP.get(scanCode);
if (baseKey == null) {
return null;
}
StringBuilder sb = new StringBuilder();
if (prefix.contains("C")) sb.append("ctrl ");
if (prefix.contains("A") || prefix.contains("2")) sb.append("alt ");
if (prefix.contains("S")) sb.append("shift ");
if (prefix.contains("M")) sb.append("meta ");
sb.append(baseKey);
return sb.toString().trim();
}
/**
* Normalize key stroke expression (e.g. "Ctrl+Shift+C" -> "ctrl shift C").
*/
public static String normalizeKeyStroke(String stroke) {
if (stroke == null || stroke.trim().isEmpty()) return null;
String s = stroke.trim();
// Check if it is an IBM scan code
if (s.toUpperCase().contains("KEY")) {
String fromScan = parseScanCodeEntry(s);
if (fromScan != null) return fromScan;
}
// Replace '+' with space
s = s.replace("+", " ");
String[] parts = s.split("\\s+");
StringBuilder mods = new StringBuilder();
String mainKey = null;
for (String p : parts) {
String lp = p.toLowerCase();
if ("ctrl".equals(lp) || "control".equals(lp)) {
mods.append("ctrl ");
} else if ("shift".equals(lp)) {
mods.append("shift ");
} else if ("alt".equals(lp)) {
mods.append("alt ");
} else if ("meta".equals(lp) || "cmd".equals(lp) || "command".equals(lp)) {
mods.append("meta ");
} else {
mainKey = p.toUpperCase();
}
}
if (mainKey == null) return null;
return (mods.toString() + mainKey).trim();
}
/**
* Resolve a mnemonic token (e.g. "[enter]", "enter", "[pf12]", "eraseeof")
* into a j3270 action name.
*/
public static String resolveMnemonic(String token) {
if (token == null) return null;
String clean = token.trim();
if (clean.startsWith("[") && clean.endsWith("]")) {
clean = clean.substring(1, clean.length() - 1);
}
clean = clean.toLowerCase();
String direct = MNEMONIC_MAP.get(clean);
if (direct != null) {
return direct;
}
// Case-insensitive match on existing action names
String upper = clean.toUpperCase();
if (MNEMONIC_MAP.containsValue(upper)) {
return upper;
}
return null;
}
private static void addBinding(Map<String, List<String>> map, String action, String keyStroke) {
List<String> list = map.computeIfAbsent(action, k -> new ArrayList<>());
if (!list.contains(keyStroke)) {
list.add(keyStroke);
}
}
}
@@ -7,6 +7,7 @@ import haus.nightmare.j3270.config.Settings;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
@@ -33,6 +34,8 @@ public class SettingsDialog extends JDialog {
private JCheckBox blockSelectCheck; private JCheckBox blockSelectCheck;
private JSpinner dynamicRowsSpinner; private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner; private JSpinner dynamicColsSpinner;
private JCheckBox enablePasteFromExcelCheck;
private JCheckBox pasteStopAtProtectedCheck;
// Entry Assist & Modes tab // Entry Assist & Modes tab
private JCheckBox docModeCheck; private JCheckBox docModeCheck;
@@ -291,10 +294,18 @@ public class SettingsDialog extends JDialog {
gbc.gridx = 1; gbc.gridx = 1;
panel.add(dynDimPanel, gbc); panel.add(dynDimPanel, gbc);
// Placeholder for potentially more behavior options below // Clipboard & Tabular Paste options
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 4; gbc.gridy = 4;
gbc.gridwidth = 2; gbc.gridwidth = 2;
enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel());
panel.add(enablePasteFromExcelCheck, gbc);
gbc.gridy = 5;
pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine());
panel.add(pasteStopAtProtectedCheck, gbc);
gbc.gridy = 6;
gbc.weighty = 1.0; gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc); panel.add(Box.createGlue(), gbc);
@@ -653,6 +664,45 @@ public class SettingsDialog extends JDialog {
}); });
btnPanel.add(btnReset); btnPanel.add(btnReset);
// Import Keymap import from .kmp file
JButton btnImportKmp = new JButton("Import Keymap...");
ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT);
btnImportKmp.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Import Keymap Profile");
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("Keyboard Map Profile (*.kmp)", "kmp"));
int ret = chooser.showOpenDialog(this);
if (ret == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
try {
Map<String, String> imported = KeyBindings.parseKmp(f);
int updatedCount = 0;
for (Map.Entry<String, String> entry : imported.entrySet()) {
String action = entry.getKey();
String binding = entry.getValue();
tempKeyBindings.put(action, binding);
for (int row = 0; row < keymapModel.getRowCount(); row++) {
if (action.equalsIgnoreCase((String) keymapModel.getValueAt(row, 0))) {
keymapModel.setValueAt(binding, row, 1);
updatedCount++;
break;
}
}
}
JOptionPane.showMessageDialog(this,
"Successfully imported " + updatedCount + " key bindings from " + f.getName(),
"Keymap Imported",
JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"Failed to import keymap: " + ex.getMessage(),
"Import Error",
JOptionPane.ERROR_MESSAGE);
}
}
});
btnPanel.add(btnImportKmp);
main.add(new JScrollPane(table), BorderLayout.CENTER); main.add(new JScrollPane(table), BorderLayout.CENTER);
main.add(btnPanel, BorderLayout.SOUTH); main.add(btnPanel, BorderLayout.SOUTH);
@@ -705,6 +755,14 @@ public class SettingsDialog extends JDialog {
// Block select mode // Block select mode
Settings.setBlockSelectMode(blockSelectCheck.isSelected()); Settings.setBlockSelectMode(blockSelectCheck.isSelected());
// Clipboard & Tabular Paste options
if (enablePasteFromExcelCheck != null) {
Settings.setEnablePasteFromExcel(enablePasteFromExcelCheck.isSelected());
}
if (pasteStopAtProtectedCheck != null) {
Settings.setPasteStopAtProtectedLine(pasteStopAtProtectedCheck.isSelected());
}
// Default Dynamic screen dimensions // Default Dynamic screen dimensions
if (dynamicRowsSpinner != null && dynamicColsSpinner != null) { if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue()); Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
@@ -137,12 +137,20 @@ public class StatusBar extends JPanel {
} }
public void applyTheme(UITheme theme) { public void applyTheme(UITheme theme) {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> applyTheme(theme));
return;
}
setBackground(ThemeManager.getStatusBarBg(theme)); setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme))); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
updateStatus(); updateStatus();
} }
public void updateStatus() { public void updateStatus() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::updateStatus);
return;
}
UITheme theme = ThemeManager.getTheme(); UITheme theme = ThemeManager.getTheme();
if (client == null) { if (client == null) {
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
@@ -170,6 +178,10 @@ public class StatusBar extends JPanel {
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme)); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break; break;
case RECONNECTING:
connectionStatus.setText("Reconnecting...");
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
break;
case TCP_PENDING: case TCP_PENDING:
case TELNET_PENDING: case TELNET_PENDING:
connectionStatus.setText("Connecting..."); connectionStatus.setText("Connecting...");
@@ -494,8 +494,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int cols = sb != null ? sb.getDisplayCols() : 80; int cols = sb != null ? sb.getDisplayCols() : 80;
int curRow = cols > 0 ? curPos / cols : 0; int curRow = cols > 0 ? curPos / cols : 0;
int curCol = cols > 0 ? curPos % cols : 0; int curCol = cols > 0 ? curPos % cols : 0;
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
if (client.getPS() != null && (text.contains("\n") || text.contains("\r"))) { if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected);
} else if (client.getPS() != null) {
client.getPS().pasteString(text, curRow, curCol); client.getPS().pasteString(text, curRow, curCol);
} else { } else {
for (char ch : text.toCharArray()) { for (char ch : text.toCharArray()) {
@@ -603,6 +607,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
public void guardedPack() { public void guardedPack() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::guardedPack);
return;
}
resizeGuard = true; resizeGuard = true;
revalidate(); revalidate();
Container top = getTopLevelAncestor(); Container top = getTopLevelAncestor();
@@ -1163,10 +1171,19 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (ip != null) { if (ip != null) {
ip.setBellEnabled(haus.nightmare.j3270.config.Settings.getEntryAssistBell()); ip.setBellEnabled(haus.nightmare.j3270.config.Settings.getEntryAssistBell());
ip.setBellColumn(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistBellCol() - 1)); ip.setBellColumn(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistBellCol() - 1));
if (ip.getBellListener() == null) {
ip.setBellListener(() -> {
SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().beep());
});
}
ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
} }
if (client.getPS() != null) {
client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel());
client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine());
}
} }
if (statusBar != null) { if (statusBar != null) {
statusBar.updateStatus(); statusBar.updateStatus();
@@ -1432,8 +1449,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (rgb != null && gWidth > 0 && gHeight > 0) { if (rgb != null && gWidth > 0 && gHeight > 0) {
long currentUpdateCount = client.getGraphicsPlane().getUpdateCount(); long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) { if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) {
cachedGraphicsImage = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB); cachedGraphicsImage = AwtPixelBufferBridge.toBufferedImage(client.getGraphicsPlane());
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
lastGraphicsUpdateCount = currentUpdateCount; lastGraphicsUpdateCount = currentUpdateCount;
} }
if (gWidth == gridW && gHeight == gridH) { if (gWidth == gridW && gHeight == gridH) {
@@ -1548,7 +1564,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) { if (slot != null) {
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0; int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg); haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg);
java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb);
if (img != null) { if (img != null) {
g2.drawImage(img, x, y, null); g2.drawImage(img, x, y, null);
drawnAsPs = true; drawnAsPs = true;
@@ -1691,7 +1708,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
public void setHodWallpaper(haus.nightmare.lib3270j.graphics.HODWallpaper wallpaper) { public void setHodWallpaper(haus.nightmare.lib3270j.graphics.HODWallpaper wallpaper) {
this.hodWallpaper = wallpaper; this.hodWallpaper = wallpaper;
if (wallpaper != null) { if (wallpaper != null) {
this.wallpaperImage = wallpaper.getHODImage(); Object hodImg = wallpaper.getHODImage();
if (hodImg instanceof Image) {
this.wallpaperImage = (Image) hodImg;
} else if (hodImg instanceof haus.nightmare.lib3270j.graphics.PixelBuffer) {
this.wallpaperImage = AwtPixelBufferBridge.toBufferedImage((haus.nightmare.lib3270j.graphics.PixelBuffer) hodImg);
}
} }
repaint(); repaint();
} }
@@ -0,0 +1,105 @@
package haus.nightmare.j3270.ui;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 13.3: Keyboard Remap (.kmp) File Importer.
* Tests parsing of scan codes, modifier combinations, bind statements,
* mnemonic actions, and comments.
*/
public class KmpKeymapImportTest {
@Test
public void testParseScanCodeAssignments() throws Exception {
String kmpContent =
"# Sample keyboard profile mapping\n" +
"; IBM scan code mappings\n" +
"KEY43=[enter]\n" +
"S-KEY112=[pf13]\n" +
"C-KEY43=[newline]\n" +
"A-KEY110=[reset]\n" +
"KEY15=[tab]\n" +
"S-KEY15=[backtab]\n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertNotNull(imported);
// KEY43 is ENTER -> ENTER
assertEquals("ENTER", imported.get("ENTER"));
// S-KEY112 is Shift+F1 -> PF13
assertEquals("shift F1", imported.get("PF13"));
// C-KEY43 is Ctrl+ENTER -> NEWLINE
assertEquals("ctrl ENTER", imported.get("NEWLINE"));
// A-KEY110 is Alt+ESCAPE -> ESCAPE
assertEquals("alt ESCAPE", imported.get("ESCAPE"));
// KEY15 is TAB -> TAB
assertEquals("TAB", imported.get("TAB"));
// S-KEY15 is Shift+TAB -> shift TAB
assertEquals("shift TAB", imported.get("shift TAB"));
}
@Test
public void testParseBindStatements() throws Exception {
String kmpContent =
"bind [enter] Ctrl+Enter\n" +
"bind [pf1] F1\n" +
"bind [pf24] Shift+F12\n" +
"bind [eraseeof] End\n" +
"bind [clear] Pause\n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertNotNull(imported);
assertEquals("ctrl ENTER", imported.get("ENTER"));
assertEquals("F1", imported.get("PF1"));
assertEquals("shift F12", imported.get("PF24"));
assertEquals("END", imported.get("END"));
assertEquals("PAUSE", imported.get("CLEAR"));
}
@Test
public void testScanCodeWithMultipleModifiers() {
// C-S-KEY85 -> Ctrl+Shift+PAGE_UP
String res = KeyBindings.parseScanCodeEntry("CS-KEY85");
assertEquals("ctrl shift PAGE_UP", res);
// 2-KEY80 -> Alt+HOME (2- prefix is Alt in scan code syntax)
String altRes = KeyBindings.parseScanCodeEntry("2-KEY80");
assertEquals("alt HOME", altRes);
}
@Test
public void testMnemonicResolution() {
assertEquals("ENTER", KeyBindings.resolveMnemonic("[enter]"));
assertEquals("ENTER", KeyBindings.resolveMnemonic("enterreset"));
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[newline]"));
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[field-exit]"));
assertEquals("PF1", KeyBindings.resolveMnemonic("[pf1]"));
assertEquals("PF12", KeyBindings.resolveMnemonic("[pf12]"));
assertEquals("PF13", KeyBindings.resolveMnemonic("[spf1]"));
assertEquals("ESCAPE", KeyBindings.resolveMnemonic("[reset]"));
assertEquals("ERASE_INPUT", KeyBindings.resolveMnemonic("[erasefld]"));
assertEquals("DOCMODE", KeyBindings.resolveMnemonic("[docmode]"));
assertEquals("WORDWRAP", KeyBindings.resolveMnemonic("[wordwrap]"));
}
@Test
public void testCommentsAndBlankLinesIgnored() throws Exception {
String kmpContent =
"[ProfileHeader]\n" +
" \n" +
"# Full line comment\n" +
"; Another comment\n" +
"KEY42=[enter] ; inline comment\n" +
" \n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertEquals(1, imported.size());
assertEquals("ENTER", imported.get("ENTER"));
}
}
@@ -58,6 +58,17 @@ public class ConnectionConfig {
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>(); private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>(); private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
// IBM HoD autoSysUnlock parity
private boolean autoSysUnlock = true;
// Phase 11: Enterprise Connection Resilience & Heartbeat
private boolean keepAliveEnabled = true;
private int keepAliveIntervalSeconds = 120;
private String keepAliveType = "NOP";
private boolean autoReconnect = false;
private int reconnectMaxRetries = 5;
private int tcpUserTimeoutMs = 0;
public ConnectionConfig() {} public ConnectionConfig() {}
public ConnectionConfig(String host, int port) { public ConnectionConfig(String host, int port) {
@@ -280,6 +291,27 @@ public class ConnectionConfig {
} }
} }
public boolean isAutoSysUnlock() { return autoSysUnlock; }
public void setAutoSysUnlock(boolean autoSysUnlock) { this.autoSysUnlock = autoSysUnlock; }
public boolean isKeepAliveEnabled() { return keepAliveEnabled; }
public void setKeepAliveEnabled(boolean enabled) { this.keepAliveEnabled = enabled; }
public int getKeepAliveIntervalSeconds() { return keepAliveIntervalSeconds; }
public void setKeepAliveIntervalSeconds(int seconds) { this.keepAliveIntervalSeconds = seconds; }
public String getKeepAliveType() { return keepAliveType; }
public void setKeepAliveType(String type) { this.keepAliveType = (type != null) ? type.trim().toUpperCase() : "NOP"; }
public boolean isAutoReconnect() { return autoReconnect; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
public int getReconnectMaxRetries() { return reconnectMaxRetries; }
public void setReconnectMaxRetries(int retries) { this.reconnectMaxRetries = Math.max(0, retries); }
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
/** /**
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"), * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"), * plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
@@ -303,10 +335,21 @@ public class ConnectionConfig {
String pUser = null; String pUser = null;
String pPass = null; String pPass = null;
boolean keepAlive = true;
boolean autoReconnect = false;
String[] tokens = s.split("\\s+"); String[] tokens = s.split("\\s+");
StringBuilder remaining = new StringBuilder(); StringBuilder remaining = new StringBuilder();
for (String tok : tokens) { for (String tok : tokens) {
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) { if (tok.equalsIgnoreCase("--keepalive") || tok.equalsIgnoreCase("-keepalive")) {
keepAlive = true;
} else if (tok.equalsIgnoreCase("--no-keepalive") || tok.equalsIgnoreCase("-no-keepalive")) {
keepAlive = false;
} else if (tok.equalsIgnoreCase("--autoreconnect") || tok.equalsIgnoreCase("-autoreconnect")) {
autoReconnect = true;
} else if (tok.equalsIgnoreCase("--no-autoreconnect") || tok.equalsIgnoreCase("-no-autoreconnect")) {
autoReconnect = false;
} else if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim(); String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
try { try {
java.net.URI uri = new java.net.URI(proxyUrl); java.net.URI uri = new java.net.URI(proxyUrl);
@@ -417,6 +460,8 @@ public class ConnectionConfig {
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
config.setUseTls(tls); config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e); config.setTn3270eEnabled(tn3270e);
config.setKeepAliveEnabled(keepAlive);
config.setAutoReconnect(autoReconnect);
if (dynamic) { if (dynamic) {
config.setDynamicDimensions(dynRows, dynCols); config.setDynamicDimensions(dynRows, dynCols);
} }
@@ -44,6 +44,9 @@ public class Telnet3270Client {
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer; private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
private TelnetConnection connection; private TelnetConnection connection;
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
private volatile Thread reconnectThread;
public Telnet3270Client(ConnectionConfig config) { public Telnet3270Client(ConnectionConfig config) {
this.config = config; this.config = config;
this.translator = new EbcdicTranslator(config.getCodePage()); this.translator = new EbcdicTranslator(config.getCodePage());
@@ -58,6 +61,7 @@ public class Telnet3270Client {
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.setAutoSysUnlock(config.isAutoSysUnlock());
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode()); 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);
@@ -87,6 +91,33 @@ public class Telnet3270Client {
@Override public void onSoundAlarm() { @Override public void onSoundAlarm() {
ps.notifyAlarm(); ps.notifyAlarm();
} }
@Override public void onKeyboardUnlocked() {
ps.notifyKeyUnlocked();
}
});
// Phase 11: Auto-reconnection and OIA status coordinator
fsm.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
if (newState == ConnectionState.RECONNECTING) {
if (oia != null) {
oia.writeToOIA("X RECONNECT");
}
initiateAutoReconnect();
} else if (newState == ConnectionState.NOT_CONNECTED) {
if (oia != null) {
oia.setInputInhibited(haus.nightmare.lib3270j.ecl.ECLOIA.INHIBIT_COMMCHECK);
}
} else if (newState.isFullSession()) {
if (oia != null) {
oia.setInputInhibited(-1);
}
}
}
@Override
public void onConnectionError(String message) {}
}); });
} }
@@ -159,11 +190,146 @@ public class Telnet3270Client {
* Disconnect from the host. * Disconnect from the host.
*/ */
public void disconnect() { public void disconnect() {
cancelAutoReconnect();
if (connection != null) { if (connection != null) {
connection.disconnect(); connection.disconnect();
connection = null; connection = null;
} }
fsm.onDisconnect(); fsm.onDisconnect(false);
}
public void cancelAutoReconnect() {
reconnecting.set(false);
if (reconnectThread != null) {
reconnectThread.interrupt();
reconnectThread = null;
}
}
public void initiateAutoReconnect() {
if (config == null || !config.isAutoReconnect()) {
return;
}
if (!reconnecting.compareAndSet(false, true)) {
return;
}
reconnectThread = new Thread(this::runAutoReconnect, "TN3270-AutoReconnect");
reconnectThread.setDaemon(true);
reconnectThread.start();
}
private void runAutoReconnect() {
int maxRetries = (config != null) ? config.getReconnectMaxRetries() : 5;
log.info("Starting automatic reconnection loop (maxRetries=" + maxRetries + ")");
try {
for (int attempt = 1; attempt <= maxRetries && reconnecting.get(); attempt++) {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s
long delaySeconds = Math.min(30, (long) Math.pow(2, attempt - 1));
log.info("Auto-reconnect attempt " + attempt + "/" + maxRetries + " scheduled in " + delaySeconds + "s");
for (int s = 0; s < delaySeconds * 10; s++) {
if (!reconnecting.get() || Thread.currentThread().isInterrupted()) {
log.info("Auto-reconnect cancelled during backoff delay");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.info("Auto-reconnect thread interrupted");
return;
}
}
if (!reconnecting.get()) {
return;
}
try {
log.info("Executing auto-reconnect attempt " + attempt + "/" + maxRetries + " to " + config.getHost() + ":" + config.getPort());
if (connection != null) {
connection.disconnect();
connection = null;
}
fsm.resetSessionState();
screenBuffer.erase(false);
connection = new TelnetConnection(config, fsm);
fsm.setConnection(connection);
connection.connect();
fsm.onConnected();
log.info("Auto-reconnect successful on attempt " + attempt);
reconnecting.set(false);
fsm.notifyScreenUpdate();
return;
} catch (IOException e) {
log.warning("Auto-reconnect attempt " + attempt + " failed: " + e.getMessage());
if (attempt < maxRetries && reconnecting.get()) {
fsm.setConnectionState(ConnectionState.RECONNECTING);
}
}
}
// All retries failed
log.warning("All " + maxRetries + " automatic reconnection attempts failed");
reconnecting.set(false);
fsm.onDisconnect(false);
fsm.onError("Automatic reconnection failed after " + maxRetries + " attempts");
} finally {
reconnecting.set(false);
reconnectThread = null;
}
}
public boolean isAutoReconnect() {
return (config != null) && config.isAutoReconnect();
}
public void setAutoReconnect(boolean autoReconnect) {
if (config != null) {
config.setAutoReconnect(autoReconnect);
}
}
public int getReconnectMaxRetries() {
return (config != null) ? config.getReconnectMaxRetries() : 5;
}
public void setReconnectMaxRetries(int retries) {
if (config != null) {
config.setReconnectMaxRetries(retries);
}
}
public boolean isReconnecting() {
return reconnecting.get() || fsm.getConnectionState() == ConnectionState.RECONNECTING;
}
public boolean isKeepAliveEnabled() {
return (config != null) && config.isKeepAliveEnabled();
}
public void setKeepAliveEnabled(boolean enabled) {
if (config != null) {
config.setKeepAliveEnabled(enabled);
}
if (connection != null) {
if (enabled) connection.startKeepAlive();
else connection.stopKeepAlive();
}
}
public int getKeepAliveIntervalSeconds() {
return (config != null) ? config.getKeepAliveIntervalSeconds() : 120;
}
public void setKeepAliveIntervalSeconds(int seconds) {
if (config != null) {
config.setKeepAliveIntervalSeconds(seconds);
}
if (connection != null && config != null && config.isKeepAliveEnabled()) {
connection.startKeepAlive();
}
} }
/** /**
@@ -294,6 +460,23 @@ public class Telnet3270Client {
return fsm; return fsm;
} }
public boolean isAutoSysUnlock() {
return (config != null) ? config.isAutoSysUnlock() : true;
}
public void setAutoSysUnlock(boolean autoSysUnlock) {
if (config != null) {
config.setAutoSysUnlock(autoSysUnlock);
}
if (dsProcessor != null) {
dsProcessor.setAutoSysUnlock(autoSysUnlock);
}
}
public boolean isContentionResolution() {
return fsm != null && fsm.isContentionResolutionNegotiated();
}
/** Send an NVT ASCII character in NVT mode. */ /** Send an NVT ASCII character in NVT mode. */
public void sendNVTChar(char c) throws IOException { public void sendNVTChar(char c) throws IOException {
fsm.sendNVTChar(c); fsm.sendNVTChar(c);
@@ -47,6 +47,13 @@ public class DataStreamProcessor {
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream(); private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
private int currentGocaSubtype = 0; private int currentGocaSubtype = 0;
// Phase 10: Auto-Unlock & Contention Resolution State
private boolean autoSysUnlock = true;
private boolean contentionResolution = false;
private boolean unlockPending = false;
private boolean unlockSysPending = false;
private boolean rcvdRead = false;
/** 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 {
@@ -131,6 +138,21 @@ public class DataStreamProcessor {
screenListeners.remove(l); screenListeners.remove(l);
} }
public boolean isAutoSysUnlock() { return autoSysUnlock; }
public void setAutoSysUnlock(boolean autoSysUnlock) { this.autoSysUnlock = autoSysUnlock; }
public boolean isContentionResolution() { return contentionResolution; }
public void setContentionResolution(boolean cr) { this.contentionResolution = cr; }
public boolean isUnlockPending() { return unlockPending; }
public void setUnlockPending(boolean pending) { this.unlockPending = pending; }
public boolean isUnlockSysPending() { return unlockSysPending; }
public void setUnlockSysPending(boolean pending) { this.unlockSysPending = pending; }
public boolean isRcvdRead() { return rcvdRead; }
public void setRcvdRead(boolean rcvdRead) { this.rcvdRead = rcvdRead; }
/** /**
* Process a 3270 data stream record. * Process a 3270 data stream record.
* *
@@ -197,16 +219,19 @@ public class DataStreamProcessor {
break; break;
case CMD_RB: case CMD_RB:
case SNA_CMD_RB: case SNA_CMD_RB:
rcvdRead = true;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
processReadBuffer(); processReadBuffer();
break; break;
case CMD_RM: case CMD_RM:
case SNA_CMD_RM: case SNA_CMD_RM:
rcvdRead = true;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
processReadModified(false); processReadModified(false);
break; break;
case CMD_RMA: case CMD_RMA:
case SNA_CMD_RMA: case SNA_CMD_RMA:
rcvdRead = true;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
processReadModified(true); processReadModified(true);
break; break;
@@ -235,7 +260,10 @@ public class DataStreamProcessor {
screen.updateDisplaySnapshot(); screen.updateDisplaySnapshot();
} }
if (keyboardRestore && inputProcessor != null) { boolean isWriteCmd = (cmd == CMD_W || cmd == SNA_CMD_W ||
cmd == CMD_EW || cmd == SNA_CMD_EW ||
cmd == CMD_EWA || cmd == SNA_CMD_EWA);
if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) {
inputProcessor.setKeyboardLocked(false); inputProcessor.setKeyboardLocked(false);
} }
@@ -333,10 +361,13 @@ public class DataStreamProcessor {
log.fine("WCC: " + String.format("0x%02x", wcc) + log.fine("WCC: " + String.format("0x%02x", wcc) +
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt); " reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
if (kbdRestore || inputProcessor != null) { if (kbdRestore) {
if (inputProcessor != null) { unlockPending = true;
inputProcessor.setKeyboardLocked(false); unlockSysPending = true;
} }
if (!contentionResolution && kbdRestore && inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
} }
if (resetMdt) { if (resetMdt) {
@@ -1101,6 +1132,9 @@ public class DataStreamProcessor {
} }
private void notifyScreenUpdated() { private void notifyScreenUpdated() {
if (screen != null) {
screen.notifyScreenUpdate();
}
for (ScreenUpdateListener l : screenListeners) { for (ScreenUpdateListener l : screenListeners) {
l.onScreenUpdated(); l.onScreenUpdated();
} }
@@ -1549,7 +1583,11 @@ public class DataStreamProcessor {
log.fine("processWCC: " + String.format("0x%02x", wcc) + log.fine("processWCC: " + String.format("0x%02x", wcc) +
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt); " reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
if (kbdRestore && inputProcessor != null) { if (kbdRestore) {
unlockPending = true;
unlockSysPending = true;
}
if (!contentionResolution && kbdRestore && inputProcessor != null) {
inputProcessor.setKeyboardLocked(false); inputProcessor.setKeyboardLocked(false);
} }
if (resetMdt) { if (resetMdt) {
@@ -1,7 +1,6 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event; package haus.nightmare.lib3270j.eNetwork.ECL.event;
import java.awt.Image; import haus.nightmare.lib3270j.graphics.Rectangle;
import java.awt.Rectangle;
/** /**
* Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsEvent. * Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsEvent.
@@ -12,11 +11,11 @@ public class ECLPSGraphicsEvent extends haus.nightmare.lib3270j.ecl.ECLPSGraphic
super(source, id); super(source, id);
} }
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image) { public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Object image) {
super(source, id, image); super(source, id, image);
} }
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image, Rectangle rectangle) { public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Object image, Rectangle rectangle) {
super(source, id, image, rectangle); super(source, id, image, rectangle);
} }
} }
@@ -1,6 +1,6 @@
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
import java.awt.Color; import haus.nightmare.lib3270j.graphics.Color;
/** /**
* Drop-in IBM Host On-Demand compatible facade for FillArea. * Drop-in IBM Host On-Demand compatible facade for FillArea.
@@ -1,12 +1,10 @@
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
import java.awt.Component;
/** /**
* Drop-in IBM Host On-Demand compatible facade for HODBitImage. * Drop-in IBM Host On-Demand compatible facade for HODBitImage.
*/ */
public class HODBitImage extends haus.nightmare.lib3270j.graphics.HODBitImage { public class HODBitImage extends haus.nightmare.lib3270j.graphics.HODBitImage {
public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) { public HODBitImage(Object comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
super(comp, width, height, data, baseColor, depth, useGraphicColors); super(comp, width, height, data, baseColor, depth, useGraphicColors);
} }
} }
@@ -1,8 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
import java.awt.Component; import haus.nightmare.lib3270j.graphics.Dimension;
import java.awt.Dimension; import haus.nightmare.lib3270j.graphics.Rectangle;
import java.awt.Rectangle;
/** /**
* Drop-in IBM Host On-Demand compatible facade for HODPart. * Drop-in IBM Host On-Demand compatible facade for HODPart.
@@ -12,15 +11,15 @@ public class HODPart extends haus.nightmare.lib3270j.graphics.HODPart {
super(); super();
} }
public HODPart(Component component) { public HODPart(Object component) {
super(component); super(component);
} }
public HODPart(Component component, Dimension dimension) { public HODPart(Object component, Dimension dimension) {
super(component, dimension); super(component, dimension);
} }
public HODPart(Component component, Rectangle rectangle) { public HODPart(Object component, Rectangle rectangle) {
super(component, rectangle); super(component, rectangle);
} }
@@ -1,7 +1,5 @@
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
import java.awt.Image;
/** /**
* Drop-in IBM Host On-Demand compatible facade for HODWallpaper. * Drop-in IBM Host On-Demand compatible facade for HODWallpaper.
*/ */
@@ -14,7 +12,7 @@ public class HODWallpaper extends haus.nightmare.lib3270j.graphics.HODWallpaper
super(displayMode); super(displayMode);
} }
public HODWallpaper(Image image, int displayMode) { public HODWallpaper(Object image, int displayMode) {
super(image, displayMode); super(image, displayMode);
} }
} }
@@ -1,17 +1,17 @@
package haus.nightmare.lib3270j.ecl; package haus.nightmare.lib3270j.ecl;
import java.awt.Color; import haus.nightmare.lib3270j.graphics.Color;
import java.awt.Component;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* Standard implementation of ECLPSGraphicsServices conforming to IBM Host On-Demand ECL. * Standard implementation of ECLPSGraphicsServices conforming to IBM Host On-Demand ECL.
* Completely decoupled from java.awt.
*/ */
public class DefaultPSGraphicsServices implements ECLPSGraphicsServices { public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
private final ECLPS ps; private final ECLPS ps;
private Component visualComponent; private Object visualComponent;
private Color[] colors; private Color[] colors;
private final List<ECLPSGraphicsListener> listeners = new CopyOnWriteArrayList<>(); private final List<ECLPSGraphicsListener> listeners = new CopyOnWriteArrayList<>();
@@ -20,11 +20,11 @@ public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
} }
@Override @Override
public void setVisualComponent(Component comp) { public void setVisualComponent(Object comp) {
this.visualComponent = comp; this.visualComponent = comp;
} }
public Component getVisualComponent() { public Object getVisualComponent() {
return visualComponent; return visualComponent;
} }
@@ -27,6 +27,12 @@ public class ECLConnection {
private String luName; private String luName;
private String workstationId = ""; private String workstationId = "";
private boolean ssl = false; private boolean ssl = false;
private boolean autoSysUnlock = true;
private boolean keepAlive = true;
private int keepAliveTimeout = 120;
private String keepAliveType = "NOP";
private boolean autoReconnect = false;
private int maxRetry = 5;
private boolean contentionResolution = false; private boolean contentionResolution = false;
private boolean luluSession = false; private boolean luluSession = false;
private boolean isNegCR = false; private boolean isNegCR = false;
@@ -71,12 +77,49 @@ public class ECLConnection {
if (props != null) { if (props != null) {
this.properties.putAll(props); this.properties.putAll(props);
convertData(this.properties); convertData(this.properties);
String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
if (asu != null) {
this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu);
}
String ka = this.properties.getProperty(ECLSession.SESSION_KEEPALIVE);
if (ka != null) {
this.keepAlive = "true".equalsIgnoreCase(ka) || "1".equals(ka);
}
String kat = this.properties.getProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT);
if (kat != null) {
try { this.keepAliveTimeout = Integer.parseInt(kat.trim()); } catch (NumberFormatException ignored) {}
}
String katyp = this.properties.getProperty(ECLSession.KEY_KEEPALIVE_TYPE);
if (katyp != null) {
this.keepAliveType = katyp;
}
String ar = this.properties.getProperty(ECLSession.SESSION_AUTORECONNECT);
if (ar != null) {
this.autoReconnect = "true".equalsIgnoreCase(ar) || "1".equals(ar);
}
String mr = this.properties.getProperty(ECLSession.SESSION_RECONNECT_RETRIES);
if (mr != null) {
try { this.maxRetry = Integer.parseInt(mr.trim()); } catch (NumberFormatException ignored) {}
}
} }
} }
public ECLConnection(ECLSession session, Telnet3270Client client) { public ECLConnection(ECLSession session, Telnet3270Client client) {
this.session = session; this.session = session;
this.client = client; this.client = client;
if (client != null && client.getConfig() != null) {
this.autoSysUnlock = client.getConfig().isAutoSysUnlock();
this.keepAlive = client.getConfig().isKeepAliveEnabled();
this.keepAliveTimeout = client.getConfig().getKeepAliveIntervalSeconds();
this.keepAliveType = client.getConfig().getKeepAliveType();
this.autoReconnect = client.getConfig().isAutoReconnect();
this.maxRetry = client.getConfig().getReconnectMaxRetries();
} else if (session != null) {
this.autoSysUnlock = session.isAutoSysUnlock();
this.keepAlive = session.isKeepAlive();
this.keepAliveTimeout = session.getKeepAliveTimeout();
this.autoReconnect = session.isAutoReconnect();
}
if (client != null) { if (client != null) {
client.addConnectionListener(new ConnectionListener() { client.addConnectionListener(new ConnectionListener() {
@@ -114,6 +157,13 @@ public class ECLConnection {
state, state, "TN3270E Negotiated", deviceType, deviceName); state, state, "TN3270E Negotiated", deviceType, deviceName);
notifyCommEvent(event); notifyCommEvent(event);
} }
@Override
public void onTN3270EFunctionsNegotiated(boolean[] functions) {
if (functions != null && functions.length > haus.nightmare.lib3270j.protocol.TN3270EConstants.FUNC_CONTENTION_RESOLUTION) {
setContentionResolution(functions[haus.nightmare.lib3270j.protocol.TN3270EConstants.FUNC_CONTENTION_RESOLUTION]);
}
}
}); });
} }
} }
@@ -398,17 +448,138 @@ public class ECLConnection {
} }
public void setSSL(boolean ssl) { SetSSL(ssl); } public void setSSL(boolean ssl) { SetSSL(ssl); }
public void setContentionResolution(boolean bl) { this.contentionResolution = bl; } public void setContentionResolution(boolean bl) {
this.contentionResolution = bl;
if (client != null && client.getTelnetFSM() != null) {
client.getTelnetFSM().setContentionResolutionNegotiated(bl);
}
}
public void SetContentionResolution(boolean bl) { setContentionResolution(bl); } public void SetContentionResolution(boolean bl) { setContentionResolution(bl); }
public boolean getContentionResolution() { return contentionResolution; } public boolean getContentionResolution() {
public boolean isContentionResolution() { return contentionResolution; } if (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().isTn3270eNegotiated()) {
return client.getTelnetFSM().isContentionResolutionNegotiated();
}
return contentionResolution;
}
public boolean isContentionResolution() { return getContentionResolution(); }
public boolean GetContentionResolution() { return getContentionResolution(); }
public boolean IsContentionResolution() { return getContentionResolution(); }
public boolean isAutoSysUnlock() {
if (client != null && client.getConfig() != null) {
return client.getConfig().isAutoSysUnlock();
}
String s = properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
if (s != null) {
return "true".equalsIgnoreCase(s) || "1".equals(s);
}
return autoSysUnlock;
}
public boolean IsAutoSysUnlock() { return isAutoSysUnlock(); }
public boolean getAutoSysUnlock() { return isAutoSysUnlock(); }
public boolean GetAutoSysUnlock() { return isAutoSysUnlock(); }
public void setAutoSysUnlock(boolean unlock) {
this.autoSysUnlock = unlock;
properties.setProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK, String.valueOf(unlock));
if (client != null) {
client.setAutoSysUnlock(unlock);
}
}
public void SetAutoSysUnlock(boolean unlock) { setAutoSysUnlock(unlock); }
public boolean isKeepAlive() {
if (client != null && client.getConfig() != null) return client.getConfig().isKeepAliveEnabled();
return keepAlive;
}
public boolean getKeepAlive() { return isKeepAlive(); }
public boolean IsKeepAlive() { return isKeepAlive(); }
public boolean GetKeepAlive() { return isKeepAlive(); }
public void setKeepAlive(boolean ka) {
this.keepAlive = ka;
this.properties.setProperty(ECLSession.SESSION_KEEPALIVE, String.valueOf(ka));
if (client != null) {
client.setKeepAliveEnabled(ka);
}
}
public void SetKeepAlive(boolean ka) { setKeepAlive(ka); }
public int getKeepAliveTimeout() {
if (client != null && client.getConfig() != null) return client.getConfig().getKeepAliveIntervalSeconds();
return keepAliveTimeout;
}
public int GetKeepAliveTimeout() { return getKeepAliveTimeout(); }
public void setKeepAliveTimeout(int timeout) {
this.keepAliveTimeout = timeout;
this.properties.setProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT, String.valueOf(timeout));
if (client != null) {
client.setKeepAliveIntervalSeconds(timeout);
}
}
public void SetKeepAliveTimeout(int timeout) { setKeepAliveTimeout(timeout); }
public String getKeepAliveType() {
if (client != null && client.getConfig() != null) return client.getConfig().getKeepAliveType();
return keepAliveType;
}
public String GetKeepAliveType() { return getKeepAliveType(); }
public void setKeepAliveType(String type) {
this.keepAliveType = type;
this.properties.setProperty(ECLSession.KEY_KEEPALIVE_TYPE, type != null ? type : "");
if (client != null && client.getConfig() != null) {
client.getConfig().setKeepAliveType(type);
}
}
public void SetKeepAliveType(String type) { setKeepAliveType(type); }
public boolean isAutoReconnect() {
if (client != null && client.getConfig() != null) return client.getConfig().isAutoReconnect();
return autoReconnect;
}
public boolean getAutoReconnect() { return isAutoReconnect(); }
public boolean IsAutoReconnect() { return isAutoReconnect(); }
public boolean GetAutoReconnect() { return isAutoReconnect(); }
public void setAutoReconnect(boolean ar) {
this.autoReconnect = ar;
this.properties.setProperty(ECLSession.SESSION_AUTORECONNECT, String.valueOf(ar));
if (client != null) {
client.setAutoReconnect(ar);
}
}
public void SetAutoReconnect(boolean ar) { setAutoReconnect(ar); }
public int getMaxRetry() {
if (client != null && client.getConfig() != null) return client.getConfig().getReconnectMaxRetries();
return maxRetry;
}
public int GetMaxRetry() { return getMaxRetry(); }
public void setMaxRetry(int retries) {
this.maxRetry = retries;
this.properties.setProperty(ECLSession.SESSION_RECONNECT_RETRIES, String.valueOf(retries));
if (client != null && client.getConfig() != null) {
client.getConfig().setReconnectMaxRetries(retries);
}
}
public void SetMaxRetry(int retries) { setMaxRetry(retries); }
public int getReconnectMaxRetries() { return getMaxRetry(); }
public void setReconnectMaxRetries(int retries) { setMaxRetry(retries); }
public void set_LULU_Session(boolean bl) { this.luluSession = bl; } public void set_LULU_Session(boolean bl) { this.luluSession = bl; }
public boolean is_LULU_Session() { return luluSession; } public boolean is_LULU_Session() { return luluSession; }
public boolean get_LULU_Session() { return luluSession; } public boolean get_LULU_Session() { return luluSession; }
public boolean isNegotiateCResolution() { return isNegCR; } public boolean isNegotiateCResolution() {
public void setNegotiatedCResolution(boolean bl) { this.isNegCR = bl; } if (client != null && client.getTelnetFSM() != null) {
return client.getTelnetFSM().isNegotiateContentionResolution();
}
return isNegCR;
}
public void setNegotiatedCResolution(boolean bl) {
this.isNegCR = bl;
if (client != null && client.getTelnetFSM() != null) {
client.getTelnetFSM().setNegotiateContentionResolution(bl);
}
}
public boolean isBIND7FArchitectureViolation() { return isBIND7FArchitectureViolation; } public boolean isBIND7FArchitectureViolation() { return isBIND7FArchitectureViolation; }
public void setBIND7FArchitectureViolation(boolean bl) { this.isBIND7FArchitectureViolation = bl; } public void setBIND7FArchitectureViolation(boolean bl) { this.isBIND7FArchitectureViolation = bl; }
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.ecl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
@@ -178,7 +181,20 @@ public class ECLOIA implements ECLConstants {
} }
} }
private final ReentrantLock oiaLock = new ReentrantLock();
private final Condition oiaCondition = oiaLock.newCondition();
public void signalWaiters() {
oiaLock.lock();
try {
oiaCondition.signalAll();
} finally {
oiaLock.unlock();
}
}
public synchronized void notifyOIAChanged() { public synchronized void notifyOIAChanged() {
signalWaiters();
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(), ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
getAlphanumericType(), isInsertMode(), getStatusString()); getAlphanumericType(), isInsertMode(), getStatusString());
for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) { for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) {
@@ -413,19 +429,28 @@ public class ECLOIA implements ECLConstants {
* @return true if keyboard unlocked, false if timeout occurred. * @return true if keyboard unlocked, false if timeout occurred.
*/ */
public boolean waitForInput(long timeoutMs) { public boolean waitForInput(long timeoutMs) {
long start = System.currentTimeMillis(); long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
while (System.currentTimeMillis() - start < timeoutMs) { if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) { return true;
return true; }
oiaLock.lock();
try {
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
while (getInputInhibited() != INHIBIT_NOT_INHIBITED) {
if (remainingNanos <= 0) {
return false;
}
try {
remainingNanos = oiaCondition.awaitNanos(remainingNanos);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} }
try { return true;
Thread.sleep(20); } finally {
} catch (InterruptedException e) { oiaLock.unlock();
Thread.currentThread().interrupt();
return false;
}
} }
return getInputInhibited() == INHIBIT_NOT_INHIBITED;
} }
/** /**
@@ -458,20 +483,26 @@ public class ECLOIA implements ECLConstants {
* Block until any OIA transition occurs. * Block until any OIA transition occurs.
*/ */
public boolean waitForTransition(long timeoutMs) { public boolean waitForTransition(long timeoutMs) {
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
int initialInhibit = getInputInhibited(); int initialInhibit = getInputInhibited();
long start = System.currentTimeMillis(); oiaLock.lock();
while (System.currentTimeMillis() - start < timeoutMs) { try {
if (getInputInhibited() != initialInhibit) { long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
return true; while (getInputInhibited() == initialInhibit) {
} if (remainingNanos <= 0) {
try { return false;
Thread.sleep(20); }
} catch (InterruptedException e) { try {
Thread.currentThread().interrupt(); remainingNanos = oiaCondition.awaitNanos(remainingNanos);
return false; } catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} }
return true;
} finally {
oiaLock.unlock();
} }
return getInputInhibited() != initialInhibit;
} }
public boolean WaitForTransition(long timeoutMs) { public boolean WaitForTransition(long timeoutMs) {
@@ -2,8 +2,12 @@ package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.charset.EbcdicTranslator; import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ExtendedAttribute; import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -37,6 +41,31 @@ public class ECLPS implements ECLConstants {
private final java.util.Map<ECLPSListener, ECLScreenDesc> descriptorListeners = new java.util.concurrent.ConcurrentHashMap<>(); private final java.util.Map<ECLPSListener, ECLScreenDesc> descriptorListeners = new java.util.concurrent.ConcurrentHashMap<>();
private final java.util.Map<ECLPSListener, Integer> listenerEventTypes = new java.util.concurrent.ConcurrentHashMap<>(); private final java.util.Map<ECLPSListener, Integer> listenerEventTypes = new java.util.concurrent.ConcurrentHashMap<>();
// Synchronization primitives for reactive automation waits (Phase 12)
private final ReentrantLock fallbackLock = new ReentrantLock();
private final Condition fallbackCondition = fallbackLock.newCondition();
public ReentrantLock getSyncLock() {
return (screen != null) ? screen.getSyncLock() : fallbackLock;
}
public Condition getSyncCondition() {
return (screen != null) ? screen.getSyncCondition() : fallbackCondition;
}
public void signalWaiters() {
if (screen != null) {
screen.signalWaiters();
} else {
fallbackLock.lock();
try {
fallbackCondition.signalAll();
} finally {
fallbackLock.unlock();
}
}
}
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) { public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
this.screen = screen; this.screen = screen;
this.inputProcessor = inputProcessor; this.inputProcessor = inputProcessor;
@@ -46,6 +75,27 @@ public class ECLPS implements ECLConstants {
this.bidiServices = new DefaultPSBIDIServices(this); this.bidiServices = new DefaultPSBIDIServices(this);
this.hindiServices = new DefaultPSHindiServices(this); this.hindiServices = new DefaultPSHindiServices(this);
this.thaiServices = new DefaultPSTHAIServices(this); this.thaiServices = new DefaultPSTHAIServices(this);
if (this.screen != null) {
this.screen.addUpdateListener(new ScreenUpdateListener() {
@Override
public void onScreenUpdated() {
signalWaiters();
}
@Override
public void onCursorMoved(int oldAddress, int newAddress) {
signalWaiters();
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
signalWaiters();
}
@Override
public void onKeyboardUnlocked() {
signalWaiters();
}
});
}
} }
public ECLPS(ECLSession session) { public ECLPS(ECLSession session) {
@@ -494,11 +544,71 @@ public class ECLPS implements ECLConstants {
return copyString(sRow, sCol, eRow, eCol); return copyString(sRow, sCol, eRow, eCol);
} }
private boolean enablePasteFromExcel = true;
private boolean pasteStopAtProtectedLine = false;
public boolean isEnablePasteFromExcel() {
if (session != null && session.getProperties() != null) {
String p = session.getProperties().getProperty(ECLSession.ENABLE_PASTE_FROM_EXCEL);
if (p != null) return Boolean.parseBoolean(p);
}
return enablePasteFromExcel;
}
public boolean IsEnablePasteFromExcel() { return isEnablePasteFromExcel(); }
public void setEnablePasteFromExcel(boolean val) {
this.enablePasteFromExcel = val;
if (session != null && session.getProperties() != null) {
session.getProperties().setProperty(ECLSession.ENABLE_PASTE_FROM_EXCEL, String.valueOf(val));
}
}
public void SetEnablePasteFromExcel(boolean val) { setEnablePasteFromExcel(val); }
public boolean isPasteStopAtProtectedLine() {
if (session != null && session.getProperties() != null) {
String p = session.getProperties().getProperty(ECLSession.PASTE_STOP_AT_PROTECTED_LINE);
if (p != null) return Boolean.parseBoolean(p);
}
return pasteStopAtProtectedLine;
}
public boolean IsPasteStopAtProtectedLine() { return isPasteStopAtProtectedLine(); }
public void setPasteStopAtProtectedLine(boolean val) {
this.pasteStopAtProtectedLine = val;
if (session != null && session.getProperties() != null) {
session.getProperties().setProperty(ECLSession.PASTE_STOP_AT_PROTECTED_LINE, String.valueOf(val));
}
}
public void SetPasteStopAtProtectedLine(boolean val) { setPasteStopAtProtectedLine(val); }
public synchronized int pasteFromExcel(String text, int row, int col) {
if (text == null || text.isEmpty() || screen == null) return 0;
if (row >= 0 && col >= 0) {
setCursorPos(row, col);
}
if (inputProcessor != null) {
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine());
}
return 0;
}
public int PasteFromExcel(String text, int row, int col) {
return pasteFromExcel(text, row, col);
}
/** /**
* Paste a multi-line rectangular block of text starting at (row, col). * Paste a multi-line rectangular block of text starting at (row, col).
*/ */
public synchronized int pasteString(String text, int row, int col) { public synchronized int pasteString(String text, int row, int col) {
if (text == null || text.isEmpty() || screen == null) return 0; if (text == null || text.isEmpty() || screen == null) return 0;
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine())) {
setCursorPos(row, col);
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine());
}
int rows = screen.getRows(); int rows = screen.getRows();
int cols = screen.getCols(); int cols = screen.getCols();
if (rows <= 0 || cols <= 0) return 0; if (rows <= 0 || cols <= 0) return 0;
@@ -532,7 +642,33 @@ public class ECLPS implements ECLConstants {
} }
public int pasteRectangular(String text, int row, int col) { public int pasteRectangular(String text, int row, int col) {
return pasteString(text, row, col); if (text == null || text.isEmpty() || screen == null) return 0;
int rows = screen.getRows();
int cols = screen.getCols();
if (rows <= 0 || cols <= 0) return 0;
String[] lines = text.split("\r?\n");
int count = 0;
for (int i = 0; i < lines.length; i++) {
int targetRow = (row + i) % rows;
String line = lines[i];
for (int c = 0; c < line.length() && (col + c) < cols; c++) {
int pos = targetRow * cols + (col + c);
if (screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(pos);
if (faIsProtected(fa & 0xFF) || screen.getCell(pos).isFieldAttribute()) {
continue;
}
}
setCursorPos(pos);
if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(c));
}
count++;
}
}
return count;
} }
public int PasteRectangular(String text, int row, int col) { public int PasteRectangular(String text, int row, int col) {
@@ -848,6 +984,20 @@ public class ECLPS implements ECLConstants {
UnregisterPSEvent(listener); UnregisterPSEvent(listener);
} }
public void dispatchEvent(ECLPSEvent event) {
notifyPSEvent(event);
}
public void notifyKeyUnlocked() {
int r = (screen != null) ? screen.getRows() : 0;
int c = (screen != null) ? screen.getCols() : 0;
int cur = (screen != null) ? screen.getCursorAddress() : 0;
int ring = ringCounter.incrementAndGet();
signalWaiters();
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.EVENT_KEY_UNLOCKED, HOST_EVENTS, 0, 0,
Math.max(0, r - 1), Math.max(0, c - 1), cur, cur, r, c, false, cursorVisible, ring, false, null));
}
public void notifyPSEvent(ECLPSEvent event) { public void notifyPSEvent(ECLPSEvent event) {
for (ECLPSListener l : psListeners) { for (ECLPSListener l : psListeners) {
ECLScreenDesc desc = descriptorListeners.get(l); ECLScreenDesc desc = descriptorListeners.get(l);
@@ -899,6 +1049,7 @@ public class ECLPS implements ECLConstants {
int c = (screen != null) ? screen.getCols() : 0; int c = (screen != null) ? screen.getCols() : 0;
int cur = (screen != null) ? screen.getCursorAddress() : 0; int cur = (screen != null) ? screen.getCursorAddress() : 0;
int ring = ringCounter.incrementAndGet(); int ring = ringCounter.incrementAndGet();
signalWaiters();
ECLPSEvent evt = new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, type, startRow, startCol, endRow, endCol, ECLPSEvent evt = new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, type, startRow, startCol, endRow, endCol,
cur, cur, r, c, full, cursorVisible, ring, startPrinter, null); cur, cur, r, c, full, cursorVisible, ring, startPrinter, null);
notifyPSEvent(evt); notifyPSEvent(evt);
@@ -910,12 +1061,14 @@ public class ECLPS implements ECLConstants {
int row = (c > 0) ? newAddress / c : 0; int row = (c > 0) ? newAddress / c : 0;
int col = (c > 0) ? newAddress % c : 0; int col = (c > 0) ? newAddress % c : 0;
int ring = ringCounter.incrementAndGet(); int ring = ringCounter.incrementAndGet();
signalWaiters();
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, USER_EVENTS, row, col, row, col, notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, USER_EVENTS, row, col, row, col,
oldAddress, newAddress, r, c, false, cursorVisible, ring, false, null)); oldAddress, newAddress, r, c, false, cursorVisible, ring, false, null));
} }
public void notifyAlarm() { public void notifyAlarm() {
int ring = ringCounter.incrementAndGet(); int ring = ringCounter.incrementAndGet();
signalWaiters();
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM, HOST_EVENTS, 0, 0, 0, 0, notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM, HOST_EVENTS, 0, 0, 0, 0,
0, 0, 0, 0, false, cursorVisible, ring, false, null)); 0, 0, 0, 0, false, cursorVisible, ring, false, null));
} }
@@ -923,6 +1076,7 @@ public class ECLPS implements ECLConstants {
public void notifyScreenResized(int rows, int cols) { public void notifyScreenResized(int rows, int cols) {
int cur = (screen != null) ? screen.getCursorAddress() : 0; int cur = (screen != null) ? screen.getCursorAddress() : 0;
int ring = ringCounter.incrementAndGet(); int ring = ringCounter.incrementAndGet();
signalWaiters();
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, HOST_EVENTS, 0, 0, rows - 1, cols - 1, notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, HOST_EVENTS, 0, 0, rows - 1, cols - 1,
cur, cur, rows, cols, true, cursorVisible, ring, false, null)); cur, cur, rows, cols, true, cursorVisible, ring, false, null));
} }
@@ -1067,20 +1221,30 @@ public class ECLPS implements ECLConstants {
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) {
if (desc == null) return true; if (desc == null) return true;
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs; long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
long start = System.currentTimeMillis();
ECLOIA oia = (session != null) ? session.GetOIA() : null; ECLOIA oia = (session != null) ? session.GetOIA() : null;
while (System.currentTimeMillis() - start < limit) { if (desc.Matches(this, oia)) {
if (desc.Matches(this, oia)) { return true;
return true; }
ReentrantLock lock = getSyncLock();
Condition cond = getSyncCondition();
lock.lock();
try {
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
while (!desc.Matches(this, oia)) {
if (remainingNanos <= 0) {
return false;
}
try {
remainingNanos = cond.awaitNanos(remainingNanos);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} }
try { return true;
Thread.sleep(25); } finally {
} catch (InterruptedException e) { lock.unlock();
Thread.currentThread().interrupt();
return false;
}
} }
return desc.Matches(this, oia);
} }
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) { public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
@@ -1097,20 +1261,30 @@ public class ECLPS implements ECLConstants {
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
if (desc == null) return true; if (desc == null) return true;
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs; long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
long start = System.currentTimeMillis();
ECLOIA oia = (session != null) ? session.GetOIA() : null; ECLOIA oia = (session != null) ? session.GetOIA() : null;
while (System.currentTimeMillis() - start < limit) { if (!desc.Matches(this, oia)) {
if (!desc.Matches(this, oia)) { return true;
return true; }
ReentrantLock lock = getSyncLock();
Condition cond = getSyncCondition();
lock.lock();
try {
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
while (desc.Matches(this, oia)) {
if (remainingNanos <= 0) {
return false;
}
try {
remainingNanos = cond.awaitNanos(remainingNanos);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} }
try { return true;
Thread.sleep(25); } finally {
} catch (InterruptedException e) { lock.unlock();
Thread.currentThread().interrupt();
return false;
}
} }
return !desc.Matches(this, oia);
} }
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) { public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
@@ -1121,69 +1295,178 @@ public class ECLPS implements ECLConstants {
* Block until the specified text appears anywhere on the presentation space. * Block until the specified text appears anywhere on the presentation space.
*/ */
public boolean waitForScreen(String text, long timeoutMs) { public boolean waitForScreen(String text, long timeoutMs) {
long start = System.currentTimeMillis(); if (text == null || text.isEmpty()) return true;
while (System.currentTimeMillis() - start < timeoutMs) { long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
if (searchString(text) >= 0) { if (searchString(text) >= 0) {
return true; return true;
}
ReentrantLock lock = getSyncLock();
Condition cond = getSyncCondition();
lock.lock();
try {
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
while (searchString(text) < 0) {
if (remainingNanos <= 0) {
return false;
}
try {
remainingNanos = cond.awaitNanos(remainingNanos);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} }
try { return true;
Thread.sleep(25); } finally {
} catch (InterruptedException e) { lock.unlock();
Thread.currentThread().interrupt();
return false;
}
} }
return searchString(text) >= 0;
} }
public boolean WaitForScreen(String text, long timeoutMs) { public boolean WaitForScreen(String text, long timeoutMs) {
return waitForScreen(text, timeoutMs); return waitForScreen(text, timeoutMs);
} }
public boolean waitForString(String text) {
return waitForScreen(text, -1L);
}
public boolean WaitForString(String text) {
return waitForScreen(text, -1L);
}
public boolean waitForString(String text, long timeoutMs) {
return waitForScreen(text, timeoutMs);
}
public boolean WaitForString(String text, long timeoutMs) {
return waitForScreen(text, timeoutMs);
}
/** /**
* Block until the specified text appears at the given (row, col) coordinate. * Block until the specified text appears at the given (row, col) coordinate.
*/ */
public boolean waitForScreen(String text, int row, int col, long timeoutMs) { public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
long start = System.currentTimeMillis(); if (text == null) return true;
while (System.currentTimeMillis() - start < timeoutMs) { long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
String onScreen = getString(row, col, text.length()); if (text.equals(getString(row, col, text.length()))) {
if (text.equals(onScreen)) { return true;
return true; }
ReentrantLock lock = getSyncLock();
Condition cond = getSyncCondition();
lock.lock();
try {
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
while (!text.equals(getString(row, col, text.length()))) {
if (remainingNanos <= 0) {
return false;
}
try {
remainingNanos = cond.awaitNanos(remainingNanos);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} }
try { return true;
Thread.sleep(25); } finally {
} catch (InterruptedException e) { lock.unlock();
Thread.currentThread().interrupt();
return false;
}
} }
return text.equals(getString(row, col, text.length()));
} }
public boolean WaitForScreen(String text, int row, int col, long timeoutMs) { public boolean WaitForScreen(String text, int row, int col, long timeoutMs) {
return waitForScreen(text, row, col, timeoutMs); return waitForScreen(text, row, col, timeoutMs);
} }
public boolean waitForString(String text, int row, int col, long timeoutMs) {
return waitForScreen(text, row, col, timeoutMs);
}
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
return waitForScreen(text, row, col, timeoutMs);
}
/** /**
* Block until the cursor moves to (row, col). * Block until the cursor moves to (row, col).
*/ */
public boolean waitForCursor(int row, int col, long timeoutMs) { public boolean waitForCursor(int row, int col, long timeoutMs) {
long start = System.currentTimeMillis(); long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
while (System.currentTimeMillis() - start < timeoutMs) { if (getCursorRow() == row && getCursorCol() == col) {
if (getCursorRow() == row && getCursorCol() == col) { return true;
return true; }
ReentrantLock lock = getSyncLock();
Condition cond = getSyncCondition();
lock.lock();
try {
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
while (getCursorRow() != row || getCursorCol() != col) {
if (remainingNanos <= 0) {
return false;
}
try {
remainingNanos = cond.awaitNanos(remainingNanos);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} }
try { return true;
Thread.sleep(25); } finally {
} catch (InterruptedException e) { lock.unlock();
Thread.currentThread().interrupt();
return false;
}
} }
return getCursorRow() == row && getCursorCol() == col;
} }
public boolean WaitForCursor(int row, int col, long timeoutMs) { public boolean WaitForCursor(int row, int col, long timeoutMs) {
return waitForCursor(row, col, timeoutMs); return waitForCursor(row, col, timeoutMs);
} }
protected boolean locked_SYSLOCK = false;
protected boolean locked_TWAIT = false;
public void lockKeyboard() {
lockKeyboard(8);
}
public void lockKeyboard(int reason) {
if (reason == 7) locked_TWAIT = true;
if (reason == 8) {
locked_SYSLOCK = true;
if (session != null && session.getOIA() != null) {
session.getOIA().setDoNotEnter(8, 0);
}
}
if (inputProcessor != null) {
inputProcessor.setKeyboardLocked(true);
}
}
public void unlockKeyboard() {
unlockKeyboard(8);
}
public void unlockKeyboard(int reason) {
if (reason == 7) locked_TWAIT = false;
if (reason == 8) {
locked_SYSLOCK = false;
if (session != null && session.getOIA() != null) {
session.getOIA().clearDoNotEnter();
}
}
if (!locked_TWAIT && !locked_SYSLOCK) {
if (inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
}
}
}
public boolean islocked_TWAIT() {
return locked_TWAIT;
}
public boolean islocked_SYSLOCK() {
if (locked_SYSLOCK) return true;
if (session != null && session.getOIA() != null && session.getOIA().isXSystem()) {
return true;
}
return false;
}
} }
@@ -27,6 +27,7 @@ public class ECLPSEvent extends EventObject {
public static final int EVENT_ALARM = PS_ALARM; public static final int EVENT_ALARM = PS_ALARM;
public static final int EVENT_RESIZE = PS_RESIZE; public static final int EVENT_RESIZE = PS_RESIZE;
public static final int EVENT_CLOSE = PS_CLOSE; public static final int EVENT_CLOSE = PS_CLOSE;
public static final int EVENT_KEY_UNLOCKED = PS_UPDATE; // HoD event type 1 for keyboard unlock / update
private final int eventType; private final int eventType;
private final int type; private final int type;
@@ -1,10 +1,11 @@
package haus.nightmare.lib3270j.ecl; package haus.nightmare.lib3270j.ecl;
import java.awt.Image; import haus.nightmare.lib3270j.graphics.PixelBuffer;
import java.awt.Rectangle; import haus.nightmare.lib3270j.graphics.Rectangle;
/** /**
* Conforms to IBM Host On-Demand ECLPSGraphicsEvent. * Conforms to IBM Host On-Demand ECLPSGraphicsEvent.
* Decoupled from java.awt.
*/ */
public class ECLPSGraphicsEvent { public class ECLPSGraphicsEvent {
public static final int GRAPHICS_CURSOR_ON = 1; public static final int GRAPHICS_CURSOR_ON = 1;
@@ -14,7 +15,7 @@ public class ECLPSGraphicsEvent {
public static final int GRAPHICS_UPDATED = 5; public static final int GRAPHICS_UPDATED = 5;
private int id; private int id;
private Image image; private Object image;
private Rectangle rect; private Rectangle rect;
private ECLPS source; private ECLPS source;
@@ -23,13 +24,13 @@ public class ECLPSGraphicsEvent {
this.id = id; this.id = id;
} }
public ECLPSGraphicsEvent(ECLPS source, int id, Image image) { public ECLPSGraphicsEvent(ECLPS source, int id, Object image) {
this.source = source; this.source = source;
this.id = id; this.id = id;
this.image = image; this.image = image;
} }
public ECLPSGraphicsEvent(ECLPS source, int id, Image image, Rectangle rectangle) { public ECLPSGraphicsEvent(ECLPS source, int id, Object image, Rectangle rectangle) {
this.source = source; this.source = source;
this.id = id; this.id = id;
this.image = image; this.image = image;
@@ -46,9 +47,13 @@ public class ECLPSGraphicsEvent {
public int getID() { return this.id; } public int getID() { return this.id; }
public int GetID() { return this.id; } public int GetID() { return this.id; }
public void setImage(Image image) { this.image = image; } public void setImage(Object image) { this.image = image; }
public Image getImage() { return this.image; } public Object getImage() { return this.image; }
public Image GetImage() { return this.image; } public Object GetImage() { return this.image; }
public PixelBuffer getPixelBuffer() {
return (this.image instanceof PixelBuffer) ? (PixelBuffer) this.image : null;
}
public void setRectangle(Rectangle rect) { this.rect = rect; } public void setRectangle(Rectangle rect) { this.rect = rect; }
public Rectangle getRectangle() { return this.rect; } public Rectangle getRectangle() { return this.rect; }
@@ -1,13 +1,13 @@
package haus.nightmare.lib3270j.ecl; package haus.nightmare.lib3270j.ecl;
import java.awt.Color; import haus.nightmare.lib3270j.graphics.Color;
import java.awt.Component;
/** /**
* Presentation Space graphics services interface conforming to IBM Host On-Demand ECL. * Presentation Space graphics services interface conforming to IBM Host On-Demand ECL.
* Completely decoupled from java.awt.
*/ */
public interface ECLPSGraphicsServices { public interface ECLPSGraphicsServices {
void setVisualComponent(Component comp); void setVisualComponent(Object comp);
void setGraphicColor(Color[] colors, boolean b); void setGraphicColor(Color[] colors, boolean b);
void mousePressed(int x, int y, int button); void mousePressed(int x, int y, int button);
void addGraphicsListener(ECLPSGraphicsListener listener); void addGraphicsListener(ECLPSGraphicsListener listener);
@@ -18,7 +18,7 @@ public class ECLSession {
private static final Logger log = Logger.getLogger(ECLSession.class.getName()); private static final Logger log = Logger.getLogger(ECLSession.class.getName());
// Standard IBM HoD Session Property Keys // Standard Session Property Keys
public static final String SESSION_HOST = "SESSION_HOST"; public static final String SESSION_HOST = "SESSION_HOST";
public static final String SESSION_PORT = "SESSION_PORT"; public static final String SESSION_PORT = "SESSION_PORT";
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE"; public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
@@ -29,6 +29,24 @@ public class ECLSession {
public static final String SESSION_TN3270E = "SESSION_TN3270E"; public static final String SESSION_TN3270E = "SESSION_TN3270E";
public static final String SESSION_WIN_TITLE = "SESSION_WIN_TITLE"; public static final String SESSION_WIN_TITLE = "SESSION_WIN_TITLE";
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT"; public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode";
public static final String ENTRYASSIST_DOCWORDWRAP = "EntryAssist_DOCwordWrap";
public static final String ENTRYASSIST_STARTCOL = "EntryAssist_startCol";
public static final String ENTRYASSIST_ENDCOL = "EntryAssist_endCol";
public static final String ENTRYASSIST_BELL = "EntryAssist_bell";
public static final String ENTRYASSIST_BELLCOL = "EntryAssist_bellCol";
public static final String ENTRYASSIST_TABSTOPS = "EntryAssist_tabstops";
private final Telnet3270Client client; private final Telnet3270Client client;
private final ECLConnection connection; private final ECLConnection connection;
@@ -98,6 +116,9 @@ public class ECLSession {
String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true"); String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true");
config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr)); config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr));
String autoSysStr = getProp(props, SESSION_AUTO_SYS_UNLOCK, "autoSysUnlock", "AutoSysUnlock", "true");
config.setAutoSysUnlock("true".equalsIgnoreCase(autoSysStr) || "yes".equalsIgnoreCase(autoSysStr) || "1".equals(autoSysStr));
String certUrl = getProp(props, "certificateURL", "CERTIFICATE_URL", "certificate_url", null); String certUrl = getProp(props, "certificateURL", "CERTIFICATE_URL", "certificate_url", null);
if (certUrl != null) config.setKeyStorePath(certUrl); if (certUrl != null) config.setKeyStorePath(certUrl);
String certPwd = getProp(props, "certificatePassword", "CERTIFICATE_PASSWORD", "certificate_password", null); String certPwd = getProp(props, "certificatePassword", "CERTIFICATE_PASSWORD", "certificate_password", null);
@@ -118,6 +139,25 @@ public class ECLSession {
config.setEnabledProtocols(tlsVer); config.setEnabledProtocols(tlsVer);
} }
String keepAliveStr = getProp(props, SESSION_KEEPALIVE, "keepAlive", "keepalive", "true");
config.setKeepAliveEnabled("true".equalsIgnoreCase(keepAliveStr) || "yes".equalsIgnoreCase(keepAliveStr) || "1".equals(keepAliveStr));
String kaTimeoutStr = getProp(props, KEY_KEEPALIVE_TIMEOUT, "keepAliveTimeout", "keepalivetimeout", null);
if (kaTimeoutStr != null) {
try { config.setKeepAliveIntervalSeconds(Integer.parseInt(kaTimeoutStr.trim())); } catch (NumberFormatException ignored) {}
}
String kaTypeStr = getProp(props, KEY_KEEPALIVE_TYPE, "keepAliveType", "keepalivetype", null);
if (kaTypeStr != null) config.setKeepAliveType(kaTypeStr);
String autoReconnectStr = getProp(props, SESSION_AUTORECONNECT, "autoReconnect", "autoreconnect", "false");
config.setAutoReconnect("true".equalsIgnoreCase(autoReconnectStr) || "yes".equalsIgnoreCase(autoReconnectStr) || "1".equals(autoReconnectStr));
String retriesStr = getProp(props, SESSION_RECONNECT_RETRIES, "reconnectMaxRetries", "reconnectRetries", null);
if (retriesStr != null) {
try { config.setReconnectMaxRetries(Integer.parseInt(retriesStr.trim())); } catch (NumberFormatException ignored) {}
}
return config; return config;
} }
@@ -161,6 +201,7 @@ public class ECLSession {
properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls())); properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls()));
if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName()); if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName());
properties.setProperty(SESSION_TN3270E, String.valueOf(cfg.isTn3270eEnabled())); properties.setProperty(SESSION_TN3270E, String.valueOf(cfg.isTn3270eEnabled()));
properties.setProperty(SESSION_AUTO_SYS_UNLOCK, String.valueOf(cfg.isAutoSysUnlock()));
} }
} }
@@ -371,6 +412,42 @@ public class ECLSession {
} }
} }
public boolean isKeepAlive() {
return (client != null) ? client.isKeepAliveEnabled() : true;
}
public boolean IsKeepAlive() { return isKeepAlive(); }
public void setKeepAlive(boolean keepAlive) {
this.properties.setProperty(SESSION_KEEPALIVE, String.valueOf(keepAlive));
if (client != null) {
client.setKeepAliveEnabled(keepAlive);
}
}
public void SetKeepAlive(boolean keepAlive) { setKeepAlive(keepAlive); }
public int getKeepAliveTimeout() {
return (client != null) ? client.getKeepAliveIntervalSeconds() : 120;
}
public int GetKeepAliveTimeout() { return getKeepAliveTimeout(); }
public void setKeepAliveTimeout(int timeout) {
this.properties.setProperty(KEY_KEEPALIVE_TIMEOUT, String.valueOf(timeout));
if (client != null) {
client.setKeepAliveIntervalSeconds(timeout);
}
}
public void SetKeepAliveTimeout(int timeout) { setKeepAliveTimeout(timeout); }
public boolean isAutoReconnect() {
return (client != null) ? client.isAutoReconnect() : false;
}
public boolean IsAutoReconnect() { return isAutoReconnect(); }
public void setAutoReconnect(boolean autoReconnect) {
this.properties.setProperty(SESSION_AUTORECONNECT, String.valueOf(autoReconnect));
if (client != null) {
client.setAutoReconnect(autoReconnect);
}
}
public void SetAutoReconnect(boolean autoReconnect) { setAutoReconnect(autoReconnect); }
// ========== Automation Keystrokes & Waits ========== // ========== Automation Keystrokes & Waits ==========
/** /**
@@ -455,6 +532,41 @@ public class ECLSession {
dispose(); dispose();
} }
public boolean isAutoSysUnlock() {
if (client != null && client.getConfig() != null) {
return client.getConfig().isAutoSysUnlock();
}
String s = properties.getProperty(SESSION_AUTO_SYS_UNLOCK);
return s != null ? Boolean.parseBoolean(s) : true;
}
public boolean getAutoSysUnlock() { return isAutoSysUnlock(); }
public boolean IsAutoSysUnlock() { return isAutoSysUnlock(); }
public boolean GetAutoSysUnlock() { return isAutoSysUnlock(); }
public void setAutoSysUnlock(boolean unlock) {
properties.setProperty(SESSION_AUTO_SYS_UNLOCK, String.valueOf(unlock));
if (client != null) {
client.setAutoSysUnlock(unlock);
}
}
public void SetAutoSysUnlock(boolean unlock) { setAutoSysUnlock(unlock); }
public boolean getContentionResolution() {
if (connection != null) return connection.getContentionResolution();
if (client != null) return client.isContentionResolution();
return false;
}
public boolean isContentionResolution() { return getContentionResolution(); }
public boolean GetContentionResolution() { return getContentionResolution(); }
public boolean IsContentionResolution() { return getContentionResolution(); }
public void setContentionResolution(boolean cr) {
if (connection != null) {
connection.setContentionResolution(cr);
}
}
public void SetContentionResolution(boolean cr) { setContentionResolution(cr); }
@Override @Override
public String toString() { public String toString() {
return String.format("ECLSession[host=%s, port=%d, connected=%b, state=%s]", return String.format("ECLSession[host=%s, port=%d, connected=%b, state=%s]",
@@ -0,0 +1,96 @@
package haus.nightmare.lib3270j.graphics;
import java.io.Serializable;
import java.util.Objects;
/**
* Platform-neutral Color representation encapsulating 32-bit ARGB.
* Completely decouples lib3270j from java.awt.Color.
*/
public class Color implements Serializable {
private static final long serialVersionUID = 1L;
public static final Color WHITE = new Color(0xFFFFFFFF);
public static final Color LIGHT_GRAY = new Color(0xFFC0C0C0);
public static final Color GRAY = new Color(0xFF808080);
public static final Color DARK_GRAY = new Color(0xFF404040);
public static final Color BLACK = new Color(0xFF000000);
public static final Color RED = new Color(0xFFFF0000);
public static final Color PINK = new Color(0xFFFFAFAF);
public static final Color ORANGE = new Color(0xFFFFC800);
public static final Color YELLOW = new Color(0xFFFFFF00);
public static final Color GREEN = new Color(0xFF00FF00);
public static final Color MAGENTA = new Color(0xFFFF00FF);
public static final Color CYAN = new Color(0xFF00FFFF);
public static final Color BLUE = new Color(0xFF0000FF);
private final int value;
public Color(int rgb) {
this.value = 0xFF000000 | rgb;
}
public Color(int rgba, boolean hasAlpha) {
if (hasAlpha) {
this.value = rgba;
} else {
this.value = 0xFF000000 | rgba;
}
}
public Color(int r, int g, int b) {
this(r, g, b, 255);
}
public Color(int r, int g, int b, int a) {
this.value = ((a & 0xFF) << 24) |
((r & 0xFF) << 16) |
((g & 0xFF) << 8) |
(b & 0xFF);
}
public Color(float r, float g, float b) {
this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5));
}
public Color(float r, float g, float b, float a) {
this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5), (int) (a * 255 + 0.5));
}
public int getRGB() {
return value;
}
public int getRed() {
return (value >> 16) & 0xFF;
}
public int getGreen() {
return (value >> 8) & 0xFF;
}
public int getBlue() {
return value & 0xFF;
}
public int getAlpha() {
return (value >> 24) & 0xFF;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Color)) return false;
return this.value == ((Color) obj).value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return getClass().getName() + "[r=" + getRed() + ",g=" + getGreen() + ",b=" + getBlue() + ",a=" + getAlpha() + "]";
}
}
@@ -0,0 +1,277 @@
package haus.nightmare.lib3270j.graphics;
import java.util.Arrays;
/**
* Default standalone implementation of PixelBuffer.
* Provides pure Java software rasterization into a 32-bit ARGB contiguous integer array.
*/
public class DefaultPixelBuffer implements PixelBuffer {
private int width;
private int height;
private int[] pixels;
private boolean hasClip = false;
private int clipX;
private int clipY;
private int clipWidth;
private int clipHeight;
public DefaultPixelBuffer(int width, int height) {
this.width = Math.max(1, width);
this.height = Math.max(1, height);
this.pixels = new int[this.width * this.height];
}
public DefaultPixelBuffer(int width, int height, int[] existingPixels) {
this.width = Math.max(1, width);
this.height = Math.max(1, height);
if (existingPixels != null && existingPixels.length >= this.width * this.height) {
this.pixels = existingPixels;
} else {
this.pixels = new int[this.width * this.height];
}
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
@Override
public int[] getPixels() {
return pixels;
}
@Override
public synchronized int getPixel(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height) return 0;
return pixels[y * width + x];
}
@Override
public synchronized void setPixelDirect(int x, int y, int argb) {
if (x < 0 || x >= width || y < 0 || y >= height) return;
if (isClipped(x, y)) return;
pixels[y * width + x] = argb;
}
@Override
public synchronized void setPixel(int x, int y, int argb) {
if (x < 0 || x >= width || y < 0 || y >= height) return;
if (isClipped(x, y)) return;
int srcA = (argb >>> 24) & 0xFF;
if (srcA == 0) return;
int idx = y * width + x;
if (srcA == 255) {
pixels[idx] = argb;
return;
}
int dst = pixels[idx];
int dstA = (dst >>> 24) & 0xFF;
if (dstA == 0) {
pixels[idx] = argb;
return;
}
int srcR = (argb >>> 16) & 0xFF;
int srcG = (argb >>> 8) & 0xFF;
int srcB = argb & 0xFF;
int dstR = (dst >>> 16) & 0xFF;
int dstG = (dst >>> 8) & 0xFF;
int dstB = dst & 0xFF;
int outA = srcA + dstA * (255 - srcA) / 255;
if (outA == 0) {
pixels[idx] = 0;
return;
}
int outR = (srcR * srcA + dstR * dstA * (255 - srcA) / 255) / outA;
int outG = (srcG * srcA + dstG * dstA * (255 - srcA) / 255) / outA;
int outB = (srcB * srcA + dstB * dstA * (255 - srcA) / 255) / outA;
pixels[idx] = (outA << 24) | (outR << 16) | (outG << 8) | outB;
}
@Override
public synchronized void clear() {
clear(0);
}
@Override
public synchronized void clear(int argb) {
Arrays.fill(pixels, argb);
}
@Override
public synchronized void setClip(int x, int y, int width, int height) {
this.hasClip = true;
this.clipX = x;
this.clipY = y;
this.clipWidth = Math.max(0, width);
this.clipHeight = Math.max(0, height);
}
@Override
public synchronized void clearClip() {
this.hasClip = false;
}
@Override
public boolean isClipped(int x, int y) {
if (!hasClip) return false;
return x < clipX || x >= (clipX + clipWidth) || y < clipY || y >= (clipY + clipHeight);
}
@Override
public synchronized void fillRect(int x, int y, int w, int h, int argb) {
if (w <= 0 || h <= 0) return;
int x1 = Math.max(0, x);
int y1 = Math.max(0, y);
int x2 = Math.min(width, x + w);
int y2 = Math.min(height, y + h);
if (hasClip) {
x1 = Math.max(x1, clipX);
y1 = Math.max(y1, clipY);
x2 = Math.min(x2, clipX + clipWidth);
y2 = Math.min(y2, clipY + clipHeight);
}
for (int row = y1; row < y2; row++) {
int rowOffset = row * width;
Arrays.fill(pixels, rowOffset + x1, rowOffset + x2, argb);
}
}
@Override
public synchronized void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY) {
if (srcPixels == null || srcW <= 0 || srcH <= 0) return;
for (int r = 0; r < srcH; r++) {
int sy = srcY + r;
int dy = dstY + r;
if (dy < 0 || dy >= height) continue;
for (int c = 0; c < srcW; c++) {
int sx = srcX + c;
int dx = dstX + c;
if (dx < 0 || dx >= width) continue;
if (isClipped(dx, dy)) continue;
int sp = srcPixels[sy * srcW + sx];
setPixel(dx, dy, sp);
}
}
}
@Override
public synchronized void drawLine(int x1, int y1, int x2, int y2, int argb) {
drawLineBresenham(x1, y1, x2, y2, argb);
}
@Override
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
int curX = x0;
int curY = y0;
while (true) {
setPixel(curX, curY, color);
if (curX == x1 && curY == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
curX += sx;
}
if (e2 < dx) {
err += dx;
curY += sy;
}
}
}
@Override
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
int lw = Math.max(1, lineWidth);
int ix0 = (int) Math.round(x0);
int iy0 = (int) Math.round(y0);
int ix1 = (int) Math.round(x1);
int iy1 = (int) Math.round(y1);
if (lw == 1) {
drawLineBresenham(ix0, iy0, ix1, iy1, colorArgb);
} else {
int half = lw / 2;
for (int ox = -half; ox <= half; ox++) {
for (int oy = -half; oy <= half; oy++) {
drawLineBresenham(ix0 + ox, iy0 + oy, ix1 + ox, iy1 + oy, colorArgb);
}
}
}
}
@Override
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int colorArgb, double strokeWidth) {
// Pure Java anti-aliased line rendering
double dx = x1 - x0;
double dy = y1 - y0;
double len = Math.hypot(dx, dy);
if (len < 1e-4) {
setPixel((int) Math.round(x0), (int) Math.round(y0), colorArgb);
return;
}
double radius = Math.max(0.5, strokeWidth * 0.5);
int minX = (int) Math.floor(Math.min(x0, x1) - radius - 1);
int maxX = (int) Math.ceil(Math.max(x0, x1) + radius + 1);
int minY = (int) Math.floor(Math.min(y0, y1) - radius - 1);
int maxY = (int) Math.ceil(Math.max(y0, y1) + radius + 1);
minX = Math.max(0, minX);
maxX = Math.min(width - 1, maxX);
minY = Math.max(0, minY);
maxY = Math.min(height - 1, maxY);
int baseAlpha = (colorArgb >>> 24) & 0xFF;
if (baseAlpha == 0) baseAlpha = 255;
int rgbOnly = colorArgb & 0x00FFFFFF;
double invLenSq = 1.0 / (len * len);
for (int py = minY; py <= maxY; py++) {
for (int px = minX; px <= maxX; px++) {
double u = ((px - x0) * dx + (py - y0) * dy) * invLenSq;
u = Math.max(0.0, Math.min(1.0, u));
double projX = x0 + u * dx;
double projY = y0 + u * dy;
double dist = Math.hypot(px - projX, py - projY);
if (dist <= radius) {
double coverage = 1.0 - (dist / radius);
coverage = Math.sin(coverage * Math.PI * 0.5); // Smooth cosine roll-off
int effectiveAlpha = (int) (baseAlpha * coverage);
if (effectiveAlpha > 0) {
setPixel(px, py, (effectiveAlpha << 24) | rgbOnly);
}
}
}
}
}
}
@@ -0,0 +1,66 @@
package haus.nightmare.lib3270j.graphics;
import java.io.Serializable;
import java.util.Objects;
/**
* Lightweight pure-Java POJO dimension for 2D width and height.
* Completely decouples lib3270j from java.awt.Dimension.
*/
public class Dimension implements Serializable {
private static final long serialVersionUID = 1L;
public int width;
public int height;
public Dimension() {
this(0, 0);
}
public Dimension(Dimension d) {
this(d != null ? d.width : 0, d != null ? d.height : 0);
}
public Dimension(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public void setSize(int width, int height) {
this.width = width;
this.height = height;
}
public void setSize(Dimension d) {
if (d != null) {
this.width = d.width;
this.height = d.height;
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Dimension)) return false;
Dimension d = (Dimension) obj;
return (width == d.width) && (height == d.height);
}
@Override
public int hashCode() {
return Objects.hash(width, height);
}
@Override
public String toString() {
return getClass().getName() + "[width=" + width + ",height=" + height + "]";
}
}
@@ -51,7 +51,7 @@ public class FillArea {
/** /**
* IBM Host On-Demand multi-polygon constructor. * IBM Host On-Demand multi-polygon constructor.
*/ */
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, java.awt.Color color) { public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, Color color) {
this(); this();
if (color != null) { if (color != null) {
this.fillColor = color.getRGB(); this.fillColor = color.getRGB();
@@ -72,9 +72,28 @@ public class FillArea {
} }
} }
public synchronized java.awt.Rectangle getBounds() { public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, int argb) {
this();
this.fillColor = argb;
if (px != null && py != null && polyCounts != null) {
int offset = 0;
for (int i = 0; i < numPolys && i < polyCounts.length; i++) {
int count = polyCounts[i];
if (count >= 2 && offset + count <= px.length && offset + count <= py.length) {
int[] sx = new int[count];
int[] sy = new int[count];
System.arraycopy(px, offset, sx, 0, count);
System.arraycopy(py, offset, sy, 0, count);
addPolygon(sx, sy, count);
}
offset += count;
}
}
}
public synchronized Rectangle getBounds() {
if (edges.isEmpty()) { if (edges.isEmpty()) {
return new java.awt.Rectangle(0, 0, 0, 0); return new Rectangle(0, 0, 0, 0);
} }
double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE; double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE; double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
@@ -88,7 +107,7 @@ public class FillArea {
int y = (int) Math.floor(minY); int y = (int) Math.floor(minY);
int w = (int) Math.ceil(maxX) - x + 1; int w = (int) Math.ceil(maxX) - x + 1;
int h = (int) Math.ceil(maxY) - y + 1; int h = (int) Math.ceil(maxY) - y + 1;
return new java.awt.Rectangle(x, y, Math.max(0, w), Math.max(0, h)); return new Rectangle(x, y, Math.max(0, w), Math.max(0, h));
} }
public synchronized void setFillModeOR() { public synchronized void setFillModeOR() {
@@ -123,18 +142,20 @@ public class FillArea {
this.pixelPattern = pat; this.pixelPattern = pat;
} }
public synchronized java.awt.Image getImage() { public synchronized PixelBuffer getPixelBuffer() {
java.awt.Rectangle b = getBounds(); Rectangle b = getBounds();
if (b.width <= 0 || b.height <= 0) { if (b.width <= 0 || b.height <= 0) {
return new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_ARGB); return new DefaultPixelBuffer(1, 1);
} }
GraphicsPlane tempPlane = new GraphicsPlane(b.x + b.width, b.y + b.height); GraphicsPlane tempPlane = new GraphicsPlane(b.x + b.width, b.y + b.height);
fill(tempPlane, fillColor, 0, solidFill ? GocaConstants.PT_SOLID : 0, false, 0, 0, 1, 0, 0, null); fill(tempPlane, fillColor, 0, solidFill ? GocaConstants.PT_SOLID : 0, false, 0, 0, 1, 0, 0, null);
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(b.width, b.height, java.awt.image.BufferedImage.TYPE_INT_ARGB); DefaultPixelBuffer cropped = new DefaultPixelBuffer(b.width, b.height);
java.awt.Graphics g = img.getGraphics(); cropped.blit(tempPlane.getRgbBuffer(), b.x, b.y, b.width, b.height, 0, 0);
g.drawImage(tempPlane.getImage(), -b.x, -b.y, null); return cropped;
g.dispose(); }
return img;
public synchronized Object getImage() {
return getPixelBuffer();
} }
public synchronized void dispose() { public synchronized void dispose() {
@@ -1,6 +1,5 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Point;
/** /**
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space. * Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
@@ -1,19 +1,5 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -23,9 +9,9 @@ import java.util.logging.Logger;
/** /**
* Offscreen rendering surface for GOCA vector graphics. * Offscreen rendering surface for GOCA vector graphics.
* Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix. * Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
* Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap). * Pure Java software rasterizer compatible with standard Java SE, Android, and headless environments.
*/ */
public class GraphicsPlane { public class GraphicsPlane implements PixelBuffer {
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName()); private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
@@ -111,28 +97,118 @@ public class GraphicsPlane {
drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth); drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth);
} }
private BufferedImage canvasImage; @Override
public synchronized void drawLine(int x1, int y1, int x2, int y2, int argb) {
drawLine((double) x1, (double) y1, (double) x2, (double) y2, argb, currentLineType, currentLineWidth);
}
public synchronized BufferedImage toBufferedImage() { @Override
if (canvasImage == null && rgbBuffer != null && canvasWidth > 0 && canvasHeight > 0) { public synchronized int getWidth() {
DataBufferInt db = new DataBufferInt(rgbBuffer, rgbBuffer.length); return canvasWidth;
DirectColorModel cm = new DirectColorModel(32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); }
WritableRaster raster = Raster.createWritableRaster(
new SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, canvasWidth, canvasHeight, @Override
new int[]{0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000}), public synchronized int getHeight() {
db, null return canvasHeight;
); }
canvasImage = new BufferedImage(cm, raster, false, null);
@Override
public synchronized int[] getPixels() {
return rgbBuffer;
}
@Override
public synchronized void fillRect(int x, int y, int width, int height, int argb) {
if (width <= 0 || height <= 0) return;
int x1 = Math.max(0, x);
int y1 = Math.max(0, y);
int x2 = Math.min(canvasWidth, x + width);
int y2 = Math.min(canvasHeight, y + height);
for (int cy = y1; cy < y2; cy++) {
for (int cx = x1; cx < x2; cx++) {
setPixel(cx, cy, argb);
}
} }
return canvasImage; hasContent = true;
updateCount++;
} }
public synchronized Image getImage() { @Override
return toBufferedImage(); public synchronized void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY) {
if (srcPixels == null || srcW <= 0 || srcH <= 0) return;
for (int r = 0; r < srcH; r++) {
int sy = srcY + r;
int dy = dstY + r;
if (dy < 0 || dy >= canvasHeight) continue;
for (int c = 0; c < srcW; c++) {
int sx = srcX + c;
int dx = dstX + c;
if (dx < 0 || dx >= canvasWidth) continue;
if (isClipped(dx, dy)) continue;
int idx = sy * srcW + sx;
if (idx < srcPixels.length) {
setPixel(dx, dy, srcPixels[idx]);
}
}
}
hasContent = true;
updateCount++;
} }
public synchronized Graphics getGraphics() { public synchronized void copyPixels(int[] srcPixels, int srcOffset, int srcScan, int dstX, int dstY, int width, int height) {
return toBufferedImage().getGraphics(); if (srcPixels == null || width <= 0 || height <= 0) return;
for (int r = 0; r < height; r++) {
int cy = dstY + r;
if (cy < 0 || cy >= canvasHeight) continue;
int srcRowStart = srcOffset + r * srcScan;
for (int c = 0; c < width; c++) {
int cx = dstX + c;
if (cx < 0 || cx >= canvasWidth) continue;
int p = srcPixels[srcRowStart + c];
setPixel(cx, cy, p);
}
}
hasContent = true;
updateCount++;
}
@Override
public synchronized void setClip(int x, int y, int width, int height) {
if (width < 0 || height < 0) {
clearClip();
} else {
this.clipPixelXMin = x;
this.clipPixelYMin = y;
this.clipPixelXMax = x + width;
this.clipPixelYMax = y + height;
this.viewingWindowActive = true;
}
}
@Override
public synchronized void clearClip() {
this.clipPixelXMin = 0;
this.clipPixelYMin = 0;
this.clipPixelXMax = canvasWidth;
this.clipPixelYMax = canvasHeight;
this.viewingWindowActive = false;
}
@Override
public boolean isClipped(int x, int y) {
if (!viewingWindowActive) {
return x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight;
}
return x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax;
}
public synchronized Rectangle getClip() {
if (!viewingWindowActive) {
return new Rectangle(0, 0, canvasWidth, canvasHeight);
}
return new Rectangle(clipPixelXMin, clipPixelYMin, clipPixelXMax - clipPixelXMin, clipPixelYMax - clipPixelYMin);
} }
public void setProgramSymbolManager(ProgramSymbolManager psm) { public void setProgramSymbolManager(ProgramSymbolManager psm) {
@@ -230,16 +306,21 @@ public class GraphicsPlane {
this.canvasWidth = w; this.canvasWidth = w;
this.canvasHeight = h; this.canvasHeight = h;
this.rgbBuffer = newBuffer; this.rgbBuffer = newBuffer;
this.canvasImage = null;
updateViewingWindowPixels(); updateViewingWindowPixels();
} }
@Override
public synchronized void clear() { public synchronized void clear() {
clear(0);
}
@Override
public synchronized void clear(int argb) {
if (rgbBuffer != null) { if (rgbBuffer != null) {
Arrays.fill(rgbBuffer, 0); Arrays.fill(rgbBuffer, argb);
} }
this.currentMixMode = 0; this.currentMixMode = 0;
hasContent = false; hasContent = (argb != 0);
updateCount++; updateCount++;
} }
@@ -536,6 +617,7 @@ public class GraphicsPlane {
/** /**
* Standard integer Bresenham line algorithm matching IBM 3179G / Host On-Demand 1-pixel rasterization. * Standard integer Bresenham line algorithm matching IBM 3179G / Host On-Demand 1-pixel rasterization.
*/ */
@Override
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) { public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
int dx = Math.abs(x1 - x0); int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0); int dy = Math.abs(y1 - y0);
@@ -598,6 +680,7 @@ public class GraphicsPlane {
/** /**
* Draws a line matching IBM 3179G / Host On-Demand rasterization. * Draws a line matching IBM 3179G / Host On-Demand rasterization.
*/ */
@Override
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) { public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : (GocaConstants.GOCA_COLORS[0] & 0x00FFFFFF); int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : (GocaConstants.GOCA_COLORS[0] & 0x00FFFFFF);
int ix0 = (int) Math.round(x0); int ix0 = (int) Math.round(x0);
@@ -628,6 +711,111 @@ public class GraphicsPlane {
drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth); drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth);
} }
@Override
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int argb, double strokeWidth) {
drawLineAA(x0, y0, x1, y1, argb);
}
/**
* Draws an anti-aliased line using Xiaolin Wu's algorithm directly into the pixel buffer.
* Pure Java implementation replacing AWT Graphics2D rendering for sub-pixel vector strokes.
*/
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int color) {
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
if (steep) {
double t = x0; x0 = y0; y0 = t;
t = x1; x1 = y1; y1 = t;
}
if (x0 > x1) {
double t = x0; x0 = x1; x1 = t;
t = y0; y0 = y1; y1 = t;
}
double dx = x1 - x0;
double dy = y1 - y0;
double gradient = (dx == 0.0) ? 1.0 : dy / dx;
// Handle first endpoint
double xend = Math.round(x0);
double yend = y0 + gradient * (xend - x0);
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
int xpxl1 = (int) xend;
int ypxl1 = (int) Math.floor(yend);
if (steep) {
plotAA(ypxl1, xpxl1, (1.0 - (yend - Math.floor(yend))) * xgap, color);
plotAA(ypxl1 + 1, xpxl1, (yend - Math.floor(yend)) * xgap, color);
} else {
plotAA(xpxl1, ypxl1, (1.0 - (yend - Math.floor(yend))) * xgap, color);
plotAA(xpxl1, ypxl1 + 1, (yend - Math.floor(yend)) * xgap, color);
}
double intery = yend + gradient;
// Handle second endpoint
xend = Math.round(x1);
yend = y1 + gradient * (xend - x1);
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
int xpxl2 = (int) xend;
int ypxl2 = (int) Math.floor(yend);
if (steep) {
plotAA(ypxl2, xpxl2, (1.0 - (yend - Math.floor(yend))) * xgap, color);
plotAA(ypxl2 + 1, xpxl2, (yend - Math.floor(yend)) * xgap, color);
} else {
plotAA(xpxl2, ypxl2, (1.0 - (yend - Math.floor(yend))) * xgap, color);
plotAA(xpxl2, ypxl2 + 1, (yend - Math.floor(yend)) * xgap, color);
}
// Main loop
if (steep) {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
int iy = (int) Math.floor(intery);
double fpart = intery - iy;
plotAA(iy, x, 1.0 - fpart, color);
plotAA(iy + 1, x, fpart, color);
intery += gradient;
}
} else {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
int iy = (int) Math.floor(intery);
double fpart = intery - iy;
plotAA(x, iy, 1.0 - fpart, color);
plotAA(x, iy + 1, fpart, color);
intery += gradient;
}
}
hasContent = true;
updateCount++;
}
private void plotAA(int x, int y, double brightness, int color) {
if (x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight || brightness <= 0.0) return;
if (brightness > 1.0) brightness = 1.0;
int sa = (color >>> 24) & 0xFF;
if (sa == 0) sa = 0xFF;
int alpha = (int) Math.round(sa * brightness);
if (alpha <= 0) return;
int sr = (color >>> 16) & 0xFF;
int sg = (color >>> 8) & 0xFF;
int sb = color & 0xFF;
int idx = y * canvasWidth + x;
int dst = rgbBuffer[idx];
int da = (dst >>> 24) & 0xFF;
if (da == 0) {
rgbBuffer[idx] = (alpha << 24) | (sr << 16) | (sg << 8) | sb;
} else {
int dr = (dst >>> 16) & 0xFF;
int dg = (dst >>> 8) & 0xFF;
int db = dst & 0xFF;
int invA = 255 - alpha;
int outR = (sr * alpha + dr * invA) / 255;
int outG = (sg * alpha + dg * invA) / 255;
int outB = (sb * alpha + db * invA) / 255;
int outA = Math.min(255, da + alpha);
rgbBuffer[idx] = (outA << 24) | (outR << 16) | (outG << 8) | outB;
}
}
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) { private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1; int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
@@ -1231,56 +1419,46 @@ public class GraphicsPlane {
double tanShear = Math.tan(Math.toRadians(shearAngle)); double tanShear = Math.tan(Math.toRadians(shearAngle));
if (ch < 6.0) { if (ch < 6.0) {
BufferedImage img = toBufferedImage(); int ptr = offset;
if (img != null) { while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
Graphics2D g = img.createGraphics(); int order = VectorSymbolData.vss_data[ptr] & 0xFF;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); if (order == 0xC1) {
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
float strokeW = (float) Math.max(0.5, Math.min(0.75, ch / 5.0)); int numPoints = byteLen / 4;
g.setStroke(new BasicStroke(strokeW, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); int dataPtr = ptr + 2;
g.setColor(new Color(color, true));
int ptr = offset; if (numPoints >= 2) {
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) { double[] px = new double[numPoints];
int order = VectorSymbolData.vss_data[ptr] & 0xFF; double[] py = new double[numPoints];
if (order == 0xC1) { for (int p = 0; p < numPoints; p++) {
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF; int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int numPoints = byteLen / 4; int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
int dataPtr = ptr + 2;
if (numPoints >= 2) { double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
Path2D.Double path = new Path2D.Double(); double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw; double sx = nx - ny * tanShear;
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch; double sy = ny;
double sx = nx - ny * tanShear; double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
double sy = ny; double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx; px[p] = x + rx;
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy; py[p] = y + ry;
}
double px = x + rx; for (int p = 0; p < numPoints - 1; p++) {
double py = y + ry; drawLineAA(px[p], py[p], px[p + 1], py[p + 1], color);
if (p == 0) path.moveTo(px, py);
else path.lineTo(px, py);
}
g.draw(path);
} }
ptr += 2 + byteLen;
} else {
ptr++;
} }
ptr += 2 + byteLen;
} else {
ptr++;
} }
g.dispose();
hasContent = true;
updateCount++;
return;
} }
hasContent = true;
updateCount++;
return;
} }
int ptr = offset; int ptr = offset;
@@ -1,30 +1,24 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.FilteredImageSource;
import java.awt.image.MemoryImageSource;
/** /**
* Bitmap image container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBitImage). * Bitmap image container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBitImage).
* Pure Java software implementation completely decoupled from java.awt.
*/ */
public class HODBitImage { public class HODBitImage {
protected Component vComponent; protected Object vComponent;
protected Dimension iSize = new Dimension(); protected Dimension iSize = new Dimension();
protected Dimension iScaledSize = new Dimension(); protected Dimension iScaledSize = new Dimension();
protected int iDepth; protected int iDepth;
protected int iScanLength; protected int iScanLength;
protected boolean _iUseGraphicColors; protected boolean _iUseGraphicColors;
protected byte[] iScaledImageData; protected byte[] iScaledImageData;
protected Image[] hImage; protected PixelBuffer[] hImage;
protected Image[] iScaledImage; protected PixelBuffer[] iScaledImage;
protected int transparentBG = 0; protected int transparentBG = 0;
protected byte[] hImageData; protected byte[] hImageData;
protected int iBaseColor; protected int iBaseColor;
public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) { public HODBitImage(Object comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
this.vComponent = comp; this.vComponent = comp;
this.iSize.width = width; this.iSize.width = width;
this.iSize.height = height; this.iSize.height = height;
@@ -32,8 +26,8 @@ public class HODBitImage {
this.iBaseColor = baseColor; this.iBaseColor = baseColor;
this.iDepth = depth; this.iDepth = depth;
this._iUseGraphicColors = useGraphicColors; this._iUseGraphicColors = useGraphicColors;
this.hImage = new Image[depth == 1 ? 17 : 1]; this.hImage = new PixelBuffer[depth == 1 ? 17 : 1];
this.iScaledImage = new Image[depth == 1 ? 17 : 1]; this.iScaledImage = new PixelBuffer[depth == 1 ? 17 : 1];
this.buildHODImage(); this.buildHODImage();
} }
@@ -72,11 +66,19 @@ public class HODBitImage {
return nArray; return nArray;
} }
public Image getHODImage(int colorIdx) { public Object getHODImage(int colorIdx) {
return this.getHODImage(this.iSize.width, this.iSize.height, colorIdx); return this.getHODPixelBuffer(this.iSize.width, this.iSize.height, colorIdx);
} }
public Image getHODImage(int w, int h, int colorIdx) { public Object getHODImage(int w, int h, int colorIdx) {
return this.getHODPixelBuffer(w, h, colorIdx);
}
public PixelBuffer getHODPixelBuffer(int colorIdx) {
return this.getHODPixelBuffer(this.iSize.width, this.iSize.height, colorIdx);
}
public PixelBuffer getHODPixelBuffer(int w, int h, int colorIdx) {
if (w <= 0 || h <= 0) return null; if (w <= 0 || h <= 0) return null;
boolean diffColor = this.iBaseColor != colorIdx && this.iDepth == 1; boolean diffColor = this.iBaseColor != colorIdx && this.iDepth == 1;
boolean matchesBase = this.iSize.width == w && this.iSize.height == h; boolean matchesBase = this.iSize.width == w && this.iSize.height == h;
@@ -88,13 +90,18 @@ public class HODBitImage {
this.iScaledSize.height = h; this.iScaledSize.height = h;
this.scaleHODImage(); this.scaleHODImage();
} }
Image image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx]; PixelBuffer image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx];
if (image == null) { if (image == null) {
HODColorChangeFilter filter = new HODColorChangeFilter(this.getHODColor(colorIdx)); HODColorChangeFilter filter = new HODColorChangeFilter(this.getHODColor(colorIdx));
Image base = matchesBase ? this.hImage[this.iBaseColor] : this.iScaledImage[this.iBaseColor]; PixelBuffer base = matchesBase ? this.hImage[this.iBaseColor] : this.iScaledImage[this.iBaseColor];
if (base != null) { if (base != null) {
FilteredImageSource source = new FilteredImageSource(base.getSource(), filter); int bw = base.getWidth();
image = Toolkit.getDefaultToolkit().createImage(source); int bh = base.getHeight();
int[] basePixels = base.getPixels();
int[] filtered = new int[bw * bh];
System.arraycopy(basePixels, 0, filtered, 0, filtered.length);
filter.apply(filtered, 0, filtered.length);
image = new DefaultPixelBuffer(bw, bh, filtered);
if (matchesBase) { if (matchesBase) {
this.hImage[colorIdx] = image; this.hImage[colorIdx] = image;
} else { } else {
@@ -121,27 +128,47 @@ public class HODBitImage {
private void buildHODImage() { private void buildHODImage() {
if (this.iSize.width <= 0 || this.iSize.height <= 0) return; if (this.iSize.width <= 0 || this.iSize.height <= 0) return;
int[] pixels = getHODImageData(this.iBaseColor); int[] pixels = getHODImageData(this.iBaseColor);
MemoryImageSource mis = new MemoryImageSource(this.iSize.width, this.iSize.height, pixels, 0, this.iSize.width); PixelBuffer buf = new DefaultPixelBuffer(this.iSize.width, this.iSize.height, pixels);
Image img = Toolkit.getDefaultToolkit().createImage(mis);
if (this.iDepth == 1) { if (this.iDepth == 1) {
this.hImage[this.iBaseColor] = img; this.hImage[this.iBaseColor] = buf;
} else { } else {
this.hImage[0] = img; this.hImage[0] = buf;
} }
} }
private void scaleHODImage() { private void scaleHODImage() {
if (this.iScaledSize.width <= 0 || this.iScaledSize.height <= 0) return; int sw = this.iScaledSize.width;
Image base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0]; int sh = this.iScaledSize.height;
if (sw <= 0 || sh <= 0) return;
PixelBuffer base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0];
if (base != null) { if (base != null) {
this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] = int bw = base.getWidth();
base.getScaledInstance(this.iScaledSize.width, this.iScaledSize.height, Image.SCALE_FAST); int bh = base.getHeight();
int[] src = base.getPixels();
int[] dst = new int[sw * sh];
for (int dy = 0; dy < sh; dy++) {
int sy = dy * bh / sh;
int srcOffset = sy * bw;
int dstOffset = dy * sw;
for (int dx = 0; dx < sw; dx++) {
int sx = dx * bw / sw;
dst[dstOffset + dx] = src[srcOffset + sx];
}
}
this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] = new DefaultPixelBuffer(sw, sh, dst);
} }
} }
private int getHODColor(int idx) { private int getHODColor(int idx) {
if (idx == 0 && this.vComponent != null) { if (idx == 0 && this.vComponent != null) {
return this.vComponent.getBackground().getRGB(); try {
java.lang.reflect.Method m = this.vComponent.getClass().getMethod("getBackground");
Object bg = m.invoke(this.vComponent);
if (bg != null) {
java.lang.reflect.Method mRgb = bg.getClass().getMethod("getRGB");
return ((Number) mRgb.invoke(bg)).intValue();
}
} catch (Exception ignored) {}
} }
return GocaConstants.getGocaColorArgb(idx); return GocaConstants.getGocaColorArgb(idx);
} }
@@ -1,7 +1,5 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Point;
import java.awt.Rectangle;
/** /**
* Bounding box encapsulation matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBounds). * Bounding box encapsulation matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBounds).
@@ -1,19 +1,17 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Color;
import java.awt.image.RGBImageFilter;
/** /**
* Image filter that replaces occurrences of one color with another color. * Image filter that replaces occurrences of one color with another color.
* Conforms to IBM Host On-Demand HODColorChangeFilter. * Conforms to IBM Host On-Demand HODColorChangeFilter.
* Platform-neutral implementation independent of java.awt.
*/ */
public class HODColorChangeFilter extends RGBImageFilter { public class HODColorChangeFilter {
protected boolean canFilterIndexColorModel = true;
private int oldRgb; private int oldRgb;
private int newRgb; private int newRgb;
public HODColorChangeFilter(int newRgb) { public HODColorChangeFilter(int newRgb) {
this.canFilterIndexColorModel = true;
this.oldRgb = -1; this.oldRgb = -1;
this.newRgb = newRgb | 0xFF000000; this.newRgb = newRgb | 0xFF000000;
} }
@@ -23,7 +21,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
} }
public HODColorChangeFilter(int oldRgb, int newRgb) { public HODColorChangeFilter(int oldRgb, int newRgb) {
this.canFilterIndexColorModel = true;
this.oldRgb = oldRgb & 0x00FFFFFF; this.oldRgb = oldRgb & 0x00FFFFFF;
this.newRgb = newRgb; this.newRgb = newRgb;
} }
@@ -48,7 +45,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
this.newRgb = newRgb; this.newRgb = newRgb;
} }
@Override
public int filterRGB(int x, int y, int rgb) { public int filterRGB(int x, int y, int rgb) {
if (oldRgb == -1) { if (oldRgb == -1) {
if ((rgb & 0xFF000000) != 0) { if ((rgb & 0xFF000000) != 0) {
@@ -61,4 +57,20 @@ public class HODColorChangeFilter extends RGBImageFilter {
} }
return rgb; return rgb;
} }
public void apply(int[] pixels, int offset, int length) {
if (pixels == null) return;
int end = Math.min(pixels.length, offset + length);
for (int i = offset; i < end; i++) {
pixels[i] = filterRGB(0, 0, pixels[i]);
}
}
public void apply(PixelBuffer buffer) {
if (buffer == null) return;
int[] pixels = buffer.getPixels();
if (pixels != null) {
apply(pixels, 0, pixels.length);
}
}
} }
@@ -1,16 +1,8 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
/** /**
* Headless graphics plane facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODGraphicsPlane). * Headless graphics plane facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODGraphicsPlane).
* Pure Java software implementation completely decoupled from java.awt.
*/ */
public class HODGraphicsPlane { public class HODGraphicsPlane {
private final GraphicsPlane delegate; private final GraphicsPlane delegate;
@@ -40,6 +32,10 @@ public class HODGraphicsPlane {
return delegate; return delegate;
} }
public PixelBuffer getPixelBuffer() {
return delegate;
}
public void resize(Dimension dim, boolean keepContent) { public void resize(Dimension dim, boolean keepContent) {
if (dim != null) { if (dim != null) {
delegate.setDimensions(dim.width, dim.height); delegate.setDimensions(dim.width, dim.height);
@@ -52,15 +48,19 @@ public class HODGraphicsPlane {
this.bounds.set(0, 0, delegate.getCanvasWidth(), delegate.getCanvasHeight()); this.bounds.set(0, 0, delegate.getCanvasWidth(), delegate.getCanvasHeight());
} }
public Graphics getHODGraphics() { public Object getHODGraphics() {
return delegate.getGraphics(); return delegate;
} }
public Image getHODImage() { public Object getHODImage() {
return delegate.getImage(); return delegate;
} }
public void setHODTemporaryGraphics(Graphics g) { public PixelBuffer getHODPixelBuffer() {
return delegate;
}
public void setHODTemporaryGraphics(Object g) {
// No-op or temporary override // No-op or temporary override
} }
@@ -116,35 +116,32 @@ public class HODGraphicsPlane {
} }
public void drawHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) { public void drawHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
Graphics g = delegate.getGraphics(); double rx = width / 2.0;
if (g != null) { double ry = height / 2.0;
g.setColor(currentColor); double cx = x + rx;
g.drawArc(x, y, width, height, startAngle, arcAngle); double cy = y + ry;
updateHODBounds(x, y); delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, false);
updateHODBounds(x + width, y + height); updateHODBounds(x, y);
} updateHODBounds(x + width, y + height);
} }
public void fillHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) { public void fillHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
Graphics g = delegate.getGraphics(); double rx = width / 2.0;
if (g != null) { double ry = height / 2.0;
g.setColor(currentColor); double cx = x + rx;
g.fillArc(x, y, width, height, startAngle, arcAngle); double cy = y + ry;
updateHODBounds(x, y); delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, true);
updateHODBounds(x + width, y + height); updateHODBounds(x, y);
} updateHODBounds(x + width, y + height);
} }
public void drawHODImage(HODBitImage bitImage, int x, int y, int w, int h) { public void drawHODImage(HODBitImage bitImage, int x, int y, int w, int h) {
if (bitImage != null) { if (bitImage != null) {
Image img = bitImage.getHODImage(w, h, currentColorIndex); PixelBuffer buf = bitImage.getHODPixelBuffer(w, h, currentColorIndex);
if (img != null) { if (buf != null) {
Graphics g = delegate.getGraphics(); delegate.blit(buf.getPixels(), 0, 0, buf.getWidth(), buf.getHeight(), x, y);
if (g != null) { updateHODBounds(x, y);
g.drawImage(img, x, y, null); updateHODBounds(x + w, y + h);
updateHODBounds(x, y);
updateHODBounds(x + w, y + h);
}
} }
} }
} }
@@ -1,20 +1,15 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Method;
/** /**
* Visual part container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODPart). * Visual part container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODPart).
* Completely decoupled from java.awt.
*/ */
public class HODPart extends Rectangle implements Serializable { public class HODPart extends Rectangle implements Serializable {
protected Component hodParent; protected Object hodParent;
protected Font _hodFont; protected Object _hodFont;
protected Color foregroundColor; protected Color foregroundColor;
protected Color backgroundColor; protected Color backgroundColor;
protected Boolean isTransparent; protected Boolean isTransparent;
@@ -22,33 +17,36 @@ public class HODPart extends Rectangle implements Serializable {
protected HODPart() {} protected HODPart() {}
public HODPart(Component component) { public HODPart(Object component) {
this(); this();
this.setHODParent(component); this.setHODParent(component);
} }
public HODPart(Component component, Dimension dimension) { public HODPart(Object component, Dimension dimension) {
super(dimension); super(0, 0, dimension != null ? dimension.width : 0, dimension != null ? dimension.height : 0);
this.setHODParent(component); this.setHODParent(component);
} }
public HODPart(Component component, Rectangle rectangle) { public HODPart(Object component, Rectangle rectangle) {
super(rectangle); super(rectangle != null ? rectangle.x : 0, rectangle != null ? rectangle.y : 0,
rectangle != null ? rectangle.width : 0, rectangle != null ? rectangle.height : 0);
this.setHODParent(component); this.setHODParent(component);
} }
public HODPart(HODPart hODPart) { public HODPart(HODPart hODPart) {
this(hODPart.getHODParent(), hODPart.getSize()); this(hODPart != null ? hODPart.getHODParent() : null, hODPart != null ? hODPart.getSize() : null);
this.setHODBackground(hODPart.getHODBackground()); if (hODPart != null) {
this.setHODForeground(hODPart.getHODForeground()); this.setHODBackground(hODPart.getHODBackground());
this.setHODFont(hODPart.getHODFont()); this.setHODForeground(hODPart.getHODForeground());
this.setHODFont(hODPart.getHODFont());
}
} }
public Component getHODParent() { public Object getHODParent() {
return this.hodParent; return this.hodParent;
} }
public void setHODParent(Component component) { public void setHODParent(Object component) {
if (component != null && !component.equals(this.hodParent)) { if (component != null && !component.equals(this.hodParent)) {
this.hodParent = component; this.hodParent = component;
} }
@@ -56,18 +54,28 @@ public class HODPart extends Rectangle implements Serializable {
public void repaint() { public void repaint() {
if (this.hodParent != null) { if (this.hodParent != null) {
this.hodParent.repaint(this.x, this.y, this.width, this.height); try {
Method m = this.hodParent.getClass().getMethod("repaint", int.class, int.class, int.class, int.class);
m.invoke(this.hodParent, this.x, this.y, this.width, this.height);
} catch (Throwable ignored) {}
} }
} }
public void paint(Component component, Graphics graphics, int x, int y, int w, int h) { public void paint(Object component, Object graphics, int x, int y, int w, int h) {
this.setBounds(x, y, w, h); this.setBounds(x, y, w, h);
if (Boolean.TRUE.equals(this._visible)) { if (Boolean.TRUE.equals(this._visible)) {
this.paintHODView(graphics); this.paintHODView(graphics);
} }
} }
protected void paintHODView(Graphics graphics) {} public void paint(PixelBuffer buffer, int x, int y, int w, int h) {
this.setBounds(x, y, w, h);
if (Boolean.TRUE.equals(this._visible)) {
this.paintHODView(buffer);
}
}
protected void paintHODView(Object graphics) {}
public Color getHODBackground() { public Color getHODBackground() {
return this.backgroundColor; return this.backgroundColor;
@@ -85,11 +93,11 @@ public class HODPart extends Rectangle implements Serializable {
this.foregroundColor = color; this.foregroundColor = color;
} }
public Font getHODFont() { public Object getHODFont() {
return this._hodFont; return this._hodFont;
} }
public void setHODFont(Font font) { public void setHODFont(Object font) {
this._hodFont = font; this._hodFont = font;
} }
@@ -1,12 +1,10 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Dimension; import java.lang.reflect.Method;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.image.BufferedImage;
/** /**
* Programmed Symbol Set manager facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODProgramSymbolManager). * Programmed Symbol Set manager facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODProgramSymbolManager).
* Completely decoupled from java.awt.
*/ */
public class HODProgramSymbolManager { public class HODProgramSymbolManager {
public static final int MAX_HOD_SLOT = 254; public static final int MAX_HOD_SLOT = 254;
@@ -51,14 +49,57 @@ public class HODProgramSymbolManager {
delegate.loadProgrammedSymbolSet(bytes, 0, bytes.length); delegate.loadProgrammedSymbolSet(bytes, 0, bytes.length);
} }
public void drawHODImageCharacter(Graphics g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) { public void drawHODImageCharacter(PixelBuffer pb, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
if (g == null || pt == null) return; if (pb == null || pt == null) return;
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint); ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
if (slot != null) { if (slot != null) {
int fg = GocaConstants.getGocaColorArgb(colorIdx); int fg = GocaConstants.getGocaColorArgb(colorIdx);
BufferedImage img = slot.getScaledImage(cellW, cellH, fg, 0); PixelBuffer glyph = slot.getScaledPixelBuffer(cellW, cellH, fg, 0);
if (img != null) { if (glyph != null) {
g.drawImage(img, pt.x, pt.y, null); int sw = glyph.getWidth();
int sh = glyph.getHeight();
int[] srcPx = glyph.getPixels();
int[] dstPx = pb.getPixels();
int pw = pb.getWidth();
int ph = pb.getHeight();
for (int r = 0; r < sh; r++) {
int dy = pt.y + r;
if (dy < 0 || dy >= ph) continue;
for (int c = 0; c < sw; c++) {
int dx = pt.x + c;
if (dx < 0 || dx >= pw) continue;
int p = srcPx[r * sw + c];
if ((p & 0xFF000000) != 0) {
dstPx[dy * pw + dx] = p;
}
}
}
}
}
}
public void drawHODImageCharacter(Object g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
if (g == null || pt == null) return;
if (g instanceof PixelBuffer) {
drawHODImageCharacter((PixelBuffer) g, lcid, codepoint, pt, colorIdx, cellW, cellH);
return;
}
// Fallback for AWT Graphics if passed reflectively
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
if (slot != null) {
int fg = GocaConstants.getGocaColorArgb(colorIdx);
PixelBuffer glyph = slot.getScaledPixelBuffer(cellW, cellH, fg, 0);
if (glyph != null) {
try {
Class<?> biClass = Class.forName("java.awt.image.BufferedImage");
Object bi = biClass.getConstructor(int.class, int.class, int.class)
.newInstance(glyph.getWidth(), glyph.getHeight(), 2); // TYPE_INT_ARGB
Method setRGB = biClass.getMethod("setRGB", int.class, int.class, int.class, int.class, int[].class, int.class, int.class);
setRGB.invoke(bi, 0, 0, glyph.getWidth(), glyph.getHeight(), glyph.getPixels(), 0, glyph.getWidth());
Method drawImg = g.getClass().getMethod("drawImage", Class.forName("java.awt.Image"), int.class, int.class, Class.forName("java.awt.image.ImageObserver"));
drawImg.invoke(g, bi, pt.x, pt.y, null);
} catch (Throwable ignored) {}
} }
} }
} }
@@ -1,7 +1,5 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Point;
import java.awt.Rectangle;
/** /**
* Coordinate transform adapter matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODTransform). * Coordinate transform adapter matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODTransform).
@@ -1,18 +1,16 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Color;
import java.awt.image.RGBImageFilter;
/** /**
* Image filter that keys out a specific background color by setting its alpha to 0x00. * Image filter that keys out a specific background color by setting its alpha to 0x00.
* Conforms to IBM Host On-Demand HODTransparentColorFilter. * Conforms to IBM Host On-Demand HODTransparentColorFilter.
* Platform-neutral implementation independent of java.awt.
*/ */
public class HODTransparentColorFilter extends RGBImageFilter { public class HODTransparentColorFilter {
protected boolean canFilterIndexColorModel = true;
private int transparentRgb; private int transparentRgb;
public HODTransparentColorFilter(int rgb) { public HODTransparentColorFilter(int rgb) {
this.canFilterIndexColorModel = true;
this.transparentRgb = rgb & 0x00FFFFFF; this.transparentRgb = rgb & 0x00FFFFFF;
} }
@@ -28,11 +26,26 @@ public class HODTransparentColorFilter extends RGBImageFilter {
this.transparentRgb = rgb & 0x00FFFFFF; this.transparentRgb = rgb & 0x00FFFFFF;
} }
@Override
public int filterRGB(int x, int y, int rgb) { public int filterRGB(int x, int y, int rgb) {
if ((rgb & 0x00FFFFFF) == transparentRgb) { if ((rgb & 0x00FFFFFF) == transparentRgb) {
return 0x00000000; return 0x00000000;
} }
return rgb; return rgb;
} }
public void apply(int[] pixels, int offset, int length) {
if (pixels == null) return;
int end = Math.min(pixels.length, offset + length);
for (int i = offset; i < end; i++) {
pixels[i] = filterRGB(0, 0, pixels[i]);
}
}
public void apply(PixelBuffer buffer) {
if (buffer == null) return;
int[] pixels = buffer.getPixels();
if (pixels != null) {
apply(pixels, 0, pixels.length);
}
}
} }
@@ -1,15 +1,10 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.Color; import java.lang.reflect.Method;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Insets;
import java.awt.image.BufferedImage;
/** /**
* Wallpaper background manager matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODWallpaper). * Wallpaper background manager matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODWallpaper).
* Completely decoupled from java.awt.
*/ */
public class HODWallpaper extends HODPart { public class HODWallpaper extends HODPart {
public static final int HOD_TILE = 0; public static final int HOD_TILE = 0;
@@ -17,8 +12,8 @@ public class HODWallpaper extends HODPart {
public static final int HOD_STRETCH = 2; public static final int HOD_STRETCH = 2;
private int _display = HOD_CENTER; private int _display = HOD_CENTER;
private Image rawImage; private Object rawImage;
private Image backgroundImage; private Object backgroundImage;
public HODWallpaper() { public HODWallpaper() {
this(HOD_CENTER); this(HOD_CENTER);
@@ -28,7 +23,7 @@ public class HODWallpaper extends HODPart {
this.setDisplay(displayMode); this.setDisplay(displayMode);
} }
public HODWallpaper(Image image, int displayMode) { public HODWallpaper(Object image, int displayMode) {
this(displayMode); this(displayMode);
this.setImage(image); this.setImage(image);
} }
@@ -46,30 +41,40 @@ public class HODWallpaper extends HODPart {
return this._display; return this._display;
} }
public void setImage(Image image) { public void setImage(Object image) {
this.rawImage = image; this.rawImage = image;
this.backgroundImage = null; this.backgroundImage = null;
this.repaint(); this.repaint();
} }
public Image getHODImage() { public Object getHODImage() {
return this.rawImage; return this.rawImage;
} }
@Override @Override
public void paint(Component component, Graphics graphics, int x, int y, int w, int h) { public void paint(Object component, Object graphics, int x, int y, int w, int h) {
this.setBounds(x, y, w, h); this.setBounds(x, y, w, h);
this.setHODParent(component); this.setHODParent(component);
super.paint(component, graphics, x, y, w, h); super.paint(component, graphics, x, y, w, h);
} }
@Override @Override
protected void paintHODView(Graphics graphics) { public void paint(PixelBuffer buffer, int x, int y, int w, int h) {
Image image = this.getHODImage(); this.setBounds(x, y, w, h);
int display = this.getDisplay(); super.paint(buffer, x, y, w, h);
Component component = this.getHODParent(); }
if (image == null || component == null) { @Override
protected void paintHODView(Object graphics) {
Object image = this.getHODImage();
int display = this.getDisplay();
if (image == null || graphics == null) {
return;
}
if (graphics instanceof PixelBuffer && image instanceof PixelBuffer) {
paintHODViewBuffer((PixelBuffer) graphics, (PixelBuffer) image);
return; return;
} }
@@ -82,7 +87,83 @@ public class HODWallpaper extends HODPart {
} }
} }
protected void hodtileImage(Graphics graphics, Image image) { public void paintHODViewBuffer(PixelBuffer dst, PixelBuffer src) {
int display = this.getDisplay();
Insets insets = this.getInsets();
int availX = this.x + insets.left;
int availY = this.y + insets.top;
int availW = this.width - (insets.left + insets.right);
int availH = this.height - (insets.top + insets.bottom);
if (availW <= 0 || availH <= 0 || src.getWidth() <= 0 || src.getHeight() <= 0) return;
if (display == HOD_CENTER) {
int cx = availX + (availW - src.getWidth()) / 2;
int cy = availY + (availH - src.getHeight()) / 2;
blitBuffer(dst, src, cx, cy);
} else if (display == HOD_TILE) {
int cols = (availW / src.getWidth()) + 1;
int rows = (availH / src.getHeight()) + 1;
int curX = availX;
for (int i = 0; i < cols; i++) {
int curY = availY;
for (int j = 0; j < rows; j++) {
blitBuffer(dst, src, curX, curY);
curY += src.getHeight();
}
curX += src.getWidth();
}
} else if (display == HOD_STRETCH) {
scaleBuffer(dst, src, availX, availY, availW, availH);
}
}
private void blitBuffer(PixelBuffer dst, PixelBuffer src, int dstX, int dstY) {
int sw = src.getWidth();
int sh = src.getHeight();
int dw = dst.getWidth();
int dh = dst.getHeight();
int[] srcPx = src.getPixels();
int[] dstPx = dst.getPixels();
for (int r = 0; r < sh; r++) {
int dy = dstY + r;
if (dy < 0 || dy >= dh) continue;
for (int c = 0; c < sw; c++) {
int dx = dstX + c;
if (dx < 0 || dx >= dw) continue;
int p = srcPx[r * sw + c];
if ((p & 0xFF000000) != 0) {
dstPx[dy * dw + dx] = p;
}
}
}
}
private void scaleBuffer(PixelBuffer dst, PixelBuffer src, int dx, int dy, int dw, int dh) {
int sw = src.getWidth();
int sh = src.getHeight();
int targetW = dst.getWidth();
int targetH = dst.getHeight();
int[] srcPx = src.getPixels();
int[] dstPx = dst.getPixels();
for (int r = 0; r < dh; r++) {
int outY = dy + r;
if (outY < 0 || outY >= targetH) continue;
int sy = r * sh / dh;
for (int c = 0; c < dw; c++) {
int outX = dx + c;
if (outX < 0 || outX >= targetW) continue;
int sx = c * sw / dw;
int p = srcPx[sy * sw + sx];
if ((p & 0xFF000000) != 0) {
dstPx[outY * targetW + outX] = p;
}
}
}
}
protected void hodtileImage(Object graphics, Object image) {
Dimension imgSize = getImageSize(image); Dimension imgSize = getImageSize(image);
if (imgSize.width <= 0 || imgSize.height <= 0) return; if (imgSize.width <= 0 || imgSize.height <= 0) return;
@@ -99,40 +180,86 @@ public class HODWallpaper extends HODPart {
for (int i = 0; i < cols; i++) { for (int i = 0; i < cols; i++) {
int curY = startY; int curY = startY;
for (int j = 0; j < rows; j++) { for (int j = 0; j < rows; j++) {
graphics.drawImage(image, curX, curY, this.getHODParent()); invokeDrawImage(graphics, image, curX, curY);
curY += imgSize.height; curY += imgSize.height;
} }
curX += imgSize.width; curX += imgSize.width;
} }
} }
protected void centerHODImage(Graphics graphics, Image image) { protected void centerHODImage(Object graphics, Object image) {
Dimension imgSize = getImageSize(image); Dimension imgSize = getImageSize(image);
if (imgSize.width <= 0 || imgSize.height <= 0) return; if (imgSize.width <= 0 || imgSize.height <= 0) return;
Insets insets = this.getInsets(); Insets insets = this.getInsets();
int cx = this.x + insets.left + (this.width - imgSize.width) / 2; int cx = this.x + insets.left + (this.width - imgSize.width) / 2;
int cy = this.y + insets.top + (this.height - imgSize.height) / 2; int cy = this.y + insets.top + (this.height - imgSize.height) / 2;
graphics.drawImage(image, cx, cy, this.getHODParent()); invokeDrawImage(graphics, image, cx, cy);
} }
protected void stretchHODImage(Graphics graphics, Image image) { protected void stretchHODImage(Object graphics, Object image) {
Insets insets = this.getInsets(); Insets insets = this.getInsets();
int sx = this.x + insets.left; int sx = this.x + insets.left;
int sy = this.y + insets.top; int sy = this.y + insets.top;
int sw = this.width - (insets.left + insets.right); int sw = this.width - (insets.left + insets.right);
int sh = this.height - (insets.top + insets.bottom); int sh = this.height - (insets.top + insets.bottom);
graphics.drawImage(image, sx, sy, sw, sh, this.getHODParent()); invokeDrawImage(graphics, image, sx, sy, sw, sh);
} }
private Dimension getImageSize(Image image) { private void invokeDrawImage(Object graphics, Object img, int x, int y) {
if (graphics == null || img == null) return;
try {
for (Method m : graphics.getClass().getMethods()) {
if (m.getName().equals("drawImage")) {
Class<?>[] pts = m.getParameterTypes();
if (pts.length == 4 && pts[1] == int.class && pts[2] == int.class) {
m.invoke(graphics, img, x, y, this.getHODParent());
return;
}
}
}
} catch (Throwable ignored) {}
}
private void invokeDrawImage(Object graphics, Object img, int x, int y, int w, int h) {
if (graphics == null || img == null) return;
try {
for (Method m : graphics.getClass().getMethods()) {
if (m.getName().equals("drawImage")) {
Class<?>[] pts = m.getParameterTypes();
if (pts.length == 6 && pts[1] == int.class && pts[2] == int.class && pts[3] == int.class && pts[4] == int.class) {
m.invoke(graphics, img, x, y, w, h, this.getHODParent());
return;
}
}
}
} catch (Throwable ignored) {}
}
private Dimension getImageSize(Object image) {
if (image == null) return new Dimension(0, 0); if (image == null) return new Dimension(0, 0);
Component c = this.getHODParent(); if (image instanceof PixelBuffer) {
int w = image.getWidth(c); PixelBuffer pb = (PixelBuffer) image;
int h = image.getHeight(c); return new Dimension(pb.getWidth(), pb.getHeight());
if (w <= 0 && image instanceof BufferedImage) { }
w = ((BufferedImage) image).getWidth(); int w = 0;
h = ((BufferedImage) image).getHeight(); int h = 0;
try {
Method mw = image.getClass().getMethod("getWidth");
w = ((Number) mw.invoke(image)).intValue();
Method mh = image.getClass().getMethod("getHeight");
h = ((Number) mh.invoke(image)).intValue();
} catch (Throwable ignored) {
try {
for (Method m : image.getClass().getMethods()) {
if (m.getName().equals("getWidth") && m.getParameterCount() == 1) {
w = ((Number) m.invoke(image, this.getHODParent())).intValue();
}
if (m.getName().equals("getHeight") && m.getParameterCount() == 1) {
h = ((Number) m.invoke(image, this.getHODParent())).intValue();
}
}
} catch (Throwable ignored2) {}
} }
return new Dimension(Math.max(0, w), Math.max(0, h)); return new Dimension(Math.max(0, w), Math.max(0, h));
} }
@@ -0,0 +1,42 @@
package haus.nightmare.lib3270j.graphics;
import java.io.Serializable;
import java.util.Objects;
/**
* Lightweight pure-Java POJO representing border insets.
* Completely decouples lib3270j from java.awt.Insets.
*/
public class Insets implements Serializable {
private static final long serialVersionUID = 1L;
public int top;
public int left;
public int bottom;
public int right;
public Insets(int top, int left, int bottom, int right) {
this.top = top;
this.left = left;
this.bottom = bottom;
this.right = right;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Insets)) return false;
Insets i = (Insets) obj;
return top == i.top && left == i.left && bottom == i.bottom && right == i.right;
}
@Override
public int hashCode() {
return Objects.hash(top, left, bottom, right);
}
@Override
public String toString() {
return getClass().getName() + "[top=" + top + ",left=" + left + ",bottom=" + bottom + ",right=" + right + "]";
}
}
@@ -0,0 +1,59 @@
package haus.nightmare.lib3270j.graphics;
/**
* Platform-neutral pixel buffer interface for 32-bit ARGB offscreen rasterization.
* Compatible with pure Java SE (Swing), Android (Bitmap), and headless environments.
*/
public interface PixelBuffer {
/** Returns width of the pixel buffer in pixels. */
int getWidth();
/** Returns height of the pixel buffer in pixels. */
int getHeight();
/** Returns contiguous 32-bit ARGB pixel array. */
int[] getPixels();
/** Returns ARGB pixel at specified coordinate, or 0 if out of bounds. */
int getPixel(int x, int y);
/** Sets ARGB pixel at specified coordinate with alpha blending and mix mode. */
void setPixel(int x, int y, int argb);
/** Sets ARGB pixel directly without blending. */
void setPixelDirect(int x, int y, int argb);
/** Clears buffer to fully transparent (0x00000000). */
void clear();
/** Clears buffer to specified ARGB color. */
void clear(int argb);
/** Draws a 1-pixel line using integer Bresenham algorithm. */
void drawLine(int x1, int y1, int x2, int y2, int argb);
/** Draws a 1-pixel line using Bresenham algorithm. */
void drawLineBresenham(int x0, int y0, int x1, int y1, int argb);
/** Draws a stroked line with line type and line width. */
void drawLine(double x0, double y0, double x1, double y1, int argb, int lineType, int lineWidth);
/** Draws an anti-aliased sub-pixel line segment with stroke width. */
void drawLineAA(double x0, double y0, double x1, double y1, int argb, double strokeWidth);
/** Fills a rectangular region with specified ARGB color. */
void fillRect(int x, int y, int width, int height, int argb);
/** Sets clipping rectangle. */
void setClip(int x, int y, int width, int height);
/** Clears clipping rectangle. */
void clearClip();
/** Checks if coordinate falls outside current clipping bounds. */
boolean isClipped(int x, int y);
/** Copies a rectangular block of pixels from source array into this buffer. */
void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY);
}
@@ -0,0 +1,71 @@
package haus.nightmare.lib3270j.graphics;
import java.io.Serializable;
import java.util.Objects;
/**
* Lightweight pure-Java POJO point for 2D presentation coordinates.
* Completely decouples lib3270j from java.awt.Point.
*/
public class Point implements Serializable {
private static final long serialVersionUID = 1L;
public int x;
public int y;
public Point() {
this(0, 0);
}
public Point(Point p) {
this(p != null ? p.x : 0, p != null ? p.y : 0);
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
}
public void setLocation(Point p) {
if (p != null) {
this.x = p.x;
this.y = p.y;
}
}
public void translate(int dx, int dy) {
this.x += dx;
this.y += dy;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point)) return false;
Point pt = (Point) obj;
return (x == pt.x) && (y == pt.y);
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return getClass().getName() + "[x=" + x + ",y=" + y + "]";
}
}
@@ -3,6 +3,7 @@ package haus.nightmare.lib3270j.graphics;
/** /**
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE). * 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). * Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
* Decoupled from java.awt using PixelBuffer.
*/ */
public class ProgramSymbolSet { public class ProgramSymbolSet {
@@ -64,8 +65,8 @@ public class ProgramSymbolSet {
private int[] cachedRgbArray; private int[] cachedRgbArray;
private int cachedFgRgb = -1; private int cachedFgRgb = -1;
private int cachedBgRgb = -1; private int cachedBgRgb = -1;
private java.awt.image.BufferedImage cachedImage; private PixelBuffer cachedPixelBuffer;
private java.awt.image.BufferedImage cachedScaledImage; private PixelBuffer cachedScaledPixelBuffer;
private int cachedTargetW = 0; private int cachedTargetW = 0;
private int cachedTargetH = 0; private int cachedTargetH = 0;
private int cachedScaledFgRgb = -1; private int cachedScaledFgRgb = -1;
@@ -103,23 +104,21 @@ public class ProgramSymbolSet {
} }
/** /**
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight). * Returns a PixelBuffer scaled directly to target dimensions (cellWidth x cellHeight).
* Enables unscaled 1:1 hardware blitting in Java2D.
*/ */
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) { public synchronized PixelBuffer getScaledPixelBuffer(int targetW, int targetH, int fgArgb, int bgArgb) {
if (targetW <= 0 || targetH <= 0) { if (targetW <= 0 || targetH <= 0) {
return getImage(fgArgb, bgArgb); return getPixelBuffer(fgArgb, bgArgb);
} }
if (targetW == width && targetH == height) { if (targetW == width && targetH == height) {
return getImage(fgArgb, bgArgb); return getPixelBuffer(fgArgb, bgArgb);
} }
if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH if (cachedScaledPixelBuffer != null && cachedTargetW == targetW && cachedTargetH == targetH
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) { && cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
return cachedScaledImage; return cachedScaledPixelBuffer;
} }
int[] srcRgb = getRgbPixels(fgArgb, bgArgb); int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB); int[] dstRgb = new int[targetW * targetH];
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
for (int dy = 0; dy < targetH; dy++) { for (int dy = 0; dy < targetH; dy++) {
int sy = dy * height / targetH; int sy = dy * height / targetH;
@@ -131,30 +130,36 @@ public class ProgramSymbolSet {
} }
} }
this.cachedScaledImage = scaled; this.cachedScaledPixelBuffer = new DefaultPixelBuffer(targetW, targetH, dstRgb);
this.cachedTargetW = targetW; this.cachedTargetW = targetW;
this.cachedTargetH = targetH; this.cachedTargetH = targetH;
this.cachedScaledFgRgb = fgArgb; this.cachedScaledFgRgb = fgArgb;
this.cachedScaledBgRgb = bgArgb; this.cachedScaledBgRgb = bgArgb;
return scaled; return this.cachedScaledPixelBuffer;
}
public synchronized PixelBuffer getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
return getScaledPixelBuffer(targetW, targetH, fgArgb, bgArgb);
} }
/** /**
* Computes and returns the cached BufferedImage for this symbol glyph. * Computes and returns the cached PixelBuffer for this symbol glyph.
* Eliminates per-cell heap allocations during high frame rate rendering.
*/ */
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) { public synchronized PixelBuffer getPixelBuffer(int fgArgb, int bgArgb) {
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) { if (cachedPixelBuffer != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
return cachedImage; return cachedPixelBuffer;
} }
int[] rgb = getRgbPixels(fgArgb, bgArgb); int[] rgb = getRgbPixels(fgArgb, bgArgb);
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB); int[] imgData = new int[rgb.length];
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
System.arraycopy(rgb, 0, imgData, 0, rgb.length); System.arraycopy(rgb, 0, imgData, 0, rgb.length);
this.cachedImage = img; this.cachedPixelBuffer = new DefaultPixelBuffer(width, height, imgData);
this.cachedFgRgb = fgArgb; this.cachedFgRgb = fgArgb;
this.cachedBgRgb = bgArgb; this.cachedBgRgb = bgArgb;
return img; return this.cachedPixelBuffer;
}
public synchronized PixelBuffer getImage(int fgArgb, int bgArgb) {
return getPixelBuffer(fgArgb, bgArgb);
} }
/** /**
@@ -0,0 +1,171 @@
package haus.nightmare.lib3270j.graphics;
import java.io.Serializable;
import java.util.Objects;
/**
* Lightweight pure-Java POJO rectangle for 2D presentation coordinates.
* Completely decouples lib3270j from java.awt.Rectangle.
*/
public class Rectangle implements Serializable {
private static final long serialVersionUID = 1L;
public int x;
public int y;
public int width;
public int height;
public Rectangle() {
this(0, 0, 0, 0);
}
public Rectangle(Rectangle r) {
this(r != null ? r.x : 0, r != null ? r.y : 0, r != null ? r.width : 0, r != null ? r.height : 0);
}
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public Rectangle(int width, int height) {
this(0, 0, width, height);
}
public Rectangle(Point p, Dimension d) {
this(p != null ? p.x : 0, p != null ? p.y : 0, d != null ? d.width : 0, d != null ? d.height : 0);
}
public Rectangle(Point p) {
this(p != null ? p.x : 0, p != null ? p.y : 0, 0, 0);
}
public Rectangle(Dimension d) {
this(0, 0, d != null ? d.width : 0, d != null ? d.height : 0);
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public void setBounds(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public void setBounds(Rectangle r) {
if (r != null) {
setBounds(r.x, r.y, r.width, r.height);
}
}
public Point getLocation() {
return new Point(x, y);
}
public void setLocation(Point p) {
if (p != null) {
this.x = p.x;
this.y = p.y;
}
}
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
}
public Dimension getSize() {
return new Dimension(width, height);
}
public void setSize(Dimension d) {
if (d != null) {
this.width = d.width;
this.height = d.height;
}
}
public void setSize(int width, int height) {
this.width = width;
this.height = height;
}
public boolean contains(int X, int Y) {
int w = this.width;
int h = this.height;
if ((w | h) < 0) return false;
int x = this.x;
int y = this.y;
if (X < x || Y < y) return false;
w += x;
h += y;
return ((w < x || w > X) && (h < y || h > Y));
}
public boolean contains(Point p) {
return p != null && contains(p.x, p.y);
}
public boolean intersects(Rectangle r) {
if (r == null) return false;
int tw = this.width;
int th = this.height;
int rw = r.width;
int rh = r.height;
if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) return false;
int tx = this.x;
int ty = this.y;
int rx = r.x;
int ry = r.y;
rw += rx;
rh += ry;
tw += tx;
th += ty;
return ((rw < rx || rw > tx) &&
(rh < ry || rh > ty) &&
(tw < tx || tw > rx) &&
(th < ty || th > ry));
}
public boolean isEmpty() {
return (width <= 0) || (height <= 0);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Rectangle)) return false;
Rectangle r = (Rectangle) obj;
return ((x == r.x) &&
(y == r.y) &&
(width == r.width) &&
(height == r.height));
}
@Override
public int hashCode() {
return Objects.hash(x, y, width, height);
}
@Override
public String toString() {
return getClass().getName() + "[x=" + x + ",y=" + y + ",width=" + width + ",height=" + height + "]";
}
}
@@ -412,6 +412,155 @@ public class InputProcessor {
} }
} }
/**
* Checks if all character locations on the given row are protected.
*/
public boolean isLineProtected(int row) {
if (screen == null || !screen.isFormatted()) return false;
int cols = screen.getCols();
int rows = screen.getRows();
if (row < 0 || row >= rows) return false;
int start = row * cols;
int end = start + cols;
for (int i = start; i < end; i++) {
if (screen.getCell(i).isFieldAttribute()) continue;
byte fa = screen.getFieldAttributeAt(i);
if (!faIsProtected(fa & 0xFF)) {
return false;
}
}
return true;
}
/**
* Paste text into the presentation space.
* When enablePasteFromExcel is true:
* - '\t' triggers a Tab action advancing to the next unprotected input field (SBA).
* - '\n' or '\r\n' triggers a NewLine action advancing to the first unprotected field of the next line.
* When pasteStopAtProtectedLine is true:
* - Halts or truncates paste if cursor reaches a protected line/boundary or if the current field is full.
*
* @param text text to paste
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
* @return number of characters pasted
*/
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
if (text == null || text.isEmpty() || screen == null || keyboardLocked) {
return 0;
}
if (isNvtMode()) {
try {
fsm.sendNVTString(text);
return text.length();
} catch (IOException e) {
log.warning("Failed to send NVT paste: " + e.getMessage());
return 0;
}
}
int count = 0;
int len = text.length();
int i = 0;
while (i < len && !keyboardLocked) {
char ch = text.charAt(i);
// Handle newline sequences: \r\n, \r, or \n
if (ch == '\r' || ch == '\n') {
if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') {
i++; // skip \n of \r\n
}
if (enablePasteFromExcel) {
int curRow = screen.getCursorRow();
int nextRow = (curRow + 1) % screen.getRows();
if (pasteStopAtProtectedLine && isLineProtected(nextRow)) {
break; // Stop paste when next line is protected
}
newline();
}
i++;
continue;
}
// Handle tab character
if (ch == '\t') {
if (enablePasteFromExcel) {
tab();
int newAddr = screen.getCursorAddress();
if (pasteStopAtProtectedLine && screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(newAddr);
if (faIsProtected(fa & 0xFF)) {
break;
}
}
}
i++;
continue;
}
// Regular printable character
if (ch >= 0x20 && ch != 0x7F) {
if (screen.isFormatted()) {
int baddr = screen.getCursorAddress();
ExtendedAttribute ea = screen.getCell(baddr);
if (ea.isFieldAttribute()) {
baddr = (baddr + 1) % (screen.getRows() * screen.getCols());
}
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) {
if (pasteStopAtProtectedLine) {
break; // Stop paste immediately at protected boundary
} else {
tab();
baddr = screen.getCursorAddress();
if (faIsProtected(screen.getFieldAttributeAt(baddr) & 0xFF)) {
break;
}
}
}
}
typeCharacter(ch);
count++;
// If pasteStopAtProtectedLine is enabled, check if the cursor after typing has hit a protected field
if (pasteStopAtProtectedLine && screen.isFormatted() && !keyboardLocked) {
int curAddr = screen.getCursorAddress();
ExtendedAttribute curCell = screen.getCell(curAddr);
if (curCell.isFieldAttribute()) {
byte nextFa = curCell.fa;
if (faIsProtected(nextFa & 0xFF)) {
if (i + 1 < len) {
char nextCh = text.charAt(i + 1);
if (nextCh != '\t' && nextCh != '\r' && nextCh != '\n') {
break;
}
}
}
} else {
byte curFa = screen.getFieldAttributeAt(curAddr);
if (faIsProtected(curFa & 0xFF)) {
if (i + 1 < len) {
char nextCh = text.charAt(i + 1);
if (nextCh != '\t' && nextCh != '\r' && nextCh != '\n') {
break;
}
}
}
}
}
}
i++;
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return count;
}
/** /**
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields). * Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
*/ */
@@ -14,4 +14,7 @@ public interface ConnectionListener {
/** Called when TN3270E negotiation completes. */ /** Called when TN3270E negotiation completes. */
default void onTN3270ENegotiated(String deviceType, String deviceName) {} default void onTN3270ENegotiated(String deviceType, String deviceName) {}
/** Called when TN3270E functions negotiation completes or changes. */
default void onTN3270EFunctionsNegotiated(boolean[] functions) {}
} }
@@ -15,4 +15,7 @@ public interface ScreenUpdateListener {
/** Called when the screen size changes (erase/write vs erase/write alternate). */ /** Called when the screen size changes (erase/write vs erase/write alternate). */
default void onScreenSizeChanged(int rows, int cols) {} default void onScreenSizeChanged(int rows, int cols) {}
/** Called when the keyboard is unlocked (e.g. via WCC restore, AUTO_SYS_UNLOCK, or Contention Resolution SDI). */
default void onKeyboardUnlocked() {}
} }
@@ -1305,6 +1305,9 @@ public class NvtProcessor {
} }
private void notifyScreenUpdated() { private void notifyScreenUpdated() {
if (screenBuffer != null) {
screenBuffer.notifyScreenUpdate();
}
for (ScreenUpdateListener l : screenListeners) { for (ScreenUpdateListener l : screenListeners) {
l.onScreenUpdated(); l.onScreenUpdated();
} }
@@ -5,6 +5,11 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField; import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList; import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLPS; import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -60,6 +65,61 @@ public class ScreenBuffer {
private final EbcdicTranslator translator; private final EbcdicTranslator translator;
private final Object renderLock = new Object(); private final Object renderLock = new Object();
// Synchronization primitives and screen update listeners (Phase 12)
private final ReentrantLock syncLock = new ReentrantLock();
private final Condition syncCondition = syncLock.newCondition();
private final List<ScreenUpdateListener> updateListeners = new CopyOnWriteArrayList<>();
public ReentrantLock getSyncLock() {
return syncLock;
}
public Condition getSyncCondition() {
return syncCondition;
}
public void addUpdateListener(ScreenUpdateListener l) {
if (l != null && !updateListeners.contains(l)) {
updateListeners.add(l);
}
}
public void removeUpdateListener(ScreenUpdateListener l) {
updateListeners.remove(l);
}
public void signalWaiters() {
syncLock.lock();
try {
syncCondition.signalAll();
} finally {
syncLock.unlock();
}
}
public void notifyScreenUpdate() {
updateDisplaySnapshot();
signalWaiters();
for (ScreenUpdateListener l : updateListeners) {
try {
l.onScreenUpdated();
} catch (Exception ignored) {}
}
}
public void notifyCursorMoved(int oldAddress, int newAddress) {
signalWaiters();
for (ScreenUpdateListener l : updateListeners) {
try {
l.onCursorMoved(oldAddress, newAddress);
} catch (Exception ignored) {}
}
}
public void notifyCursorMoved() {
notifyCursorMoved(cursorAddress, cursorAddress);
}
public Object getRenderLock() { public Object getRenderLock() {
return renderLock; return renderLock;
} }
@@ -272,9 +332,16 @@ public class ScreenBuffer {
// ========== Cursor ========== // ========== Cursor ==========
public int getCursorAddress() { return cursorAddress; } public int getCursorAddress() { return cursorAddress; }
public synchronized void setCursorAddress(int addr) { public void setCursorAddress(int addr) {
this.cursorAddress = addr; int oldAddr;
this.displayCursorAddress = addr; synchronized (this) {
oldAddr = this.cursorAddress;
this.cursorAddress = addr;
this.displayCursorAddress = addr;
}
if (oldAddr != addr) {
notifyCursorMoved(oldAddr, addr);
}
} }
public synchronized void setCursorPosition(int row, int col) { public synchronized void setCursorPosition(int row, int col) {
int r = Math.max(0, Math.min(row, rows - 1)); int r = Math.max(0, Math.min(row, rows - 1));
@@ -831,19 +898,28 @@ public class ScreenBuffer {
} }
public synchronized void setText(String text) { public synchronized void setText(String text) {
setText(text, 0);
}
public synchronized void setText(String text, int pos) {
if (text == null) return; if (text == null) return;
int size = rows * cols; int size = rows * cols;
int len = Math.min(text.length(), size); if (pos < 0 || pos >= size) return;
int len = Math.min(text.length(), size - pos);
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
char ch = text.charAt(i); char ch = text.charAt(i);
int ebc = translator.unicodeToEbcdic(ch); int ebc = translator.unicodeToEbcdic(ch);
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0); buffer[pos + i].ec = (byte) (ebc >= 0 ? ebc : 0);
buffer[i].ucs4 = ch; buffer[pos + i].ucs4 = ch;
} }
screenChanged = true; screenChanged = true;
updateDisplaySnapshot(); updateDisplaySnapshot();
} }
public void setText(String text, int row, int col) {
setText(text, row * cols + col);
}
public int searchString(String target) { public int searchString(String target) {
if (target == null || target.isEmpty()) return -1; if (target == null || target.isEmpty()) return -1;
String full = getText(); String full = getText();
@@ -1119,8 +1195,10 @@ public class ScreenBuffer {
ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen]; ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen];
for (int i = 0; i < wordLen; i++) { for (int i = 0; i < wordLen; i++) {
wordCells[i] = new ExtendedAttribute(); wordCells[i] = new ExtendedAttribute();
wordCells[i].copyFrom(getCell(wordStartAddr + i)); ExtendedAttribute srcCell = getCell(wordStartAddr + i);
getCell(wordStartAddr + i).clear(); wordCells[i].copyFrom(srcCell);
srcCell.ec = 0;
srcCell.ucs4 = 0;
} }
int nextRow = (curRow + 1) % rows; int nextRow = (curRow + 1) % rows;
@@ -6,6 +6,8 @@ import haus.nightmare.lib3270j.Telnet3270Client;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.logging.Level; import java.util.logging.Level;
@@ -28,6 +30,10 @@ public class TelnetConnection {
private final TelnetFSM fsm; private final TelnetFSM fsm;
private final ConnectionConfig config; private final ConnectionConfig config;
private final AtomicLong lastActivityTime = new AtomicLong(System.currentTimeMillis());
private ScheduledExecutorService keepAliveExecutor;
private volatile boolean intentionalDisconnect = false;
private javax.net.ssl.SSLSession sslSession; private javax.net.ssl.SSLSession sslSession;
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) { public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
@@ -91,6 +97,7 @@ public class TelnetConnection {
if (config.getSoTimeoutMs() > 0) { if (config.getSoTimeoutMs() > 0) {
rawSocket.setSoTimeout(config.getSoTimeoutMs()); rawSocket.setSoTimeout(config.getSoTimeoutMs());
} }
applyExtendedSocketOptions(rawSocket);
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs()); rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
// Perform proxy handshake if configured // Perform proxy handshake if configured
@@ -123,6 +130,7 @@ public class TelnetConnection {
if (config.getSoTimeoutMs() > 0) { if (config.getSoTimeoutMs() > 0) {
sslSocket.setSoTimeout(config.getSoTimeoutMs()); sslSocket.setSoTimeout(config.getSoTimeoutMs());
} }
applyExtendedSocketOptions(sslSocket);
applyTlsSocketSettings(sslSocket); applyTlsSocketSettings(sslSocket);
sslSocket.startHandshake(); sslSocket.startHandshake();
socket = sslSocket; socket = sslSocket;
@@ -143,10 +151,13 @@ public class TelnetConnection {
log.info("Connected to " + socket.getRemoteSocketAddress()); log.info("Connected to " + socket.getRemoteSocketAddress());
intentionalDisconnect = false;
lastActivityTime.set(System.currentTimeMillis());
running = true; running = true;
readerThread = new Thread(this::readLoop, "TN3270-Reader"); readerThread = new Thread(this::readLoop, "TN3270-Reader");
readerThread.setDaemon(true); readerThread.setDaemon(true);
readerThread.start(); readerThread.start();
startKeepAlive();
} }
/** /**
@@ -167,6 +178,7 @@ public class TelnetConnection {
if (config.getSoTimeoutMs() > 0) { if (config.getSoTimeoutMs() > 0) {
sslSocket.setSoTimeout(config.getSoTimeoutMs()); sslSocket.setSoTimeout(config.getSoTimeoutMs());
} }
applyExtendedSocketOptions(sslSocket);
applyTlsSocketSettings(sslSocket); applyTlsSocketSettings(sslSocket);
sslSocket.startHandshake(); sslSocket.startHandshake();
this.socket = sslSocket; this.socket = sslSocket;
@@ -200,6 +212,106 @@ public class TelnetConnection {
} }
} }
private void applyExtendedSocketOptions(Socket s) {
if (s == null) return;
try {
Class<?> extClass = Class.forName("jdk.net.ExtendedSocketOptions");
// TCP_KEEPIDLE, TCP_KEEPINTERVAL, TCP_KEEPCOUNT
if (config != null && config.isSoKeepAlive() && config.getKeepAliveIntervalSeconds() > 0) {
try {
java.lang.reflect.Field fIdle = extClass.getField("TCP_KEEPIDLE");
@SuppressWarnings("unchecked")
SocketOption<Integer> optIdle = (SocketOption<Integer>) fIdle.get(null);
s.setOption(optIdle, config.getKeepAliveIntervalSeconds());
} catch (Throwable ignored) {}
try {
java.lang.reflect.Field fIntv = extClass.getField("TCP_KEEPINTERVAL");
@SuppressWarnings("unchecked")
SocketOption<Integer> optIntv = (SocketOption<Integer>) fIntv.get(null);
s.setOption(optIntv, Math.max(1, Math.min(10, config.getKeepAliveIntervalSeconds())));
} catch (Throwable ignored) {}
try {
java.lang.reflect.Field fCnt = extClass.getField("TCP_KEEPCOUNT");
@SuppressWarnings("unchecked")
SocketOption<Integer> optCnt = (SocketOption<Integer>) fCnt.get(null);
s.setOption(optCnt, 3);
} catch (Throwable ignored) {}
}
// TCP_USER_TIMEOUT
if (config != null && config.getTcpUserTimeoutMs() > 0) {
try {
java.lang.reflect.Field fTimeout = extClass.getField("TCP_USER_TIMEOUT");
@SuppressWarnings("unchecked")
SocketOption<Integer> optTimeout = (SocketOption<Integer>) fTimeout.get(null);
s.setOption(optTimeout, config.getTcpUserTimeoutMs());
} catch (Throwable ignored) {}
}
} catch (Throwable ignored) {
// Extended socket options not supported on this platform/runtime (e.g. macOS/Android)
}
}
public synchronized void startKeepAlive() {
stopKeepAlive();
if (config != null && config.isKeepAliveEnabled() && config.getKeepAliveIntervalSeconds() > 0) {
keepAliveExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "TN3270-KeepAlive");
t.setDaemon(true);
return t;
});
int interval = config.getKeepAliveIntervalSeconds();
long checkPeriod = Math.max(1, Math.min(5, interval));
keepAliveExecutor.scheduleWithFixedDelay(this::checkAndSendKeepAlive, checkPeriod, checkPeriod, TimeUnit.SECONDS);
log.fine("Keep-Alive heartbeat scheduled every " + interval + "s (check every " + checkPeriod + "s)");
}
}
public synchronized void stopKeepAlive() {
if (keepAliveExecutor != null) {
keepAliveExecutor.shutdownNow();
keepAliveExecutor = null;
}
}
private void checkAndSendKeepAlive() {
if (!running || !isConnected()) {
return;
}
long now = System.currentTimeMillis();
long elapsed = now - lastActivityTime.get();
long intervalMs = (config != null ? config.getKeepAliveIntervalSeconds() : 120) * 1000L;
if (elapsed >= intervalMs) {
try {
sendKeepAliveHeartbeat();
} catch (Exception e) {
log.log(Level.WARNING, "Failed to send keep-alive heartbeat", e);
}
}
}
public synchronized void sendKeepAliveHeartbeat() throws IOException {
if (!isConnected() || outputStream == null) {
return;
}
String type = (config != null) ? config.getKeepAliveType() : "NOP";
if ("TIMING-MARK".equalsIgnoreCase(type)) {
log.fine("Transmitting Keep-Alive heartbeat: IAC DO TIMING-MARK");
byte[] tm = new byte[] { (byte) IAC, (byte) DO, (byte) TELOPT_TM };
outputStream.write(tm);
outputStream.flush();
} else {
log.fine("Transmitting Keep-Alive heartbeat: IAC NOP");
byte[] nop = new byte[] { (byte) IAC, (byte) NOP };
outputStream.write(nop);
outputStream.flush();
}
lastActivityTime.set(System.currentTimeMillis());
}
public long getLastActivityTime() {
return lastActivityTime.get();
}
private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException { private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
OutputStream out = s.getOutputStream(); OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream(); InputStream in = s.getInputStream();
@@ -411,6 +523,7 @@ public class TelnetConnection {
if (outputStream == null) return; if (outputStream == null) return;
outputStream.write(data, offset, length); outputStream.write(data, offset, length);
outputStream.flush(); outputStream.flush();
lastActivityTime.set(System.currentTimeMillis());
if (log.isLoggable(Level.FINE)) { if (log.isLoggable(Level.FINE)) {
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length)); log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
} }
@@ -432,13 +545,16 @@ public class TelnetConnection {
byte[] escaped = out.toByteArray(); byte[] escaped = out.toByteArray();
outputStream.write(escaped, 0, escaped.length); outputStream.write(escaped, 0, escaped.length);
outputStream.flush(); outputStream.flush();
lastActivityTime.set(System.currentTimeMillis());
} }
/** /**
* Disconnect from the host. * Disconnect from the host.
*/ */
public void disconnect() { public void disconnect() {
intentionalDisconnect = true;
running = false; running = false;
stopKeepAlive();
try { try {
if (socket != null && !socket.isClosed()) { if (socket != null && !socket.isClosed()) {
socket.shutdownInput(); socket.shutdownInput();
@@ -471,10 +587,12 @@ public class TelnetConnection {
int n = inputStream.read(buf); int n = inputStream.read(buf);
if (n < 0) { if (n < 0) {
log.info("Host disconnected (EOF)"); log.info("Host disconnected (EOF)");
fsm.onDisconnect(); boolean unexpected = !intentionalDisconnect;
fsm.onDisconnect(unexpected);
break; break;
} }
if (n > 0) { if (n > 0) {
lastActivityTime.set(System.currentTimeMillis());
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));
} }
@@ -486,14 +604,23 @@ public class TelnetConnection {
} }
} }
} }
} catch (SocketTimeoutException e) {
if (running && !intentionalDisconnect) {
log.warning("Socket read timeout (" + (config != null ? config.getSoTimeoutMs() : 0) + "ms): " + e.getMessage());
fsm.onDisconnect(true);
}
} catch (SocketException e) { } catch (SocketException e) {
if (running) { if (running && !intentionalDisconnect) {
log.info("Socket closed: " + e.getMessage()); log.info("Socket closed unexpectedly: " + e.getMessage());
fsm.onDisconnect(); fsm.onDisconnect(true);
} else if (running) {
fsm.onDisconnect(false);
} }
} catch (IOException e) { } catch (IOException e) {
if (running) { if (running && !intentionalDisconnect) {
log.log(Level.WARNING, "Read error", e); log.log(Level.WARNING, "Read error: " + e.getMessage());
fsm.onDisconnect(true);
} else if (running) {
fsm.onError("Read error: " + e.getMessage()); fsm.onError("Read error: " + e.getMessage());
} }
} catch (Throwable t) { } catch (Throwable t) {
@@ -501,6 +628,8 @@ public class TelnetConnection {
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t); log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
fsm.onError("Network loop error: " + t.getMessage()); fsm.onError("Network loop error: " + t.getMessage());
} }
} finally {
stopKeepAlive();
} }
} }
@@ -120,6 +120,11 @@ public class TelnetFSM {
private String connectedLu; private String connectedLu;
private String connectedType; private String connectedType;
// Phase 10: Contention Resolution & Auto-Unlock State
private boolean sdi_flag = false;
private boolean kri_flag = false;
private boolean negotiateContentionResolution = true;
public enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP } public enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) { public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
@@ -190,7 +195,7 @@ public class TelnetFSM {
eFuncs[FUNC_SYSREQ] = true; eFuncs[FUNC_SYSREQ] = true;
eFuncs[FUNC_SNA_SENSE] = true; eFuncs[FUNC_SNA_SENSE] = true;
eFuncs[FUNC_DATA_STREAM_CTL] = true; eFuncs[FUNC_DATA_STREAM_CTL] = true;
eFuncs[FUNC_CONTENTION_RESOLUTION] = true; eFuncs[FUNC_CONTENTION_RESOLUTION] = negotiateContentionResolution;
statusDisplay(STATUS_CONNECTING, "Connecting to host"); statusDisplay(STATUS_CONNECTING, "Connecting to host");
changeState(ConnectionState.TELNET_PENDING); changeState(ConnectionState.TELNET_PENDING);
@@ -951,6 +956,10 @@ public class TelnetFSM {
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames()); log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
log.info("TN3270E negotiation complete"); log.info("TN3270E negotiation complete");
if (dsProcessor != null) {
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
}
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered // RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
// to be bound immediately upon completion of the FUNCTIONS negotiation. // to be bound immediately upon completion of the FUNCTIONS negotiation.
if (eFuncs[FUNC_BIND_IMAGE]) { if (eFuncs[FUNC_BIND_IMAGE]) {
@@ -963,6 +972,7 @@ public class TelnetFSM {
// Notify listeners // Notify listeners
for (ConnectionListener l : connectionListeners) { for (ConnectionListener l : connectionListeners) {
l.onTN3270ENegotiated(connectedType, connectedLu); l.onTN3270ENegotiated(connectedType, connectedLu);
l.onTN3270EFunctionsNegotiated(eFuncs);
} }
} }
@@ -996,9 +1006,59 @@ public class TelnetFSM {
processTN3270ERecord(data); processTN3270ERecord(data);
} else { } else {
// Plain TN3270 mode: data is raw 3270 data stream // Plain TN3270 mode: data is raw 3270 data stream
dsProcessor.processRecord(data, 0, data.length, true); if (dsProcessor != null) {
dsProcessor.processRecord(data, 0, data.length, false);
}
notifyScreenUpdate(); notifyScreenUpdate();
} }
// Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR
if (dsProcessor != null) {
boolean crActive = isContentionResolutionNegotiated();
if (crActive) {
if (this.sdi_flag && !dsProcessor.isRcvdRead()) {
if (dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
}
this.sdi_flag = false;
dsProcessor.setUnlockPending(false);
if (dsProcessor.isUnlockSysPending() || this.kri_flag) {
if (dsProcessor.getInputProcessor() != null && dsProcessor.getInputProcessor().getOIA() != null) {
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
this.kri_flag = false;
dsProcessor.setUnlockSysPending(false);
notifyKeyboardUnlocked();
}
this.kri_flag = false;
} else {
// Contention Resolution is not active
boolean autoSysUnlock = (config != null) ? config.isAutoSysUnlock() : true;
if (autoSysUnlock && !dsProcessor.isRcvdRead()) {
boolean sysLocked = false;
if (dsProcessor.getInputProcessor() != null) {
if (dsProcessor.getInputProcessor().isKeyboardLocked() ||
(dsProcessor.getInputProcessor().getOIA() != null && dsProcessor.getInputProcessor().getOIA().isXSystem())) {
sysLocked = true;
}
}
if (sysLocked) {
if (dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
if (dsProcessor.getInputProcessor().getOIA() != null) {
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
dsProcessor.setUnlockSysPending(false);
notifyKeyboardUnlocked();
}
}
}
this.sdi_flag = false;
this.kri_flag = false;
dsProcessor.setRcvdRead(false);
}
} }
public void processTn3270eHeader(byte[] data) { public void processTn3270eHeader(byte[] data) {
@@ -1016,6 +1076,12 @@ public class TelnetFSM {
int responseFlag = data[2] & 0xFF; int responseFlag = data[2] & 0xFF;
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF); int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
this.sdi_flag = (requestFlag & 0x01) != 0;
this.kri_flag = (requestFlag & 0x02) != 0;
if (dsProcessor != null) {
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
}
log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) + log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) +
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber); " rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
@@ -1033,7 +1099,7 @@ public class TelnetFSM {
tn3270eSubmode = TN3270ESubmode.E_3270; tn3270eSubmode = TN3270ESubmode.E_3270;
} }
try { try {
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true); dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, false);
notifyScreenUpdate(); notifyScreenUpdate();
// Send positive response if required // Send positive response if required
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
@@ -1423,8 +1489,49 @@ public class TelnetFSM {
} }
} }
public void setConnectionState(ConnectionState newState) {
changeState(newState);
}
public void onDisconnect() { public void onDisconnect() {
changeState(ConnectionState.NOT_CONNECTED); onDisconnect(false);
}
public void onDisconnect(boolean unexpected) {
if (unexpected && config != null && config.isAutoReconnect()) {
log.info("Unexpected connection loss — transitioning to RECONNECTING");
changeState(ConnectionState.RECONNECTING);
} else {
changeState(ConnectionState.NOT_CONNECTED);
}
}
/**
* Cleanly reset Telnet and TN3270E session state prior to reconnecting.
*/
public synchronized void resetSessionState() {
state = TNS_DATA;
java.util.Arrays.fill(myOpts, false);
java.util.Arrays.fill(hisOpts, false);
ibuf.reset();
sbbuf.reset();
tn3270eNegotiated = false;
tn3270eSubmode = TN3270ESubmode.UNBOUND;
tn3270eBound = false;
java.util.Arrays.fill(eFuncs, false);
eXmitSeq = 0;
lastRcvSeq = 0;
lastRespType = 0;
lastRespCode = 0;
responseRequired = RSF_NO_RESPONSE;
deferredWillTtype = false;
tn3270eDeviceTypeSent = false;
ttypeIndex = 0;
luIndex = 0;
connectedLu = null;
connectedType = null;
sdi_flag = false;
kri_flag = false;
} }
public void onError(String message) { public void onError(String message) {
@@ -1434,7 +1541,10 @@ public class TelnetFSM {
} }
} }
private void notifyScreenUpdate() { public void notifyScreenUpdate() {
if (screenBuffer != null) {
screenBuffer.notifyScreenUpdate();
}
for (ScreenUpdateListener l : screenListeners) { for (ScreenUpdateListener l : screenListeners) {
l.onScreenUpdated(); l.onScreenUpdated();
} }
@@ -1810,4 +1920,41 @@ public class TelnetFSM {
public boolean isFunctionNegotiated(int func) { public boolean isFunctionNegotiated(int func) {
return func >= 0 && func < eFuncs.length && eFuncs[func]; return func >= 0 && func < eFuncs.length && eFuncs[func];
} }
public boolean isContentionResolutionNegotiated() {
return tn3270eNegotiated && eFuncs[FUNC_CONTENTION_RESOLUTION];
}
public boolean isNegotiateContentionResolution() {
return negotiateContentionResolution;
}
public void setNegotiateContentionResolution(boolean neg) {
this.negotiateContentionResolution = neg;
eFuncs[FUNC_CONTENTION_RESOLUTION] = neg;
}
public void setContentionResolutionNegotiated(boolean cr) {
if (cr) {
this.tn3270eNegotiated = true;
}
eFuncs[FUNC_CONTENTION_RESOLUTION] = cr;
if (dsProcessor != null) {
dsProcessor.setContentionResolution(cr);
}
}
public boolean isSdi_flag() { return sdi_flag; }
public void setSdi_flag(boolean sdi) { this.sdi_flag = sdi; }
public boolean isKri_flag() { return kri_flag; }
public void setKri_flag(boolean kri) { this.kri_flag = kri; }
public void notifyKeyboardUnlocked() {
for (ScreenUpdateListener l : screenListeners) {
try {
l.onKeyboardUnlocked();
} catch (Exception ignored) {}
}
}
} }
@@ -152,6 +152,14 @@ public class DS3270 {
public boolean suppressClearEvent; public boolean suppressClearEvent;
public boolean wsfvalid = true; public boolean wsfvalid = true;
// Phase 10: State tracking matching HoD DS3270
protected boolean autoSysUnlock = true;
protected boolean sdi_flag = false;
protected boolean kri_flag = false;
protected boolean unlock_pending = false;
protected boolean unlock_sys_pending = false;
protected boolean rcvdRead = false;
// Underlying lib3270j data stream processor // Underlying lib3270j data stream processor
private final DataStreamProcessor delegate; private final DataStreamProcessor delegate;
private ECLSession session; private ECLSession session;
@@ -172,9 +180,15 @@ public class DS3270 {
public DS3270(ECLSession session, ECLPS ps) { public DS3270(ECLSession session, ECLPS ps) {
this.session = session; this.session = session;
this.ps = ps; this.ps = ps;
if (session != null) {
this.autoSysUnlock = session.isAutoSysUnlock();
}
ScreenBuffer sb = (ps != null) ? ps.getScreenBuffer() : new ScreenBuffer(); ScreenBuffer sb = (ps != null) ? ps.getScreenBuffer() : new ScreenBuffer();
EbcdicTranslator trans = (ps != null) ? ps.getTranslator() : new EbcdicTranslator(); EbcdicTranslator trans = (ps != null) ? ps.getTranslator() : new EbcdicTranslator();
this.delegate = new DataStreamProcessor(sb, trans); this.delegate = new DataStreamProcessor(sb, trans);
if (session != null) {
this.delegate.setAutoSysUnlock(session.isAutoSysUnlock());
}
if (ps != null && ps.getInputProcessor() != null) { if (ps != null && ps.getInputProcessor() != null) {
this.delegate.setInputProcessor(ps.getInputProcessor()); this.delegate.setInputProcessor(ps.getInputProcessor());
} }
@@ -214,8 +228,135 @@ public class DS3270 {
return false; return false;
} }
public boolean isAutoSysUnlock() {
return autoSysUnlock;
}
public void setAutoSysUnlock(boolean autoSysUnlock) {
this.autoSysUnlock = autoSysUnlock;
if (delegate != null) {
delegate.setAutoSysUnlock(autoSysUnlock);
}
}
public boolean isSdi_flag() { return sdi_flag; }
public void setSdi_flag(boolean sdi) { this.sdi_flag = sdi; }
public boolean isKri_flag() { return kri_flag; }
public void setKri_flag(boolean kri) { this.kri_flag = kri; }
public boolean isUnlock_pending() { return unlock_pending; }
public void setUnlock_pending(boolean pending) { this.unlock_pending = pending; }
public boolean isUnlock_sys_pending() { return unlock_sys_pending; }
public void setUnlock_sys_pending(boolean pending) { this.unlock_sys_pending = pending; }
public boolean isRcvdRead() {
return rcvdRead || (delegate != null && delegate.isRcvdRead());
}
public void setRcvdRead(boolean rcvd) {
this.rcvdRead = rcvd;
if (delegate != null) delegate.setRcvdRead(rcvd);
}
public void receiveHeaderData(short s, short s2, short s3, int n) {
this.sdi_flag = (s2 & request_bit_SDI) != 0;
this.kri_flag = (s2 & request_bit_KRI) != 0;
}
public void receiveHeader(short s, short s2, short s3, int n) {
receiveHeaderData(s, s2, s3, n);
}
public void endOfRecord() { public void endOfRecord() {
log.fine("DS3270 endOfRecord"); log.fine("DS3270 endOfRecord");
boolean crActive = (session != null && session.getContentionResolution());
if (crActive) {
if (this.sdi_flag && !isRcvdRead()) {
if (this.ps != null) {
this.ps.unlockKeyboard(7);
} else if (delegate != null && delegate.getInputProcessor() != null) {
delegate.getInputProcessor().setKeyboardLocked(false);
}
this.sdi_flag = false;
this.unlock_pending = false;
if (this.unlock_sys_pending || this.kri_flag) {
if (this.ps != null) {
this.ps.unlockKeyboard(8);
} else if (delegate != null && delegate.getInputProcessor() != null) {
delegate.getInputProcessor().setKeyboardLocked(false);
if (delegate.getInputProcessor().getOIA() != null) {
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
}
this.kri_flag = false;
this.unlock_sys_pending = false;
}
if (this.ps != null) {
this.ps.dispatchEvent(new haus.nightmare.lib3270j.ecl.ECLPSEvent(this.ps, haus.nightmare.lib3270j.ecl.ECLPSEvent.EVENT_KEY_UNLOCKED));
}
} else {
// Contention resolution is not active
if (this.unlock_pending && !isRcvdRead()) {
if (this.ps != null) {
this.ps.unlockKeyboard(7);
} else if (delegate != null && delegate.getInputProcessor() != null) {
delegate.getInputProcessor().setKeyboardLocked(false);
}
if (this.autoSysUnlock) {
if (this.ps != null) {
this.ps.unlockKeyboard(8);
} else if (delegate != null && delegate.getInputProcessor() != null) {
delegate.getInputProcessor().setKeyboardLocked(false);
if (delegate.getInputProcessor().getOIA() != null) {
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
this.unlock_sys_pending = false;
}
this.unlock_pending = false;
}
if (this.unlock_sys_pending && !isRcvdRead()) {
if (this.ps != null) {
this.ps.unlockKeyboard(8);
} else if (delegate != null && delegate.getInputProcessor() != null) {
delegate.getInputProcessor().setKeyboardLocked(false);
if (delegate.getInputProcessor().getOIA() != null) {
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
this.unlock_sys_pending = false;
}
if (this.autoSysUnlock && !isRcvdRead()) {
boolean sysLocked = false;
if (this.ps != null && this.ps.islocked_SYSLOCK()) {
sysLocked = true;
} else if (delegate != null && delegate.getInputProcessor() != null) {
if (delegate.getInputProcessor().isKeyboardLocked() ||
(delegate.getInputProcessor().getOIA() != null && delegate.getInputProcessor().getOIA().isXSystem())) {
sysLocked = true;
}
}
if (sysLocked) {
if (this.ps != null) {
this.ps.unlockKeyboard(8);
}
if (delegate != null && delegate.getInputProcessor() != null) {
delegate.getInputProcessor().setKeyboardLocked(false);
if (delegate.getInputProcessor().getOIA() != null) {
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
this.unlock_sys_pending = false;
}
}
}
this.rcvdRead = false;
if (delegate != null) delegate.setRcvdRead(false);
this.unlock_pending = false;
this.sdi_flag = false;
this.kri_flag = false;
} }
public int receiveData(short[] sArray, int off, int len) { public int receiveData(short[] sArray, int off, int len) {
@@ -233,6 +374,10 @@ public class DS3270 {
public void processWCC(short wcc) { public void processWCC(short wcc) {
delegate.processWCC(wcc); delegate.processWCC(wcc);
if ((wcc & WCC_RESTORE) > 0) {
this.unlock_pending = true;
this.unlock_sys_pending = true;
}
} }
public void processSBA(int baddr) { public void processSBA(int baddr) {
@@ -0,0 +1,371 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLConnection;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.ecl.ECLPSEvent;
import haus.nightmare.lib3270j.ecl.ECLPSListener;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import haus.nightmare.lib3270j.tn3270.DS3270;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Phase 10 Test Suite: DataStream Protocol Hardening & AUTO_SYS_UNLOCK Parity.
*
* Verifies:
* 10.1 WCC Keyboard Restore Logic (intermediate writes without restore bit keep keyboard locked).
* 10.2 AUTO_SYS_UNLOCK parity across ConnectionConfig, ECLSession, ECLConnection, DS3270, and TelnetFSM.
* 10.3 Contention Resolution negotiation, SDI/KRI tracking, and ECLPSEvent.EVENT_KEY_UNLOCKED dispatch.
*/
public class Phase10ProtocolHardeningTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private DataStreamProcessor processor;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
inputProcessor = new InputProcessor(screen, translator, null);
processor = new DataStreamProcessor(screen, translator);
processor.setInputProcessor(inputProcessor);
}
// =========================================================================
// 10.1 WCC Keyboard Restore Logic
// =========================================================================
@Test
@DisplayName("10.1: Write without WCC restore bit keeps keyboard locked")
public void testWriteWithoutWccRestoreKeepsKeyboardLocked() {
// Initially lock the keyboard
inputProcessor.setKeyboardLocked(true);
assertTrue(inputProcessor.isKeyboardLocked());
// Construct 3270 Write Command (0x01) with WCC = 0xC0 (Reset MDT only, no keyboard restore 0x02)
byte[] record = new byte[] {
(byte) DS3270Constants.CMD_WRITE,
(byte) 0xC0, // WCC: bit 6 set (reset MDT), bit 1 (restore) NOT set
(byte) 0x11, 0x40, 0x40, // SBA to 0
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
};
processor.processRecord(record, 0, record.length, false);
// Keyboard MUST remain locked because WCC restore bit 0x02 was not set
assertTrue(inputProcessor.isKeyboardLocked(),
"Keyboard must remain locked when WCC does not have restore bit (0x02) set");
}
@Test
@DisplayName("10.1: Write with WCC restore bit unlocks keyboard")
public void testWriteWithWccRestoreUnlocksKeyboard() {
// Initially lock the keyboard
inputProcessor.setKeyboardLocked(true);
assertTrue(inputProcessor.isKeyboardLocked());
// Construct 3270 Write Command (0x01) with WCC = 0xC2 (Reset MDT + Keyboard Restore bit 0x02)
byte[] record = new byte[] {
(byte) DS3270Constants.CMD_WRITE,
(byte) 0xC2, // WCC: restore bit (0x02) set
(byte) 0x11, 0x40, 0x40, // SBA to 0
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
};
processor.processRecord(record, 0, record.length, false);
// Keyboard MUST now be unlocked
assertFalse(inputProcessor.isKeyboardLocked(),
"Keyboard must be unlocked when WCC has restore bit (0x02) set");
}
@Test
@DisplayName("10.1: Intermediate writes without restore bit maintain keyboard lock across sequence")
public void testIntermediateWritesKeepKeyboardLockedUntilFinalRestore() {
inputProcessor.setKeyboardLocked(true);
assertTrue(inputProcessor.isKeyboardLocked());
// First intermediate write: WCC = 0x00 (no restore)
byte[] write1 = new byte[] {
(byte) DS3270Constants.CMD_WRITE,
(byte) 0x00,
(byte) 0x11, 0x40, 0x40,
(byte) 0xC1
};
processor.processRecord(write1, 0, write1.length, false);
assertTrue(inputProcessor.isKeyboardLocked(), "Write 1 without restore bit must leave keyboard locked");
// Second intermediate write: Erase / Write with WCC = 0x40 (Reset, no restore)
byte[] write2 = new byte[] {
(byte) DS3270Constants.CMD_ERASE_WRITE,
(byte) 0x40,
(byte) 0x11, 0x40, 0x50,
(byte) 0xC2
};
processor.processRecord(write2, 0, write2.length, false);
assertTrue(inputProcessor.isKeyboardLocked(), "Write 2 without restore bit must leave keyboard locked");
// Final write: WCC = 0x42 (Reset + Restore)
byte[] write3 = new byte[] {
(byte) DS3270Constants.CMD_WRITE,
(byte) 0x42,
(byte) 0x11, 0x40, 0x60,
(byte) 0xC3
};
processor.processRecord(write3, 0, write3.length, false);
assertFalse(inputProcessor.isKeyboardLocked(), "Final write with restore bit must unlock keyboard");
}
// =========================================================================
// 10.2 AUTO_SYS_UNLOCK Parity
// =========================================================================
@Test
@DisplayName("10.2: autoSysUnlock configuration property propagation across ECL components")
public void testAutoSysUnlockConfigurationPropagation() {
// Default ConnectionConfig should have autoSysUnlock == true
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
assertTrue(config.isAutoSysUnlock(), "Default autoSysUnlock in ConnectionConfig must be true");
config.setAutoSysUnlock(false);
assertFalse(config.isAutoSysUnlock());
// Telnet3270Client propagation
Telnet3270Client client = new Telnet3270Client(config);
assertFalse(client.isAutoSysUnlock());
// ECLSession propagation
ECLSession session = new ECLSession(client);
assertFalse(session.isAutoSysUnlock());
assertFalse(session.getConnection().isAutoSysUnlock());
// Modify via ECLSession
session.setAutoSysUnlock(true);
assertTrue(session.isAutoSysUnlock());
assertTrue(client.isAutoSysUnlock());
assertTrue(config.isAutoSysUnlock());
// Test ECLSession Properties parsing
Properties props = new Properties();
props.setProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK, "false");
props.setProperty(ECLSession.SESSION_HOST, "mainframe.org");
ECLSession propsSession = new ECLSession(props);
assertFalse(propsSession.isAutoSysUnlock());
assertFalse(propsSession.getConnection().isAutoSysUnlock());
// ECLConnection standalone properties
ECLConnection standaloneConn = new ECLConnection(props);
assertFalse(standaloneConn.isAutoSysUnlock());
}
@Test
@DisplayName("10.2: DS3270 endOfRecord unlocks keyboard and clears OIA when autoSysUnlock is true and in X SYSTEM")
public void testDs3270AutoSysUnlockOnEor() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
config.setAutoSysUnlock(true);
Telnet3270Client client = new Telnet3270Client(config);
ECLSession session = new ECLSession(client);
ECLPS ps = session.GetPS();
ECLOIA oia = session.GetOIA();
DS3270 ds = new DS3270(session, ps);
assertTrue(ds.isAutoSysUnlock());
// Set connected state so OIA does not treat it as communication check
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
// Put session in X SYSTEM lock
ps.lockKeyboard(8); // reason 8: SYSLOCK
assertTrue(ps.islocked_SYSLOCK());
assertTrue(client.getInputProcessor().isKeyboardLocked());
assertTrue(oia.isXSystem());
// Trigger endOfRecord() with autoSysUnlock=true
ds.endOfRecord();
// Keyboard must be auto-unlocked and OIA cleared
assertFalse(client.getInputProcessor().isKeyboardLocked(),
"Keyboard should be unlocked on EOR when autoSysUnlock is true");
assertFalse(oia.isXSystem(),
"OIA X SYSTEM lock should be cleared on EOR when autoSysUnlock is true");
}
@Test
@DisplayName("10.2: DS3270 endOfRecord leaves keyboard locked when autoSysUnlock is false and in X SYSTEM")
public void testDs3270AutoSysUnlockDisabledKeepsSystemLock() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
config.setAutoSysUnlock(false);
Telnet3270Client client = new Telnet3270Client(config);
ECLSession session = new ECLSession(client);
ECLPS ps = session.GetPS();
DS3270 ds = new DS3270(session, ps);
assertFalse(ds.isAutoSysUnlock());
// Put session in X SYSTEM lock
ps.lockKeyboard(8);
assertTrue(ps.islocked_SYSLOCK());
assertTrue(client.getInputProcessor().isKeyboardLocked());
// Trigger endOfRecord() with autoSysUnlock=false
ds.endOfRecord();
// Keyboard MUST remain locked
assertTrue(client.getInputProcessor().isKeyboardLocked(),
"Keyboard must remain locked on EOR when autoSysUnlock is false");
}
// =========================================================================
// 10.3 Contention Resolution & Pre-Data State Transitions
// =========================================================================
@Test
@DisplayName("10.3: Contention Resolution negotiation flag synchronization with ECLConnection")
public void testContentionResolutionNegotiationSync() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
Telnet3270Client client = new Telnet3270Client(config);
ECLSession session = new ECLSession(client);
ECLConnection conn = session.getConnection();
TelnetFSM fsm = client.getTelnetFSM();
assertNotNull(fsm);
assertTrue(fsm.isNegotiateContentionResolution(),
"Default negotiateContentionResolution should be true");
// Test setter on FSM and reflection in ECLConnection
fsm.setContentionResolutionNegotiated(true);
assertTrue(fsm.isContentionResolutionNegotiated(),
"Contention resolution should be negotiated in FSM");
assertTrue(conn.getContentionResolution(),
"ECLConnection should reflect negotiated contention resolution");
assertTrue(session.getContentionResolution(),
"ECLSession should reflect negotiated contention resolution");
}
@Test
@DisplayName("10.3: Contention Resolution SDI flag restores keyboard and fires EVENT_KEY_UNLOCKED on EOR")
public void testContentionResolutionWithSdiRestoresKeyboardAndFiresEvent() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
Telnet3270Client client = new Telnet3270Client(config);
ECLSession session = new ECLSession(client);
ECLPS ps = session.GetPS();
DS3270 ds = new DS3270(session, ps);
// Enable Contention Resolution on session
session.setContentionResolution(true);
assertTrue(session.getContentionResolution());
// Lock keyboard
client.getInputProcessor().setKeyboardLocked(true);
assertTrue(client.getInputProcessor().isKeyboardLocked());
// Register listener for EVENT_KEY_UNLOCKED
AtomicInteger eventTypeReceived = new AtomicInteger(-1);
AtomicBoolean unlockedEventFired = new AtomicBoolean(false);
ps.RegisterPSEvent(new ECLPSListener() {
@Override
public void PSNotifyEvent(ECLPSEvent event) {
eventTypeReceived.set(event.getEventType());
unlockedEventFired.set(true);
}
});
// Inbound TN3270E header with SDI bit set:
// s2 request flag has bit 0 (0x01) set -> Send Data Indicator (SDI)
short s1 = 0;
short s2 = DS3270.request_bit_SDI; // 1
short s3 = 0;
ds.receiveHeaderData(s1, s2, s3, 0);
assertTrue(ds.isSdi_flag(), "SDI flag should be tracked from TN3270E header");
// Trigger endOfRecord()
ds.endOfRecord();
// Keyboard should be unlocked because SDI was received
assertFalse(client.getInputProcessor().isKeyboardLocked(),
"Keyboard should be restored on EOR when CR is active and SDI is set");
assertFalse(ds.isSdi_flag(), "SDI flag should be reset after EOR");
assertTrue(unlockedEventFired.get(), "ECLPSEvent must be dispatched on keyboard unlock");
assertEquals(ECLPSEvent.EVENT_KEY_UNLOCKED, eventTypeReceived.get(),
"Dispatched event must match EVENT_KEY_UNLOCKED");
}
@Test
@DisplayName("10.3: Contention Resolution WITHOUT SDI flag keeps keyboard locked on EOR")
public void testContentionResolutionWithoutSdiKeepsKeyboardLocked() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
Telnet3270Client client = new Telnet3270Client(config);
ECLSession session = new ECLSession(client);
ECLPS ps = session.GetPS();
DS3270 ds = new DS3270(session, ps);
session.setContentionResolution(true);
assertTrue(session.getContentionResolution());
// Initially lock keyboard
client.getInputProcessor().setKeyboardLocked(true);
// Inbound TN3270E header WITHOUT SDI flag (s2 = 0)
ds.receiveHeaderData((short) 0, (short) 0, (short) 0, 0);
assertFalse(ds.isSdi_flag());
// Trigger EOR
ds.endOfRecord();
// Keyboard MUST stay locked because host did not grant Send Data Indicator (turn)
assertTrue(client.getInputProcessor().isKeyboardLocked(),
"Keyboard must remain locked on EOR when Contention Resolution is active and SDI is not set");
}
@Test
@DisplayName("10.3: Contention Resolution KRI flag resets system lock pending on EOR")
public void testContentionResolutionKriResetsSystemLock() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
Telnet3270Client client = new Telnet3270Client(config);
ECLSession session = new ECLSession(client);
ECLPS ps = session.GetPS();
DS3270 ds = new DS3270(session, ps);
session.setContentionResolution(true);
// Put in system lock
ps.lockKeyboard(8);
assertTrue(ps.islocked_SYSLOCK());
// Receive header with KRI flag (bit 1 = 0x02) and SDI flag (bit 0 = 0x01)
short s2 = (short) (DS3270.request_bit_SDI | DS3270.request_bit_KRI);
ds.receiveHeaderData((short) 0, s2, (short) 0, 0);
assertTrue(ds.isSdi_flag());
assertTrue(ds.isKri_flag());
ds.endOfRecord();
// Keyboard restored and system lock reset
assertFalse(client.getInputProcessor().isKeyboardLocked());
assertFalse(ps.islocked_SYSLOCK());
assertFalse(ds.isKri_flag());
}
}
@@ -0,0 +1,397 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.ecl.ECLConnection;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.telnet.TelnetConnection;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
/**
* Phase 11 Test Suite: Enterprise Connection Resilience & "Dirty" Connection Handling.
*
* Verifies:
* 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine (NOP and TIMING-MARK, activity reset, shutdown).
* 11.2 Automatic Reconnection with Exponential Backoff (unexpected disconnect, state transitions, retry loop, recovery, exhaustion).
* 11.3 Transport-Level Socket Tuning & Options (soTimeout, extended socket options, CLI parsing).
* 11.4 IBM HoD ECL Compatibility Facade (SESSION_KEEPALIVE, keepAliveTimeout, SESSION_AUTORECONNECT, ECLConnection methods).
*/
public class Phase11ConnectionResilienceTest {
// =========================================================================
// 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine
// =========================================================================
@Test
@DisplayName("11.1: Keep-Alive transmits Telnet IAC NOP (0xFF 0xF1) when idle")
public void testKeepAliveTransmitsIacNop() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
int port = server.getLocalPort();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
config.setKeepAliveEnabled(true);
config.setKeepAliveIntervalSeconds(1);
config.setKeepAliveType("NOP");
Telnet3270Client client = new Telnet3270Client(config);
CountDownLatch clientConnected = new CountDownLatch(1);
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
Thread serverThread = new Thread(() -> {
try {
Socket s = server.accept();
serverAccepted.set(s);
clientConnected.countDown();
} catch (Exception ignored) {}
});
serverThread.start();
client.connect();
assertTrue(clientConnected.await(5, TimeUnit.SECONDS), "Server did not accept client connection");
Socket s = serverAccepted.get();
assertNotNull(s);
InputStream in = s.getInputStream();
// Explicitly trigger or wait for keepalive heartbeat
client.getConnection().sendKeepAliveHeartbeat();
byte[] buf = new byte[2];
int read = in.read(buf);
assertEquals(2, read);
assertEquals((byte) TelnetConstants.IAC, buf[0]);
assertEquals((byte) TelnetConstants.NOP, buf[1]);
client.disconnect();
}
}
@Test
@DisplayName("11.1: Keep-Alive transmits Telnet IAC DO TIMING-MARK (0xFF 0xFD 0x06)")
public void testKeepAliveTransmitsTimingMark() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
int port = server.getLocalPort();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
config.setKeepAliveEnabled(true);
config.setKeepAliveIntervalSeconds(1);
config.setKeepAliveType("TIMING-MARK");
Telnet3270Client client = new Telnet3270Client(config);
CountDownLatch clientConnected = new CountDownLatch(1);
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
Thread serverThread = new Thread(() -> {
try {
Socket s = server.accept();
serverAccepted.set(s);
clientConnected.countDown();
} catch (Exception ignored) {}
});
serverThread.start();
client.connect();
assertTrue(clientConnected.await(5, TimeUnit.SECONDS));
Socket s = serverAccepted.get();
assertNotNull(s);
InputStream in = s.getInputStream();
client.getConnection().sendKeepAliveHeartbeat();
byte[] buf = new byte[3];
int read = in.read(buf);
assertEquals(3, read);
assertEquals((byte) TelnetConstants.IAC, buf[0]);
assertEquals((byte) TelnetConstants.DO, buf[1]);
assertEquals((byte) TelnetConstants.TELOPT_TM, buf[2]);
client.disconnect();
}
}
@Test
@DisplayName("11.1: Transmission resets last activity timestamp")
public void testTransmissionResetsActivityTimestamp() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
int port = server.getLocalPort();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
Telnet3270Client client = new Telnet3270Client(config);
new Thread(() -> {
try {
server.accept();
} catch (Exception ignored) {}
}).start();
client.connect();
TelnetConnection conn = client.getConnection();
long initialTime = conn.getLastActivityTime();
Thread.sleep(15);
conn.sendRaw(new byte[] { 0x01, 0x02 });
long updatedTime = conn.getLastActivityTime();
assertTrue(updatedTime >= initialTime + 10, "Activity timestamp was not updated on sendRaw");
client.disconnect();
}
}
// =========================================================================
// 11.2 Automatic Reconnection with Exponential Backoff
// =========================================================================
@Test
@DisplayName("11.2: Unexpected EOF transitions to RECONNECTING state when autoReconnect is true")
public void testUnexpectedDisconnectTransitionsToReconnecting() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
int port = server.getLocalPort();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
config.setAutoReconnect(true);
config.setReconnectMaxRetries(3);
Telnet3270Client client = new Telnet3270Client(config);
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
CountDownLatch reconnectingLatch = new CountDownLatch(1);
client.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
stateHistory.add(newState);
if (newState == ConnectionState.RECONNECTING) {
reconnectingLatch.countDown();
}
}
@Override public void onConnectionError(String message) {}
});
AtomicReference<Socket> acceptedSocket = new AtomicReference<>();
Thread serverThread = new Thread(() -> {
try {
Socket s = server.accept();
acceptedSocket.set(s);
} catch (Exception ignored) {}
});
serverThread.start();
client.connect();
serverThread.join(2000);
assertNotNull(acceptedSocket.get());
// Abruptly sever the connection from the host side (EOF)
acceptedSocket.get().close();
assertTrue(reconnectingLatch.await(4, TimeUnit.SECONDS), "Client did not enter RECONNECTING state on unexpected EOF");
assertTrue(stateHistory.contains(ConnectionState.RECONNECTING), "State history must include RECONNECTING");
assertTrue(client.isReconnecting());
// Check OIA message updated
assertNotNull(client.getOIA());
client.disconnect();
assertFalse(client.isReconnecting());
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
}
}
@Test
@DisplayName("11.2: Intentional client.disconnect() transitions directly to NOT_CONNECTED without auto-reconnect")
public void testIntentionalDisconnectDoesNotTriggerAutoReconnect() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
int port = server.getLocalPort();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
config.setAutoReconnect(true);
config.setReconnectMaxRetries(3);
Telnet3270Client client = new Telnet3270Client(config);
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
client.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
stateHistory.add(newState);
}
@Override public void onConnectionError(String message) {}
});
new Thread(() -> {
try {
server.accept();
} catch (Exception ignored) {}
}).start();
client.connect();
Thread.sleep(100);
// Explicit client disconnect
client.disconnect();
assertFalse(stateHistory.contains(ConnectionState.RECONNECTING),
"Intentional disconnect should never transition to RECONNECTING");
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
}
}
@Test
@DisplayName("11.2: Auto-reconnect retry loop successfully re-establishes connection")
public void testAutoReconnectSuccessfulReconnection() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
int port = server.getLocalPort();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
config.setAutoReconnect(true);
config.setReconnectMaxRetries(3);
Telnet3270Client client = new Telnet3270Client(config);
CountDownLatch reconnectedLatch = new CountDownLatch(1);
AtomicReference<Socket> firstConn = new AtomicReference<>();
AtomicReference<Socket> secondConn = new AtomicReference<>();
Thread serverThread = new Thread(() -> {
try {
Socket s1 = server.accept();
firstConn.set(s1);
// Close first socket to trigger reconnect
Thread.sleep(50);
s1.close();
// Accept the reconnection attempt
Socket s2 = server.accept();
secondConn.set(s2);
reconnectedLatch.countDown();
} catch (Exception ignored) {}
});
serverThread.start();
client.connect();
// Wait for reconnection to succeed
assertTrue(reconnectedLatch.await(8, TimeUnit.SECONDS), "Server did not receive reconnection attempt");
assertNotNull(secondConn.get());
// Wait for client to complete handshake / notify connected
Thread.sleep(200);
assertTrue(client.isConnected());
client.disconnect();
}
}
// =========================================================================
// 11.3 Transport-Level Socket Tuning & Options
// =========================================================================
@Test
@DisplayName("11.3: Configurable SO_TIMEOUT is applied to TCP socket")
public void testSoTimeoutApplied() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
int port = server.getLocalPort();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
config.setSoTimeoutMs(750);
Telnet3270Client client = new Telnet3270Client(config);
new Thread(() -> {
try { server.accept(); } catch (Exception ignored) {}
}).start();
client.connect();
assertNotNull(client.getConnection());
// Client socket should have SO_TIMEOUT applied
assertEquals(750, config.getSoTimeoutMs());
client.disconnect();
}
}
@Test
@DisplayName("11.3: parseHostString parses keepalive and autoreconnect flags")
public void testParseHostStringFlags() {
ConnectionConfig c1 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --keepalive --autoreconnect", 23, TerminalModel.IBM_3279_4);
assertTrue(c1.isKeepAliveEnabled());
assertTrue(c1.isAutoReconnect());
assertEquals("mainframe.corp.com", c1.getHost());
assertEquals(23, c1.getPort());
ConnectionConfig c2 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --no-keepalive --no-autoreconnect", 23, TerminalModel.IBM_3279_4);
assertFalse(c2.isKeepAliveEnabled());
assertFalse(c2.isAutoReconnect());
}
// =========================================================================
// 11.4 IBM HoD ECL Compatibility Facade
// =========================================================================
@Test
@DisplayName("11.4: ECLSession parses HoD properties for Keep-Alive and Auto-Reconnect")
public void testEclSessionPropertiesParsing() {
Properties props = new Properties();
props.setProperty(ECLSession.SESSION_HOST, "mvs01.test.net");
props.setProperty(ECLSession.SESSION_PORT, "23");
props.setProperty(ECLSession.SESSION_KEEPALIVE, "true");
props.setProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT, "180");
props.setProperty(ECLSession.KEY_KEEPALIVE_TYPE, "TIMING-MARK");
props.setProperty(ECLSession.SESSION_AUTORECONNECT, "true");
props.setProperty(ECLSession.SESSION_RECONNECT_RETRIES, "8");
ECLSession session = new ECLSession(props);
assertTrue(session.isKeepAlive());
assertEquals(180, session.getKeepAliveTimeout());
assertTrue(session.isAutoReconnect());
ECLConnection conn = session.getConnection();
assertNotNull(conn);
assertTrue(conn.isKeepAlive());
assertEquals(180, conn.getKeepAliveTimeout());
assertEquals("TIMING-MARK", conn.getKeepAliveType());
assertTrue(conn.isAutoReconnect());
assertEquals(8, conn.getMaxRetry());
}
@Test
@DisplayName("11.4: ECLConnection getters and setters synchronize with underlying client")
public void testEclConnectionGettersSetters() {
ConnectionConfig cfg = new ConnectionConfig("localhost", 23);
Telnet3270Client client = new Telnet3270Client(cfg);
ECLSession session = new ECLSession(client);
ECLConnection conn = session.getConnection();
conn.setKeepAlive(false);
assertFalse(conn.isKeepAlive());
assertFalse(client.getConfig().isKeepAliveEnabled());
conn.setKeepAliveTimeout(60);
assertEquals(60, conn.getKeepAliveTimeout());
assertEquals(60, client.getConfig().getKeepAliveIntervalSeconds());
conn.setKeepAliveType("TIMING-MARK");
assertEquals("TIMING-MARK", conn.getKeepAliveType());
assertEquals("TIMING-MARK", client.getConfig().getKeepAliveType());
conn.setAutoReconnect(true);
assertTrue(conn.isAutoReconnect());
assertTrue(client.getConfig().isAutoReconnect());
conn.setMaxRetry(10);
assertEquals(10, conn.getMaxRetry());
assertEquals(10, client.getConfig().getReconnectMaxRetries());
}
}
@@ -0,0 +1,89 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 13.2: Interactive Entry Assist Engine.
* Verifies interactive word wrap in DOC mode and audible bell alert
* when typing reaches the configured bell column.
*/
public class InteractiveEntryAssistTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
}
@Test
public void testAudibleBellTriggersAtConfiguredBellColumn() {
input.setBellEnabled(true);
input.setBellColumn(74); // 0-based column 74 = column 75
AtomicInteger bellCount = new AtomicInteger(0);
input.setBellListener(bellCount::incrementAndGet);
screen.setCursorPosition(0, 72);
input.typeCharacter('A'); // col 73
assertEquals(0, bellCount.get());
input.typeCharacter('B'); // col 74 -> triggers bell
assertEquals(1, bellCount.get());
input.typeCharacter('C'); // col 75 -> remains 1 on current row
assertEquals(1, bellCount.get());
// Moving to next row and typing across bell column triggers bell again
screen.setCursorPosition(1, 73);
input.typeCharacter('X'); // advances to col 74 on row 1
assertEquals(2, bellCount.get());
}
@Test
public void testDocModeWordWrapMovesPartialWordToNextRow() {
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
screen.setLeftMargin(0); // Col 0
screen.setRightMargin(15); // Col 15
assertTrue(screen.isEntryAssistDOCmode());
assertTrue(screen.isEntryAssistWordWrap());
// Type "HELLO " (6 chars) starting at col 0
screen.setCursorPosition(0, 0);
for (char c : "HELLO ".toCharArray()) {
input.typeCharacter(c);
}
assertEquals(0, screen.getCursorRow());
assertEquals(6, screen.getCursorCol());
// Move cursor near right margin: Row 0, Col 13
screen.setCursorPosition(0, 13);
input.typeCharacter('P'); // col 14
input.typeCharacter('A'); // col 15 (at right margin)
input.typeCharacter('R'); // crosses right margin -> triggers word wrap
// After word wrap, the word "PAR" should be moved to Row 1, left margin (Col 0)
assertEquals(1, screen.getCursorRow());
assertTrue(screen.getCursorCol() >= 3);
// Verify content on Row 1
assertEquals('P', (char) screen.getCell(80).ucs4);
assertEquals('A', (char) screen.getCell(81).ucs4);
assertEquals('R', (char) screen.getCell(82).ucs4);
}
}
@@ -0,0 +1,319 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.jupiter.api.Assertions.*;
/**
* Phase 12 Test Suite: Reactive Synchronization & Thread-Safety Hardening.
*
* Verifies:
* 12.1 Condition-Based Synchronization for ECL Automation Calls:
* - Sub-millisecond reactive wakeup for waitForScreen(ECLScreenDesc).
* - Reactive wakeup for waitForCursor(row, col).
* - Reactive wakeup for waitForString(text) and waitWhileScreen(ECLScreenDesc).
* - Clean timeout handling without CPU busy-wait spinning.
* - Graceful thread interruption handling.
* - ECLOIA condition-based reactive waitForInput and waitForTransition.
* - ScreenBuffer listener dispatch and cursor change tracking.
* - Concurrency safety with multiple waiters.
*/
public class Phase12SynchronizationTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private ECLPS ps;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
inputProcessor = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, inputProcessor, translator);
}
@Test
@DisplayName("12.1: waitForScreen(ECLScreenDesc) wakes up reactively upon screen update")
public void testReactiveWaitForScreenDesc() throws Exception {
ECLScreenDesc desc = new ECLScreenDesc();
desc.addString("TSO/E LOGON", 5, 10);
AtomicBoolean result = new AtomicBoolean(false);
AtomicLong durationMs = new AtomicLong(-1);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(1);
Thread waiter = new Thread(() -> {
started.countDown();
long t0 = System.currentTimeMillis();
boolean matched = ps.waitForScreen(desc, 5000);
durationMs.set(System.currentTimeMillis() - t0);
result.set(matched);
done.countDown();
});
waiter.start();
assertTrue(started.await(1, TimeUnit.SECONDS));
Thread.sleep(50); // Give waiter time to block on condition
// Write matching text at 1-based (5, 10) -> 0-based row 4 col 9
screen.setText("TSO/E LOGON", 4, 9);
screen.notifyScreenUpdate();
assertTrue(done.await(2, TimeUnit.SECONDS));
assertTrue(result.get(), "waitForScreen should match");
assertTrue(durationMs.get() < 1500, "Should unblock reactively well before 5000ms timeout (took " + durationMs.get() + "ms)");
}
@Test
@DisplayName("12.1: waitForCursor(row, col) wakes up reactively upon cursor position change")
public void testReactiveWaitForCursor() throws Exception {
screen.setCursorPosition(0, 0);
AtomicBoolean result = new AtomicBoolean(false);
AtomicLong durationMs = new AtomicLong(-1);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(1);
Thread waiter = new Thread(() -> {
started.countDown();
long t0 = System.currentTimeMillis();
boolean matched = ps.waitForCursor(12, 34, 5000);
durationMs.set(System.currentTimeMillis() - t0);
result.set(matched);
done.countDown();
});
waiter.start();
assertTrue(started.await(1, TimeUnit.SECONDS));
Thread.sleep(50);
// Move cursor
screen.setCursorPosition(12, 34);
assertTrue(done.await(2, TimeUnit.SECONDS));
assertTrue(result.get(), "waitForCursor should succeed");
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon cursor move (took " + durationMs.get() + "ms)");
}
@Test
@DisplayName("12.1: waitForString(text) wakes up reactively upon string appearance")
public void testReactiveWaitForString() throws Exception {
AtomicBoolean result = new AtomicBoolean(false);
AtomicLong durationMs = new AtomicLong(-1);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(1);
Thread waiter = new Thread(() -> {
started.countDown();
long t0 = System.currentTimeMillis();
boolean matched = ps.waitForString("COMMAND ===>", 5000);
durationMs.set(System.currentTimeMillis() - t0);
result.set(matched);
done.countDown();
});
waiter.start();
assertTrue(started.await(1, TimeUnit.SECONDS));
Thread.sleep(50);
screen.setText("COMMAND ===>", 20, 2);
screen.notifyScreenUpdate();
assertTrue(done.await(2, TimeUnit.SECONDS));
assertTrue(result.get(), "waitForString should find the text");
assertTrue(durationMs.get() < 1500, "Should unblock reactively (took " + durationMs.get() + "ms)");
}
@Test
@DisplayName("12.1: waitWhileScreen(desc) wakes up reactively when condition ceases to match")
public void testReactiveWaitWhileScreen() throws Exception {
// Place initial text
screen.setText("HOLDING", 0, 0);
ECLScreenDesc desc = new ECLScreenDesc();
desc.addString("HOLDING");
AtomicBoolean result = new AtomicBoolean(false);
AtomicLong durationMs = new AtomicLong(-1);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(1);
Thread waiter = new Thread(() -> {
started.countDown();
long t0 = System.currentTimeMillis();
boolean finished = ps.waitWhileScreen(desc, 5000);
durationMs.set(System.currentTimeMillis() - t0);
result.set(finished);
done.countDown();
});
waiter.start();
assertTrue(started.await(1, TimeUnit.SECONDS));
Thread.sleep(50);
// Erase screen so "HOLDING" is gone
screen.erase(false);
screen.notifyScreenUpdate();
assertTrue(done.await(2, TimeUnit.SECONDS));
assertTrue(result.get(), "waitWhileScreen should return true once condition is gone");
assertTrue(durationMs.get() < 1500, "Should unblock reactively when text erased (took " + durationMs.get() + "ms)");
}
@Test
@DisplayName("12.1: Clean timeout without false match or spinning")
public void testCleanTimeout() {
ECLScreenDesc desc = new ECLScreenDesc();
desc.addString("NON_EXISTENT_STRING");
long t0 = System.currentTimeMillis();
boolean matched = ps.waitForScreen(desc, 150);
long elapsed = System.currentTimeMillis() - t0;
assertFalse(matched, "Should not match non-existent text");
assertTrue(elapsed >= 100, "Should have waited for the full timeout window (elapsed=" + elapsed + "ms)");
}
@Test
@DisplayName("12.1: Graceful thread interruption during wait")
public void testThreadInterruption() throws Exception {
ECLScreenDesc desc = new ECLScreenDesc();
desc.addString("NEVER_APPEARS");
AtomicBoolean wasInterrupted = new AtomicBoolean(false);
AtomicBoolean result = new AtomicBoolean(true);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(1);
Thread waiter = new Thread(() -> {
started.countDown();
boolean r = ps.waitForScreen(desc, 10000);
result.set(r);
if (Thread.currentThread().isInterrupted()) {
wasInterrupted.set(true);
}
done.countDown();
});
waiter.start();
assertTrue(started.await(1, TimeUnit.SECONDS));
Thread.sleep(50);
waiter.interrupt();
assertTrue(done.await(2, TimeUnit.SECONDS));
assertFalse(result.get(), "Interrupted wait should return false");
assertTrue(wasInterrupted.get(), "Thread interrupt flag should be preserved");
}
@Test
@DisplayName("12.1: ECLOIA reactive waitForInput and waitForTransition")
public void testECLOIAReactiveWait() throws Exception {
ECLOIA oia = new ECLOIA(screen, inputProcessor, null);
oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEMWAIT);
AtomicBoolean inputReady = new AtomicBoolean(false);
AtomicLong durationMs = new AtomicLong(-1);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(1);
Thread waiter = new Thread(() -> {
started.countDown();
long t0 = System.currentTimeMillis();
boolean ready = oia.waitForInput(5000);
durationMs.set(System.currentTimeMillis() - t0);
inputReady.set(ready);
done.countDown();
});
waiter.start();
assertTrue(started.await(1, TimeUnit.SECONDS));
Thread.sleep(50);
// Unlock OIA
oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED);
assertTrue(done.await(2, TimeUnit.SECONDS));
assertTrue(inputReady.get(), "waitForInput should succeed");
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon OIA unlock (took " + durationMs.get() + "ms)");
}
@Test
@DisplayName("12.1: ScreenBuffer listener dispatch and cursor movement tracking")
public void testScreenBufferListenerDispatch() {
AtomicBoolean screenUpdated = new AtomicBoolean(false);
AtomicInteger cursorOld = new AtomicInteger(-1);
AtomicInteger cursorNew = new AtomicInteger(-1);
ScreenUpdateListener listener = new ScreenUpdateListener() {
@Override
public void onScreenUpdated() {
screenUpdated.set(true);
}
@Override
public void onCursorMoved(int oldAddress, int newAddress) {
cursorOld.set(oldAddress);
cursorNew.set(newAddress);
}
};
screen.addUpdateListener(listener);
screen.notifyScreenUpdate();
assertTrue(screenUpdated.get(), "Listener should receive onScreenUpdated");
screen.setCursorAddress(123);
assertEquals(0, cursorOld.get(), "Old cursor should be 0");
assertEquals(123, cursorNew.get(), "New cursor should be 123");
screen.removeUpdateListener(listener);
screenUpdated.set(false);
screen.notifyScreenUpdate();
assertFalse(screenUpdated.get(), "Removed listener should not receive updates");
}
@Test
@DisplayName("12.1: Concurrent waiters on the same ScreenBuffer")
public void testConcurrentWaiters() throws Exception {
int numWaiters = 5;
CountDownLatch startLatch = new CountDownLatch(numWaiters);
CountDownLatch doneLatch = new CountDownLatch(numWaiters);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < numWaiters; i++) {
new Thread(() -> {
startLatch.countDown();
boolean ok = ps.waitForString("BATCH_SIGNAL", 5000);
if (ok) {
successCount.incrementAndGet();
}
doneLatch.countDown();
}).start();
}
assertTrue(startLatch.await(2, TimeUnit.SECONDS));
Thread.sleep(50);
screen.setText("BATCH_SIGNAL", 100);
screen.notifyScreenUpdate();
assertTrue(doneLatch.await(3, TimeUnit.SECONDS));
assertEquals(numWaiters, successCount.get(), "All concurrent waiters should unblock reactively");
}
}
@@ -10,9 +10,9 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.awt.Image; import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
import java.awt.Rectangle; import haus.nightmare.lib3270j.graphics.PixelBuffer;
import java.awt.image.BufferedImage; import haus.nightmare.lib3270j.graphics.Rectangle;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -406,11 +406,12 @@ public class Phase5EclEventTest {
} }
// Test with image and rectangle // Test with image and rectangle
Image testImage = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); PixelBuffer testImage = new DefaultPixelBuffer(32, 32);
Rectangle rect = new Rectangle(0, 0, 32, 32); Rectangle rect = new Rectangle(0, 0, 32, 32);
ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect); ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect);
assertSame(testImage, fullGEvent.GetImage()); assertSame(testImage, fullGEvent.GetImage());
assertSame(testImage, fullGEvent.getImage()); assertSame(testImage, fullGEvent.getImage());
assertSame(testImage, fullGEvent.getPixelBuffer());
assertEquals(rect, fullGEvent.GetRectangle()); assertEquals(rect, fullGEvent.GetRectangle());
assertEquals(rect, fullGEvent.getRectangle()); assertEquals(rect, fullGEvent.getRectangle());
@@ -0,0 +1,124 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 13.1: Excel & Tabular Clipboard Paste.
* Verifies tab-delimited text navigation, newline row advancement,
* and boundary truncation when pasteStopAtProtectedLine is set.
*/
public class TabularPasteTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testExcelPasteTabsAndNewlinesAdvanceAcrossFields() {
// Setup 2 rows with 2 unprotected fields each
// Row 0: pos 0 (unprotected), pos 10 (protected), pos 11 (unprotected), pos 30 (protected)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
// Row 1: pos 80 (unprotected), pos 90 (protected), pos 91 (unprotected), pos 110 (protected)
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
screen.setFieldAttribute(90, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setFieldAttribute(91, (byte) FA_PRINTABLE);
screen.setFieldAttribute(110, (byte) (FA_PRINTABLE | FA_PROTECT));
// Position cursor at first field (pos 1)
screen.setCursorAddress(1);
String tabularData = "ABC\tDEF\r\nGHI\tJKL";
int pasted = input.pasteText(tabularData, true, false);
assertEquals(12, pasted);
// Verify Field 1 (Row 0, Col 1-3)
assertEquals("ABC", ps.getString(1, 3));
// Verify Field 2 (Row 0, Col 12-14)
assertEquals("DEF", ps.getString(12, 3));
// Verify Field 3 (Row 1, Col 1-3 -> pos 81)
assertEquals("GHI", ps.getString(81, 3));
// Verify Field 4 (Row 1, Col 12-14 -> pos 92)
assertEquals("JKL", ps.getString(92, 3));
}
@Test
public void testPasteStopAtProtectedBoundaryHaltsPaste() {
// Field 1: pos 0 (unprotected), pos 5 (protected)
// Data area is pos 1..4 (4 characters)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(5, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
// Try to paste 6 characters when only 4 fit
String overflow = "123456";
int pasted = input.pasteText(overflow, false, true);
// Should paste 4 chars and halt at the protected boundary pos 5
assertEquals(4, pasted);
assertEquals("1234", ps.getString(1, 4));
// Field at pos 10 should not be touched
assertEquals(" ", ps.getString(11, 1));
}
@Test
public void testPasteStopAtProtectedLineHaltsAtProtectedRow() {
// Row 0: unprotected field at pos 0
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
// Row 1: entirely protected starting at pos 80
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCursorAddress(1);
String multiRow = "DATA1\r\nDATA2";
int pasted = input.pasteText(multiRow, true, true);
// "DATA1" (5 chars) pasted, then newline sees next line is protected and halts
assertEquals(5, pasted);
assertEquals("DATA1", ps.getString(1, 5));
}
@Test
public void testEclPsPasteFromExcelMethodsAndFlags() {
assertTrue(ps.isEnablePasteFromExcel());
assertFalse(ps.isPasteStopAtProtectedLine());
ps.setEnablePasteFromExcel(false);
assertFalse(ps.isEnablePasteFromExcel());
ps.setPasteStopAtProtectedLine(true);
assertTrue(ps.isPasteStopAtProtectedLine());
// Setup fields
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
ps.pasteFromExcel("HELLO\tWORLD", 0, 1);
assertEquals("HELLO", ps.getString(1, 5));
assertEquals("WORLD", ps.getString(12, 5));
}
}
@@ -0,0 +1,100 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Architectural compliance audit test verifying 100% decoupling of lib3270j from java.awt.
* Ensures headless execution and portability to Android (which lacks java.awt.*).
*/
public class AwtDecouplingAuditTest {
private static final Pattern IMPORT_AWT_PATTERN = Pattern.compile("^\\s*import\\s+java\\.awt\\..*;");
private static final Pattern DIRECT_AWT_USAGE_PATTERN = Pattern.compile("(?<!Class\\.forName\\(\")java\\.awt\\.[A-Za-z0-9_]+");
@Test
@DisplayName("Audit lib3270j/src/main/java for zero java.awt.* imports or references")
public void testZeroAwtDependenciesInMainSource() throws IOException {
File srcMainDir = new File("src/main/java");
if (!srcMainDir.exists()) {
srcMainDir = new File("lib3270j/src/main/java");
}
assertTrue(srcMainDir.exists() && srcMainDir.isDirectory(),
"src/main/java directory must exist at " + srcMainDir.getAbsolutePath());
List<String> violations = new ArrayList<>();
try (Stream<Path> paths = Files.walk(srcMainDir.toPath())) {
List<Path> javaFiles = paths
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.collect(Collectors.toList());
assertFalse(javaFiles.isEmpty(), "Found no java files in " + srcMainDir);
for (Path p : javaFiles) {
List<String> lines = Files.readAllLines(p);
for (int lineNum = 0; lineNum < lines.size(); lineNum++) {
String line = lines.get(lineNum);
String trimmed = line.trim();
// Ignore comment lines
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
continue;
}
if (IMPORT_AWT_PATTERN.matcher(line).find()) {
violations.add(String.format("%s:%d -> %s", p.getFileName(), lineNum + 1, trimmed));
} else if (line.contains("java.awt.") && !line.contains("Class.forName(\"java.awt.") && !line.contains("\"java.awt.")) {
violations.add(String.format("%s:%d [raw type reference] -> %s", p.getFileName(), lineNum + 1, trimmed));
}
}
}
}
if (!violations.isEmpty()) {
fail("Found java.awt dependencies in lib3270j main source:\n" + String.join("\n", violations));
}
}
@Test
@DisplayName("Verify PixelBuffer and software rasterizer functions without AWT")
public void testPixelBufferPureJavaOperations() {
PixelBuffer pb = new DefaultPixelBuffer(100, 100);
assertEquals(100, pb.getWidth());
assertEquals(100, pb.getHeight());
// Fill rect
pb.fillRect(10, 10, 20, 20, 0xFFFF0000);
assertEquals(0xFFFF0000, pb.getPixel(15, 15));
assertEquals(0, pb.getPixel(5, 5));
// Draw line AA
pb.drawLineAA(0, 0, 99, 99, 0xFF00FF00, 1.0);
int diagPixel = pb.getPixel(50, 50);
assertNotEquals(0, diagPixel, "Anti-aliased diagonal line should render pixels");
// Clipping
pb.setClip(20, 20, 10, 10);
assertTrue(pb.isClipped(2, 80));
assertFalse(pb.isClipped(25, 25));
assertEquals(0, pb.getPixel(2, 80));
pb.setPixel(2, 80, 0xFF0000FF);
assertEquals(0, pb.getPixel(2, 80), "Clipped pixel must not be modified");
pb.clearClip();
assertFalse(pb.isClipped(5, 5));
}
}
@@ -1,7 +1,6 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Point;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -2,7 +2,6 @@ package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.awt.Point;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -7,8 +7,6 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Color;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
/** /**
@@ -4,14 +4,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
/** /**
@@ -201,7 +193,7 @@ public class Phase2GocaEngineTest {
@Test @Test
@DisplayName("Test HODWallpaper Tile, Center, and Stretch") @DisplayName("Test HODWallpaper Tile, Center, and Stretch")
public void testHODWallpaper() { public void testHODWallpaper() {
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); PixelBuffer img = new DefaultPixelBuffer(32, 32);
HODWallpaper wp = new HODWallpaper(img, HODWallpaper.HOD_CENTER); HODWallpaper wp = new HODWallpaper(img, HODWallpaper.HOD_CENTER);
assertEquals(HODWallpaper.HOD_CENTER, wp.getDisplay()); assertEquals(HODWallpaper.HOD_CENTER, wp.getDisplay());
@@ -211,10 +203,8 @@ public class Phase2GocaEngineTest {
wp.setDisplay(HODWallpaper.HOD_STRETCH); wp.setDisplay(HODWallpaper.HOD_STRETCH);
assertEquals(HODWallpaper.HOD_STRETCH, wp.getDisplay()); assertEquals(HODWallpaper.HOD_STRETCH, wp.getDisplay());
BufferedImage canvas = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); PixelBuffer canvas = new DefaultPixelBuffer(100, 100);
Graphics g = canvas.getGraphics(); wp.paint(canvas, 0, 0, 100, 100);
wp.paint(new java.awt.Canvas(), g, 0, 0, 100, 100);
g.dispose();
} }
@Test @Test
@@ -283,8 +273,9 @@ public class Phase2GocaEngineTest {
assertEquals(101, bounds.width); assertEquals(101, bounds.width);
assertEquals(101, bounds.height); assertEquals(101, bounds.height);
Image img = fa.getImage(); Object img = fa.getImage();
assertNotNull(img); assertNotNull(img);
assertNotNull(fa.getPixelBuffer());
fa.dispose(); fa.dispose();
} }
@@ -10,7 +10,7 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Point; import haus.nightmare.lib3270j.graphics.Point;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -0,0 +1,111 @@
package haus.nightmare.lib3270j.integration;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Live integration test verifying headless connection to live MVS host:
* mvs.hugfreevikings.wtf:1023 using guest credentials.
*/
@Tag("integration")
public class LiveHostHeadlessTest {
private static final Logger logger = Logger.getLogger(LiveHostHeadlessTest.class.getName());
private static final String HOST = "mvs.hugfreevikings.wtf";
private static final int PORT = 1023;
@Test
@DisplayName("Connect headless Telnet3270Client to mvs.hugfreevikings.wtf:1023")
public void testLiveMvsConnection() throws Exception {
// Probe host reachability (timeout 3 seconds)
boolean reachable = false;
try (Socket probe = new Socket()) {
probe.connect(new InetSocketAddress(HOST, PORT), 3000);
reachable = true;
} catch (IOException e) {
logger.warning("Live host " + HOST + ":" + PORT + " unreachable: " + e.getMessage());
}
Assumptions.assumeTrue(reachable, "Skipping live test: " + HOST + ":" + PORT + " is not reachable");
ConnectionConfig config = new ConnectionConfig(HOST, PORT, TerminalModel.IBM_3279_2);
config.setConnectTimeoutMs(5000);
config.setSoTimeoutMs(10000);
Telnet3270Client client = new Telnet3270Client(config);
CountDownLatch connectedLatch = new CountDownLatch(1);
CountDownLatch screenLatch = new CountDownLatch(1);
client.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
if (newState == ConnectionState.CONNECTED_3270) {
connectedLatch.countDown();
}
}
@Override
public void onConnectionError(String message) {}
});
client.addScreenUpdateListener(new ScreenUpdateListener() {
@Override
public void onScreenUpdated() {
ScreenBuffer sb = client.getScreenBuffer();
if (sb != null && sb.isFormatted()) {
screenLatch.countDown();
}
}
});
try {
client.connect();
boolean connected = connectedLatch.await(5, TimeUnit.SECONDS);
assertTrue(connected, "Client must establish CONNECTED_3270 state");
boolean screenReceived = screenLatch.await(5, TimeUnit.SECONDS);
assertTrue(screenReceived, "Client must receive formatted screen from live MVS host");
ScreenBuffer sb = client.getScreenBuffer();
assertNotNull(sb);
StringBuilder fullText = new StringBuilder();
for (int i = 0; i < sb.getSize(); i++) {
char c = sb.getCell(i).ucs4;
fullText.append(c > ' ' ? c : ' ');
}
String screenContent = fullText.toString().trim();
assertFalse(screenContent.isEmpty(), "Screen buffer text should not be empty");
// Verify headless graphics plane is initialized without AWT
assertNotNull(client.getGraphicsPlane());
assertEquals(720, client.getGraphicsPlane().getWidth());
assertEquals(384, client.getGraphicsPlane().getHeight());
logger.info("Successfully connected to live host. Banner preview: " +
screenContent.substring(0, Math.min(200, screenContent.length())).replaceAll("\\s+", " "));
} finally {
client.disconnect();
}
}
}