6 Commits

Author SHA1 Message Date
rudi 3a576b79b4 Phase 6
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s
2026-08-28 13:57:09 -04:00
rudi 40ebd40fe2 Phase 5 2026-08-28 13:36:02 -04:00
rudi f310d39fc1 Phase 4 2026-08-28 13:26:40 -04:00
rudi a44ea00d3a Phase 3 2026-08-28 13:11:28 -04:00
rudi 037ad1f941 Phase 2 2026-08-28 13:00:12 -04:00
rudi 79258c0c44 Phase 1 2026-08-28 12:38:56 -04:00
109 changed files with 14263 additions and 586 deletions
@@ -1,17 +1,20 @@
package haus.nightmare.j3270;
import haus.nightmare.lib3270j.*;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.j3270.ui.StatusBar;
import haus.nightmare.j3270.ui.TerminalPanel;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.j3270.ui.*;
import haus.nightmare.j3270.ft.FileTransfer;
import haus.nightmare.j3270.ft.FileTransferDialog;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.*;
import java.awt.print.PrinterJob;
import java.io.IOException;
import java.util.logging.*;
@@ -36,6 +39,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private FileTransfer fileTransfer;
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
// Dialog instances
private FindDialog findDialog;
private ScriptDialog scriptDialog;
private FieldInspectorDialog fieldInspectorDialog;
private PrinterSessionDialog printerSessionDialog;
public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
@@ -52,7 +61,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
refreshTimer = new Timer(100, e -> {
if (client != null) {
statusBar.updateStatus();
// Keep focus on terminal panel when window is active
if (isActive() && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
@@ -66,12 +74,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
disconnect();
if (refreshTimer != null)
refreshTimer.stop();
if (printerSessionDialog != null) {
printerSessionDialog.dispose();
}
terminalPanel.dispose();
}
@Override
public void windowActivated(WindowEvent e) {
// When window gains focus, push to terminal panel
terminalPanel.requestFocusInWindow();
}
});
@@ -79,11 +89,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private void buildUI() {
terminalPanel = new TerminalPanel();
// Status bar
statusBar = new StatusBar();
// Layout
getContentPane().setLayout(new BorderLayout());
getContentPane().setBackground(new Color(10, 10, 10));
getContentPane().add(terminalPanel, BorderLayout.CENTER);
@@ -95,11 +102,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
menuBar.setBackground(new Color(30, 30, 30));
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
// File menu
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
// 1. File menu
JMenu fileMenu = createMenu("File");
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog));
fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
@@ -108,7 +121,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}));
menuBar.add(fileMenu);
// View menu
// 2. Edit menu
JMenu editMenu = createMenu("Edit");
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
editMenu.addSeparator();
editMenu.add(createMenuItem("Select All", KeyEvent.VK_A, () -> terminalPanel.selectAll()));
editMenu.add(createMenuItem("Clear Selection", -1, () -> terminalPanel.clearSelection()));
editMenu.addSeparator();
editMenu.add(createMenuItem("Find on Screen...", KeyEvent.VK_F, this::showFindDialog));
editMenu.add(createMenuItem("Find Next", KeyEvent.VK_G, this::findNext));
editMenu.add(createMenuItem("Find Previous", KeyEvent.VK_G, this::findPrevious, true));
menuBar.add(editMenu);
// 3. View menu
JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
@@ -116,10 +143,67 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.setFontSize(16);
terminalPanel.guardedPack();
}));
viewMenu.addSeparator();
// CodePage Submenu
JMenu cpMenu = createMenu("Code Page");
String[] codePages = {
"037 - US / Canada / Brazil",
"1047 - IBM Open Systems / z/OS Unix",
"500 - International Latin-1",
"273 - Germany / Austria",
"277 - Denmark / Norway",
"278 - Sweden / Finland",
"280 - Italy",
"284 - Spain / Latin America",
"285 - United Kingdom",
"297 - France",
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"933 - Korean Mixed DBCS"
};
for (String cpItem : codePages) {
String cpId = cpItem.split(" -")[0].trim();
JMenuItem cpMi = new JMenuItem(cpItem);
cpMi.setBackground(new Color(40, 40, 40));
cpMi.setForeground(new Color(200, 200, 200));
cpMi.addActionListener(e -> changeCodePage(cpId));
cpMenu.add(cpMi);
}
viewMenu.add(cpMenu);
// Graphics Mode Submenu
JMenu gfxMenu = createMenu("Graphics Mode");
for (GraphicsMode gm : GraphicsMode.values()) {
JMenuItem gmMi = new JMenuItem(gm.name());
gmMi.setBackground(new Color(40, 40, 40));
gmMi.setForeground(new Color(200, 200, 200));
gmMi.addActionListener(e -> changeGraphicsMode(gm));
gfxMenu.add(gmMi);
}
viewMenu.add(gfxMenu);
viewMenu.addSeparator();
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu);
// Actions menu
// 4. Actions menu
JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendEnter();
terminalPanel.repaint();
}
}));
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
if (client != null)
client.sendClear();
@@ -130,17 +214,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.reset();
terminalPanel.repaint();
}));
actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> {
if (client != null && client.getConnectionState().isFullSession()) {
client.eraseInput();
terminalPanel.repaint();
}
}, true));
actionsMenu.add(createMenuItem("Attention", KeyEvent.VK_A, () -> {
if (client != null && client.isConnected()) client.attn();
}, true));
actionsMenu.add(createMenuItem("System Request", KeyEvent.VK_S, () -> {
if (client != null && client.isConnected()) {
client.sysReq();
terminalPanel.repaint();
}
}, true));
actionsMenu.add(createMenuItem("Cursor Select", KeyEvent.VK_Q, () -> {
if (client != null && client.getConnectionState().isFullSession()) {
client.cursorSelect();
terminalPanel.repaint();
}
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
terminalPanel.toggleLightPen();
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
menuBar.add(actionsMenu);
// Help menu
// 5. Help menu
JMenu helpMenu = createMenu("Help");
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
helpMenu.add(createMenuItem("Mnemonic Keystroke Reference", -1, this::showMnemonicHelp));
helpMenu.addSeparator();
helpMenu.add(createMenuItem("About", -1, this::showAbout));
menuBar.add(helpMenu);
@@ -169,6 +277,164 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return createMenuItem(name, mnemonic, action, false);
}
// ========== Dialog Openers ==========
private void showFindDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using Find.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (findDialog == null) {
findDialog = new FindDialog(this, client, terminalPanel);
}
findDialog.setVisible(true);
}
private void findNext() {
if (client == null) return;
String last = FindDialog.getLastSearchText();
if (last == null || last.isEmpty()) {
showFindDialog();
} else {
ScreenBuffer sb = client.getScreenBuffer();
int curAddr = sb.getCursorAddress();
int cols = sb.getDisplayCols();
int curRow = curAddr / cols;
int curCol = (curAddr % cols + 1) % cols;
if (curCol == 0) curRow = (curRow + 1) % sb.getDisplayRows();
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_FORWARD, !FindDialog.getLastMatchCase());
if (found >= 0) {
sb.setCursorAddress(found);
terminalPanel.setSearchHighlight(found, last.length());
terminalPanel.repaint();
} else {
Toolkit.getDefaultToolkit().beep();
}
}
}
private void findPrevious() {
if (client == null) return;
String last = FindDialog.getLastSearchText();
if (last == null || last.isEmpty()) {
showFindDialog();
} else {
ScreenBuffer sb = client.getScreenBuffer();
int curAddr = sb.getCursorAddress();
int cols = sb.getDisplayCols();
int curRow = curAddr / cols;
int curCol = (curAddr % cols - 1 + cols) % cols;
if (curCol == cols - 1) curRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_BACKWARD, !FindDialog.getLastMatchCase());
if (found >= 0) {
sb.setCursorAddress(found);
terminalPanel.setSearchHighlight(found, last.length());
terminalPanel.repaint();
} else {
Toolkit.getDefaultToolkit().beep();
}
}
}
private void showScriptDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before running scripts.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (scriptDialog == null) {
scriptDialog = new ScriptDialog(this, client, terminalPanel);
}
scriptDialog.setVisible(true);
}
private void showFieldInspectorDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
if (fieldInspectorDialog == null) {
fieldInspectorDialog = new FieldInspectorDialog(this, client, terminalPanel);
} else {
fieldInspectorDialog.refreshFields();
}
fieldInspectorDialog.setVisible(true);
}
private void showPrinterSessionDialog() {
if (printerSessionDialog == null) {
String host = (client != null) ? client.getConfig().getHost() : lastHost;
int port = (client != null) ? client.getConfig().getPort() : lastPort;
boolean tls = (client != null) ? client.getConfig().isUseTls() : false;
printerSessionDialog = new PrinterSessionDialog(this, host, port, tls);
}
printerSessionDialog.setVisible(true);
}
private void showPasteLineWrapDialog() {
if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
if (text == null || text.isEmpty()) {
JOptionPane.showMessageDialog(this, "Clipboard is empty.", "Clipboard", JOptionPane.INFORMATION_MESSAGE);
return;
}
int cols = client.getScreenBuffer().getDisplayCols();
String endColStr = JOptionPane.showInputDialog(this, "Enter right margin column (1-" + cols + ") for wrapping [0 = no right margin]:", "80");
if (endColStr != null) {
int endCol = 0;
try {
endCol = Integer.parseInt(endColStr.trim());
} catch (NumberFormatException ignored) {}
terminalPanel.pasteLineWrap(text, endCol, true);
}
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Could not read clipboard: " + ex.getMessage(), "Paste Error", JOptionPane.ERROR_MESSAGE);
}
}
private void changeCodePage(String cpId) {
if (client != null) {
client.setCodePage(cpId);
client.getScreenBuffer().translateToUnicode();
terminalPanel.refreshScreen();
statusBar.updateStatus();
}
haus.nightmare.j3270.config.Settings.setCodePage(cpId);
}
private void changeGraphicsMode(GraphicsMode mode) {
if (client != null && client.getDataStreamProcessor() != null) {
client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode);
terminalPanel.repaint();
}
haus.nightmare.j3270.config.Settings.setGraphicsMode(mode);
}
private void printScreen() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before printing screen.", "Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
PrinterJob job = PrinterJob.getPrinterJob();
job.setJobName("j3270 Screen Capture");
job.setPrintable(terminalPanel);
if (job.printDialog()) {
try {
job.print();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void showFileTransferDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
@@ -187,7 +453,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
monitor.setMillisToDecideToPopup(0);
monitor.setMillisToPopup(0);
// Start a timer to check for manual cancellation
Timer cancelCheckTimer = new Timer(500, e -> {
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
fileTransfer.cancel();
@@ -208,7 +473,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
public void onBytesTransferred(long bytes) {
if (monitor != null) {
monitor.setNote(bytes + " bytes transferred");
// We don't always know max size in IND$FILE, so we just pulse/update note
}
}
@@ -242,6 +506,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setInitialPort(initialPort);
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setVisible(true);
if (dialog.isConfirmed()) {
@@ -265,16 +530,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
lastHost = config.getHost();
lastPort = config.getPort();
// Save for auto-connect
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
// Disconnect existing connection
disconnect();
// Wire interactive certificate verifier if none configured
if (config.getCertificateVerifier() == null) {
config.setCertificateVerifier((chain, authType, exception) ->
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
@@ -291,7 +554,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
// Resize window to match model
terminalPanel.guardedPack();
new Thread(() -> {
@@ -307,7 +569,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
}, "Connect-Thread").start();
// Focus the terminal panel
terminalPanel.requestFocusInWindow();
}
@@ -330,8 +591,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
int current = terminalPanel.getFontSize();
int newSize = Math.max(8, Math.min(72, current + delta));
terminalPanel.setFontSize(newSize);
// Pack after font change — setFontSize sets the resize guard
// to prevent the componentResized from re-triggering autoFitFont
terminalPanel.guardedPack();
}
@@ -344,7 +603,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
statusBar.updateStatus();
terminalPanel.repaint();
// Auto-resize and focus on first full session
if (newState.isFullSession() && !oldState.isFullSession()) {
terminalPanel.guardedPack();
terminalPanel.requestFocusInWindow();
@@ -365,6 +623,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (deviceName != null) {
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
}
statusBar.updateStatus();
});
}
@@ -375,10 +634,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (screenUpdatePending.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
screenUpdatePending.set(false);
// During an active file transfer, let the CUT/DFT handler drive
// keyboard state. In x3270, ft_cut_data() runs before WCC
// keyboard-restore is applied — the keyboard stays locked for the
// entire CUT transfer.
if (fileTransfer != null) {
fileTransfer.onScreenUpdated();
}
@@ -400,6 +655,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(() -> {
terminalPanel.guardedPack();
statusBar.updateStatus();
});
}
@@ -421,11 +677,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"Insert — Toggle insert mode\n" +
"Escape — Reset\n" +
"PageUp/Down — PF7/PF8\n" +
"Alt+C — Clear\n" +
"Alt+1/2/3 — PA1/PA2/PA3\n" +
"Cmd+DDisconnect\n" +
"Cmd+QQuit\n" +
"Cmd+=/-/0 — Font size +/-/reset";
"Alt+C / Ctrl+K — Clear\n" +
"Alt+E — Erase Input\n" +
"Alt+AAttention\n" +
"Alt+SSystem Request\n" +
"Alt+Q — Cursor Select\n" +
"Alt+L — Toggle Light Pen\n" +
"Cmd/Ctrl+F — Find on Screen\n" +
"Cmd/Ctrl+G / F3— Find Next\n" +
"Cmd/Ctrl+T — File Transfer\n" +
"Cmd/Ctrl+D — Disconnect\n" +
"Cmd/Ctrl+Q — Quit\n" +
"Cmd/Ctrl+=/-/0 — Font size +/-/reset";
JTextArea area = new JTextArea(text);
area.setEditable(false);
@@ -433,18 +696,56 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area);
sp.setPreferredSize(new Dimension(360, 400));
sp.setPreferredSize(new Dimension(380, 430));
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
}
private void showMnemonicHelp() {
String text = "IBM Host On-Demand ECL Mnemonic Tokens:\n\n" +
"[enter] — Send Enter key\n" +
"[tab] — Tab to next field\n" +
"[backtab] — Back-tab to previous field\n" +
"[clear] — Clear screen\n" +
"[reset] — Reset keyboard lock\n" +
"[eraseeof] — Erase to End-of-Field\n" +
"[eraseinput] — Erase all unprotected fields\n" +
"[newline] — Advance to next line\n" +
"[dup] — Insert Duplicate order (0x1C)\n" +
"[fm] — Insert Field Mark order (0x1E)\n" +
"[attn] — Send Attention signal\n" +
"[sysreq] — System Request\n" +
"[cursel] — Cursor Select\n" +
"[pf1] - [pf24] — PF1 through PF24 keys\n" +
"[pa1] - [pa3] — PA1 through PA3 keys\n" +
"[up]/[down] — Cursor up / down\n" +
"[left]/[right] — Cursor left / right\n" +
"[home] — Cursor to home\n\n" +
"Example Script:\n" +
"LOGON APPLID(TSO)[enter]USER[tab]PASSWORD[enter][pf3]";
JTextArea area = new JTextArea(text);
area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area);
sp.setPreferredSize(new Dimension(440, 430));
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
}
private void showAbout() {
JOptionPane.showMessageDialog(this,
"j3270 — Java TN3270 Terminal Emulator\n\n" +
"A Java reimplementation of x3270.\n" +
"lib3270j v0.1.0\n\n" +
"Supports: TN3270, TN3270E (RFC 2355)\n" +
"Models: IBM 3278/3279 Models 2-5\n" +
"Colors, Extended Attributes, Query Replies",
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" +
"lib3270j v0.2.0\n\n" +
"Features:\n" +
"• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" +
"• Models 2-5 & Dynamic Geometry\n" +
"• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" +
"• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" +
"• GDDM/GOCA Vector Graphics & Programmed Symbols\n" +
"• 22+ Pluggable EBCDIC Codepages & DBCS Support\n" +
"• ECL Presentation Space Search, Inspector & Scripting",
"About j3270", JOptionPane.INFORMATION_MESSAGE);
}
@@ -455,7 +756,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
boolean cliTls = false;
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
haus.nightmare.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
@@ -470,11 +771,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} else if ("--tn3270e".equals(args[i])) {
cliTn3270e = true;
} else if (args[i].startsWith("--graphics=")) {
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
cliGraphicsMode = GraphicsMode.fromString(args[i].substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(args[i])) {
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
configFile = args[++i];
} else {
@@ -482,7 +783,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
}
// Configure logging for application packages
Level logLevel = debug ? Level.ALL : Level.INFO;
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
@@ -522,7 +822,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
System.err.println("Could not create j3270.log: " + e.getMessage());
}
// Load INI config file if specified
if (configFile != null) {
try {
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
@@ -533,43 +832,38 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
}
// Dark look and feel
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
log.fine("Could not set system look and feel");
}
// Use native macOS menu bar if applicable
System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("apple.awt.application.name", "j3270");
final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert;
final Boolean finalTn3270e = cliTn3270e;
final haus.nightmare.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App();
app.setVisible(true);
// If host:port given on command line, connect directly
if (!remainingArgs.isEmpty()) {
String hostArg = remainingArgs.get(0);
int port = finalTls ? 992 : 23;
if (remainingArgs.size() >= 2) {
try {
port = Integer.parseInt(remainingArgs.get(1));
} catch (NumberFormatException ignored) {
}
} catch (NumberFormatException ignored) {}
}
TerminalModel model = TerminalModel.IBM_3279_4;
if (remainingArgs.size() >= 3) {
try {
int modelNum = Integer.parseInt(remainingArgs.get(2));
model = TerminalModel.forModel(modelNum, true);
} catch (Exception ignored) {
}
} catch (Exception ignored) {}
}
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
if (finalTls) {
@@ -95,6 +95,14 @@ public class Settings {
prefs.put("graphicsMode", (mode != null ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH).name());
}
public static String getCodePage() {
return prefs.get("codePage", "037");
}
public static void setCodePage(String codePage) {
prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037");
}
public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null);
try {
@@ -256,6 +264,11 @@ public class Settings {
case "enableTn3270e":
setAutoConnectTn3270e(Boolean.parseBoolean(value));
break;
case "codePage":
case "codepage":
case "charset":
setCodePage(value);
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
default:
log.warning("Unknown behavior/connection key: " + key);
@@ -324,6 +337,7 @@ public class Settings {
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
}
w.println("codePage = " + getCodePage());
w.println("blockSelectMode = " + getBlockSelectMode());
w.println();
@@ -53,6 +53,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.callback = callback;
}
public Telnet3270Client getClient() {
return client;
}
/**
* Start a new file transfer with the given configuration.
* @return null if started successfully, error message otherwise.
@@ -1,6 +1,8 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
@@ -18,6 +20,8 @@ public class FileTransferDialog extends JDialog {
private JTextField localFileField;
private JButton browseLocalButton;
private JTextField hostFileField;
private JButton browseHostButton;
private JComboBox<Integer> mtuCombo;
// Options
private JRadioButton asciiRadio;
@@ -40,7 +44,7 @@ public class FileTransferDialog extends JDialog {
private JButton cancelButton;
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
super(owner, "File Transfer (IND$FILE)", false); // non-modal to see progress
super(owner, "File Transfer (IND$FILE)", false);
this.owner = owner;
this.coordinator = coordinator;
@@ -76,7 +80,6 @@ public class FileTransferDialog extends JDialog {
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
dirPanel.add(sendRadio);
// Disable append/overwrite when sending
sendRadio.addActionListener(e -> updateOptionStates());
receiveRadio.addActionListener(e -> updateOptionStates());
@@ -94,7 +97,13 @@ public class FileTransferDialog extends JDialog {
// Host File
hostFileField = new JTextField(20);
formPanel.add(createRow("Host File:", hostFileField));
browseHostButton = new JButton("Browse Host...");
browseHostButton.addActionListener(e -> browseHostDirectory());
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
hostPanel.setOpaque(false);
hostPanel.add(hostFileField, BorderLayout.CENTER);
hostPanel.add(browseHostButton, BorderLayout.EAST);
formPanel.add(createRow("Host File:", hostPanel));
// Mode
asciiRadio = new JRadioButton("ASCII");
@@ -114,6 +123,11 @@ public class FileTransferDialog extends JDialog {
formPanel.add(createRow("Transfer Mode:", modePanel));
// MTU Buffer Size
mtuCombo = new JComboBox<>(new Integer[]{2048, 4096, 8192, 16384, 32768});
mtuCombo.setSelectedItem(FTConstants.DFT_BUF);
formPanel.add(createRow("DFT Buffer Size:", mtuCombo));
// Options (checkboxes)
crCheck = new JCheckBox("Add/Remove CR");
crCheck.setSelected(true);
@@ -198,10 +212,7 @@ public class FileTransferDialog extends JDialog {
setContentPane(mainPanel);
// Theme styling for components
applyTheme(mainPanel);
// Initialize state
updateOptionStates();
}
@@ -236,14 +247,29 @@ public class FileTransferDialog extends JDialog {
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
// Auto-fill host file if empty
if (hostFileField.getText().trim().isEmpty()) {
hostFileField.setText(chooser.getSelectedFile().getName());
}
}
}
private void browseHostDirectory() {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String initialQuery = hostFileField.getText().trim();
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
dialog.setVisible(true);
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
hostFileField.setText(dialog.getSelectedHostFile());
if (localFileField.getText().trim().isEmpty()) {
String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", "");
int lastDot = cleanName.lastIndexOf('.');
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1);
localFileField.setText(cleanName);
}
}
}
private void startTransfer() {
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
@@ -262,6 +288,11 @@ public class FileTransferDialog extends JDialog {
config.setAppend(appendCheck.isSelected());
config.setOverwrite(overwriteCheck.isSelected());
Integer mtu = (Integer) mtuCombo.getSelectedItem();
if (mtu != null) {
config.setDftBufferSize(mtu);
}
config.setRecfm(recfmField.getText().trim());
config.setLrecl(lreclField.getText().trim());
config.setBlksize(blksizeField.getText().trim());
@@ -272,7 +303,6 @@ public class FileTransferDialog extends JDialog {
if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
} else {
// Success, close dialog (progress will be shown separately)
dispose();
}
}
@@ -15,6 +15,7 @@ public class ConnectDialog extends JDialog {
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JComboBox<String> codePageCombo;
private JTextField luField;
private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox;
@@ -114,9 +115,57 @@ public class ConnectDialog extends JDialog {
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
mainPanel.add(graphicsCombo, gbc);
// Code Page
gbc.gridx = 0;
gbc.gridy = 5;
gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setForeground(fg);
cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
String[] commonCodePages = {
"037 - US / Canada / Brazil",
"1047 - IBM Open Systems / z/OS Unix",
"500 - International Latin-1",
"273 - Germany / Austria",
"277 - Denmark / Norway",
"278 - Sweden / Finland",
"280 - Italy",
"284 - Spain / Latin America",
"285 - United Kingdom",
"297 - France",
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"933 - Korean Mixed DBCS"
};
codePageCombo = new JComboBox<>(commonCodePages);
codePageCombo.setEditable(true);
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
for (String item : commonCodePages) {
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
codePageCombo.setSelectedItem(item);
break;
}
}
codePageCombo.setBackground(new Color(45, 45, 45));
codePageCombo.setForeground(fg);
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
mainPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox
gbc.gridx = 1;
gbc.gridy = 5;
gbc.gridy = 6;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
tlsCheckBox.setBackground(new Color(30, 30, 30));
@@ -137,7 +186,7 @@ public class ConnectDialog extends JDialog {
// Verify Certificate Checkbox
gbc.gridx = 1;
gbc.gridy = 6;
gbc.gridy = 7;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
@@ -149,7 +198,7 @@ public class ConnectDialog extends JDialog {
// TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
gbc.gridy = 8;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
tn3270eCheckBox.setForeground(fg);
@@ -181,7 +230,7 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 8;
gbc.gridy = 9;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
@@ -224,6 +273,18 @@ public class ConnectDialog extends JDialog {
result.setLuName(lu);
}
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
Object cpSelection = codePageCombo.getSelectedItem();
if (cpSelection != null) {
String cpStr = cpSelection.toString().trim();
int dash = cpStr.indexOf(" -");
if (dash > 0) {
cpStr = cpStr.substring(0, dash).trim();
}
result.setCodePage(cpStr);
haus.nightmare.j3270.config.Settings.setCodePage(cpStr);
}
result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
@@ -260,4 +321,17 @@ public class ConnectDialog extends JDialog {
public void setInitialTn3270e(boolean tn3270e) {
tn3270eCheckBox.setSelected(tn3270e);
}
public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
String item = codePageCombo.getItemAt(i);
if (item.startsWith(cp + " ") || item.equals(cp)) {
codePageCombo.setSelectedIndex(i);
return;
}
}
codePageCombo.setSelectedItem(cp);
}
}
}
@@ -0,0 +1,233 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
/**
* Live Field Inspector dialog displaying all fields in the 3270 Presentation Space (ECLFieldList).
*/
public class FieldInspectorDialog extends JDialog implements ScreenUpdateListener {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private DefaultTableModel tableModel;
private JTable table;
private JLabel countLabel;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_BORDER = new Color(65, 65, 65);
private static final Color DARK_SELECTION = new Color(75, 110, 175);
public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client;
this.terminalPanel = terminalPanel;
buildUI();
setSize(850, 420);
setLocationRelativeTo(parent);
if (client != null) {
client.addScreenUpdateListener(this);
refreshFields();
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
if (client != null) {
// Screen update listener removed on close
}
}
});
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(DARK_BG);
// Top info bar
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen");
countLabel.setForeground(DARK_FG);
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh");
refreshBtn.setBackground(new Color(55, 55, 55));
refreshBtn.setForeground(DARK_FG);
refreshBtn.addActionListener(e -> refreshFields());
topPanel.add(refreshBtn, BorderLayout.EAST);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Table
String[] columns = {
"#", "Pos", "Start", "End", "Len", "Prot", "MDT", "Num", "Hi-Int", "Hidden", "Pen-Sel", "Content Text"
};
tableModel = new DefaultTableModel(columns, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
table = new JTable(tableModel);
table.setBackground(DARK_FIELD_BG);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(DARK_FG);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
// Column widths
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
}
// Center align boolean columns
DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
centerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
for (int i = 5; i <= 10; i++) {
table.getColumnModel().getColumn(i).setCellRenderer(centerRenderer);
}
table.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting() && table.getSelectedRow() >= 0) {
selectFieldOnTerminal(table.getSelectedRow());
}
});
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(DARK_BORDER));
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom panel
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
bottomPanel.setOpaque(false);
JButton jumpBtn = new JButton("Jump to Selected Field");
jumpBtn.setBackground(new Color(50, 100, 160));
jumpBtn.setForeground(Color.WHITE);
jumpBtn.addActionListener(e -> {
int row = table.getSelectedRow();
if (row >= 0) {
selectFieldOnTerminal(row);
}
});
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(55, 55, 55));
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose());
bottomPanel.add(jumpBtn);
bottomPanel.add(closeBtn);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
}
public synchronized void refreshFields() {
if (client == null || !client.getConnectionState().isFullSession()) {
tableModel.setRowCount(0);
countLabel.setText("Not connected or screen unformatted");
return;
}
ECLFieldList fieldList = client.getFieldList();
List<ECLField> fields = fieldList.getFields();
int selectedRow = table.getSelectedRow();
tableModel.setRowCount(0);
int cols = client.getScreenBuffer().getDisplayCols();
for (int i = 0; i < fields.size(); i++) {
ECLField f = fields.get(i);
int startRow = f.getStartRow() + 1;
int startCol = f.getStartCol() + 1;
int endRow = f.getEndRow() + 1;
int endCol = f.getEndCol() + 1;
String text = f.getText();
if (f.isHidden()) {
text = "••••••••";
}
tableModel.addRow(new Object[]{
(i + 1),
f.getStart(),
String.format("%02d/%02d", startRow, startCol),
String.format("%02d/%02d", endRow, endCol),
f.getLength(),
f.isProtected() ? "YES" : "-",
f.isModified() ? "YES" : "-",
f.isNumeric() ? "YES" : "-",
f.isHighIntensity() ? "YES" : "-",
f.isHidden() ? "YES" : "-",
f.isPenSelectable() ? "YES" : "-",
text
});
}
countLabel.setText(fields.size() + " fields detected on screen (" + client.getScreenBuffer().getDisplayRows() + "x" + cols + ")");
if (selectedRow >= 0 && selectedRow < tableModel.getRowCount()) {
table.setRowSelectionInterval(selectedRow, selectedRow);
}
}
private void selectFieldOnTerminal(int rowIndex) {
if (client == null || rowIndex < 0) return;
List<ECLField> fields = client.getFieldList().getFields();
if (rowIndex < fields.size()) {
ECLField f = fields.get(rowIndex);
client.getScreenBuffer().setCursorAddress(f.getDataStart());
if (terminalPanel != null) {
terminalPanel.setSelectionRange(f.getDataStart(), f.getEnd());
terminalPanel.repaint();
}
}
}
// ========== ScreenUpdateListener ==========
@Override
public void onScreenUpdated() {
SwingUtilities.invokeLater(this::refreshFields);
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(this::refreshFields);
}
@Override
public void onSoundAlarm() {}
}
@@ -0,0 +1,234 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
/**
* Find dialog for searching text in the 3270 Presentation Space using ECLPS.
*/
public class FindDialog extends JDialog {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextField searchField;
private JCheckBox matchCaseCheck;
private JRadioButton forwardRadio;
private JRadioButton backwardRadio;
private JLabel statusLabel;
private static String lastSearchText = "";
private static boolean lastMatchCase = false;
private static boolean lastForward = true;
public FindDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "Find on Screen", false);
this.client = client;
this.terminalPanel = terminalPanel;
buildUI();
pack();
setLocationRelativeTo(parent);
setResizable(false);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
// Form
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setOpaque(false);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Search text
gbc.gridx = 0;
gbc.gridy = 0;
JLabel findLabel = new JLabel("Find what:");
findLabel.setForeground(fg);
findLabel.setFont(labelFont);
formPanel.add(findLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
searchField = new JTextField(20);
searchField.setText(lastSearchText);
searchField.setBackground(new Color(50, 50, 50));
searchField.setForeground(fg);
searchField.setCaretColor(fg);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
searchField.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(70, 70, 70)),
BorderFactory.createEmptyBorder(3, 6, 3, 6)));
formPanel.add(searchField, gbc);
// Options
gbc.gridx = 1;
gbc.gridy = 1;
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
matchCaseCheck.setForeground(fg);
matchCaseCheck.setOpaque(false);
matchCaseCheck.setFont(labelFont);
matchCaseCheck.setFocusPainted(false);
formPanel.add(matchCaseCheck, gbc);
// Direction
gbc.gridx = 1;
gbc.gridy = 2;
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false);
JLabel dirLabel = new JLabel("Direction: ");
dirLabel.setForeground(fg);
dirLabel.setFont(labelFont);
forwardRadio = new JRadioButton("Down", lastForward);
backwardRadio = new JRadioButton("Up", !lastForward);
forwardRadio.setForeground(fg);
backwardRadio.setForeground(fg);
forwardRadio.setOpaque(false);
backwardRadio.setOpaque(false);
forwardRadio.setFocusPainted(false);
backwardRadio.setFocusPainted(false);
ButtonGroup bg = new ButtonGroup();
bg.add(forwardRadio);
bg.add(backwardRadio);
dirPanel.add(dirLabel);
dirPanel.add(forwardRadio);
dirPanel.add(Box.createHorizontalStrut(10));
dirPanel.add(backwardRadio);
formPanel.add(dirPanel, gbc);
// Status
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridwidth = 2;
statusLabel = new JLabel(" ");
statusLabel.setForeground(new Color(255, 120, 120));
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
formPanel.add(statusLabel, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton findNextBtn = new JButton("Find Next");
findNextBtn.setBackground(new Color(60, 63, 65));
findNextBtn.setForeground(fg);
findNextBtn.setFont(labelFont);
findNextBtn.addActionListener(e -> findNext());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 63, 65));
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(findNextBtn);
buttonPanel.add(closeBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
getRootPane().setDefaultButton(findNextBtn);
searchField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
findNext();
} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
dispose();
}
}
});
}
public void findNext() {
find(forwardRadio.isSelected());
}
public void findPrevious() {
find(!forwardRadio.isSelected());
}
private void find(boolean forward) {
String target = searchField.getText();
if (target == null || target.isEmpty()) {
statusLabel.setText("Please enter search text.");
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("Terminal is not connected.");
return;
}
lastSearchText = target;
lastMatchCase = matchCaseCheck.isSelected();
lastForward = forwardRadio.isSelected();
ScreenBuffer sb = client.getScreenBuffer();
int curAddr = sb.getCursorAddress();
int cols = sb.getDisplayCols();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
// Offset search start position past current character so repeat search moves to next occurrence
int startRow = curRow;
int startCol = curCol;
if (forward) {
startCol = (curCol + 1) % cols;
if (startCol == 0) startRow = (curRow + 1) % sb.getDisplayRows();
} else {
startCol = (curCol - 1 + cols) % cols;
if (startCol == cols - 1) startRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
}
int dir = forward ? ECLConstants.SEARCH_FORWARD : ECLConstants.SEARCH_BACKWARD;
int foundPos = client.getPS().searchString(target, startRow, startCol, dir, !matchCaseCheck.isSelected());
if (foundPos >= 0) {
statusLabel.setText("Found at position " + (foundPos + 1) + " (" + (foundPos / cols + 1) + "/" + (foundPos % cols + 1) + ")");
statusLabel.setForeground(new Color(100, 255, 100));
sb.setCursorAddress(foundPos);
if (terminalPanel != null) {
terminalPanel.setSearchHighlight(foundPos, target.length());
terminalPanel.repaint();
}
} else {
statusLabel.setText("Text not found: \"" + target + "\"");
statusLabel.setForeground(new Color(255, 120, 120));
if (terminalPanel != null) {
terminalPanel.clearSearchHighlight();
terminalPanel.repaint();
}
}
}
public static String getLastSearchText() {
return lastSearchText;
}
public static boolean getLastMatchCase() {
return lastMatchCase;
}
public static boolean getLastForward() {
return lastForward;
}
}
@@ -0,0 +1,289 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
*/
public class HostDirectoryDialog extends JDialog {
private final Telnet3270Client client;
private final FTConfig.HostType initialHostType;
private JComboBox<FTConfig.HostType> hostTypeCombo;
private JTextField queryField;
private JTable table;
private DefaultTableModel tableModel;
private JLabel statusLabel;
private String selectedHostFile = null;
private boolean confirmed = false;
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
super(parent, "Host Catalog & Directory Browser", true);
this.client = client;
this.initialHostType = hostType;
buildUI(initialQuery);
setSize(700, 420);
setLocationRelativeTo(parent);
}
private void buildUI(String initialQuery) {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Top Query bar
JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel envLabel = new JLabel("System:");
envLabel.setForeground(fg);
envLabel.setFont(font);
topPanel.add(envLabel, gbc);
gbc.gridx = 1;
hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS});
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
hostTypeCombo.setBackground(new Color(50, 50, 50));
hostTypeCombo.setForeground(fg);
topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:");
queryLabel.setForeground(fg);
queryLabel.setFont(font);
topPanel.add(queryLabel, gbc);
gbc.gridx = 3;
gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setBackground(new Color(50, 50, 50));
queryField.setForeground(fg);
queryField.setCaretColor(fg);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
queryField.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(70, 70, 70)),
new EmptyBorder(3, 5, 3, 5)));
topPanel.add(queryField, gbc);
gbc.gridx = 4;
gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse");
parseBtn.setBackground(new Color(55, 55, 55));
parseBtn.setForeground(fg);
parseBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery());
topPanel.add(parseBtn, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Table
tableModel = new DefaultTableModel();
table = new JTable(tableModel);
table.setBackground(new Color(45, 45, 45));
table.setForeground(fg);
table.setSelectionBackground(new Color(75, 110, 175));
table.setSelectionForeground(Color.WHITE);
table.setGridColor(new Color(65, 65, 65));
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(fg);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && table.getSelectedRow() >= 0) {
onConfirmSelection();
}
}
});
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(new Color(65, 65, 65)));
scrollPane.getViewport().setBackground(new Color(45, 45, 45));
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(new Color(170, 170, 170));
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen");
pasteScreenBtn.setBackground(new Color(50, 50, 50));
pasteScreenBtn.setForeground(fg);
pasteScreenBtn.setFont(font);
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton selectBtn = new JButton("Select Dataset");
selectBtn.setBackground(new Color(50, 120, 50));
selectBtn.setForeground(Color.WHITE);
selectBtn.setFont(font);
selectBtn.addActionListener(e -> onConfirmSelection());
JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(fg);
cancelBtn.setFont(font);
cancelBtn.addActionListener(e -> {
confirmed = false;
dispose();
});
btnPanel.add(pasteScreenBtn);
btnPanel.add(Box.createHorizontalStrut(10));
btnPanel.add(selectBtn);
btnPanel.add(cancelBtn);
bottomPanel.add(btnPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
// Initial setup
setupTableColumns();
if (initialQuery != null && !initialQuery.isEmpty()) {
runQuery();
} else {
parseCurrentScreen();
}
}
private void setupTableColumns() {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
tableModel.setRowCount(0);
if (hostType == FTConfig.HostType.CMS) {
tableModel.setColumnIdentifiers(new Object[]{
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
});
} else {
tableModel.setColumnIdentifiers(new Object[]{
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents"
});
}
}
private void runQuery() {
setupTableColumns();
currentEntries.clear();
String query = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (client != null && client.getXfer() != null) {
if (hostType == FTConfig.HostType.CMS) {
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
currentEntries.addAll(cmsEntries);
for (CMSDirectoryEntry entry : cmsEntries) {
tableModel.addRow(new Object[]{
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
});
}
} else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
currentEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) {
tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
});
}
}
}
statusLabel.setText(currentEntries.size() + " entries found.");
}
private void parseCurrentScreen() {
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("No active session to parse.");
return;
}
setupTableColumns();
currentEntries.clear();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String screenText = client.getPS().getString(0, client.getPS().getSize());
if (hostType == FTConfig.HostType.CMS) {
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText);
currentEntries.addAll(cmsEntries);
for (CMSDirectoryEntry entry : cmsEntries) {
tableModel.addRow(new Object[]{
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
});
}
} else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText);
currentEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) {
tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
});
}
}
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space.");
}
private void onConfirmSelection() {
int row = table.getSelectedRow();
if (row < 0 || row >= currentEntries.size()) {
JOptionPane.showMessageDialog(this, "Please select an entry from the table.",
"No Selection", JOptionPane.WARNING_MESSAGE);
return;
}
HostDirectoryEntry entry = currentEntries.get(row);
selectedHostFile = entry.getName();
confirmed = true;
dispose();
}
public boolean isConfirmed() {
return confirmed;
}
public String getSelectedHostFile() {
return selectedHostFile;
}
}
@@ -0,0 +1,478 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.printer.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* IBM 3287 / 3286 TN3270E Printer Session Manager and Spool Viewer.
* Integrates directly with lib3270j.printer.Telnet3270EP and PD3270.
*/
public class PrinterSessionDialog extends JDialog implements PrintSessionListener {
private final Frame parent;
private PrinterConfig config;
private Telnet3270EP printerSession;
// Configuration Fields
private JTextField hostField;
private JTextField portField;
private JTextField printerLuField;
private JTextField displayLuField;
private JComboBox<String> codePageCombo;
private JCheckBox tlsCheck;
private JCheckBox verifyCertCheck;
private JComboBox<PrinterConfig.DestinationType> destinationCombo;
private JTextField targetField;
// Status Fields
private JLabel statusLabel;
private JLabel sessionTypeLabel;
private JLabel pagesLabel;
private JLabel bytesLabel;
private JButton connectBtn;
private JButton disconnectBtn;
// Spool Display
private JTextArea spoolArea;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent;
config = new PrinterConfig(defaultHost, defaultPort);
config.setUseTls(defaultTls);
config.setDestinationType(PrinterConfig.DestinationType.MEMORY);
buildUI();
setSize(780, 560);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(DARK_BG);
// Top: Configuration Panel
JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false);
topPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(65, 65, 65)), "Printer Session Configuration"));
((TitledBorder) topPanel.getBorder()).setTitleColor(DARK_FG);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
// Host / Port
gbc.gridx = 0; gbc.gridy = 0;
JLabel hLbl = new JLabel("Host:");
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont);
topPanel.add(hLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
hostField = createDarkField(config.getHost() != null ? config.getHost() : "localhost", 14);
topPanel.add(hostField, gbc);
gbc.gridx = 2; gbc.weightx = 0;
JLabel pLbl = new JLabel("Port:");
pLbl.setForeground(DARK_FG); pLbl.setFont(labelFont);
topPanel.add(pLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5;
portField = createDarkField(String.valueOf(config.getPort()), 5);
topPanel.add(portField, gbc);
// Printer LU / Display LU
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
JLabel pluLbl = new JLabel("Printer LU:");
pluLbl.setForeground(DARK_FG); pluLbl.setFont(labelFont);
topPanel.add(pluLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
printerLuField = createDarkField("", 10);
topPanel.add(printerLuField, gbc);
gbc.gridx = 2; gbc.weightx = 0;
JLabel assocLbl = new JLabel("Assoc LU:");
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont);
topPanel.add(assocLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5;
displayLuField = createDarkField("", 10);
topPanel.add(displayLuField, gbc);
// CodePage & TLS
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
JLabel cpLbl = new JLabel("CodePage:");
cpLbl.setForeground(DARK_FG); cpLbl.setFont(labelFont);
topPanel.add(cpLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"});
codePageCombo.setBackground(DARK_FIELD_BG);
codePageCombo.setForeground(DARK_FG);
topPanel.add(codePageCombo, gbc);
gbc.gridx = 2; gbc.gridwidth = 2;
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
tlsPanel.setOpaque(false);
tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls());
tlsCheck.setOpaque(false); tlsCheck.setForeground(DARK_FG); tlsCheck.setFocusPainted(false);
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
verifyCertCheck.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false);
tlsPanel.add(tlsCheck);
tlsPanel.add(verifyCertCheck);
topPanel.add(tlsPanel, gbc);
// Destination Type & Target
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
JLabel destLbl = new JLabel("Destination:");
destLbl.setForeground(DARK_FG); destLbl.setFont(labelFont);
topPanel.add(destLbl, gbc);
gbc.gridx = 1;
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
destinationCombo.setBackground(DARK_FIELD_BG);
destinationCombo.setForeground(DARK_FG);
topPanel.add(destinationCombo, gbc);
gbc.gridx = 2;
JLabel tgtLbl = new JLabel("Target Path:");
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont);
topPanel.add(tgtLbl, gbc);
gbc.gridx = 3;
targetField = createDarkField("printer_output.txt", 12);
topPanel.add(targetField, gbc);
// Connect / Disconnect Buttons
gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 4;
JPanel connBtnPan = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
connBtnPan.setOpaque(false);
connectBtn = new JButton("Start Printer Session");
connectBtn.setBackground(new Color(50, 120, 50));
connectBtn.setForeground(Color.WHITE);
connectBtn.addActionListener(e -> startPrinterSession());
disconnectBtn = new JButton("Stop Session");
disconnectBtn.setBackground(new Color(120, 50, 50));
disconnectBtn.setForeground(Color.WHITE);
disconnectBtn.setEnabled(false);
disconnectBtn.addActionListener(e -> stopPrinterSession());
connBtnPan.add(connectBtn);
connBtnPan.add(disconnectBtn);
topPanel.add(connBtnPan, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Center: Spool Area & Status
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
centerPanel.setOpaque(false);
centerPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(65, 65, 65)), "Printer Spool & Status"));
((TitledBorder) centerPanel.getBorder()).setTitleColor(DARK_FG);
// Status Header
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
statusHeader.setOpaque(false);
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
statusLabel = new JLabel("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180));
statusLabel.setFont(labelFont);
sessionTypeLabel = new JLabel("Session: -");
sessionTypeLabel.setForeground(DARK_FG);
sessionTypeLabel.setFont(labelFont);
pagesLabel = new JLabel("Pages: 0");
pagesLabel.setForeground(DARK_FG);
pagesLabel.setFont(labelFont);
bytesLabel = new JLabel("Bytes: 0");
bytesLabel.setForeground(DARK_FG);
bytesLabel.setFont(labelFont);
statusHeader.add(statusLabel);
statusHeader.add(sessionTypeLabel);
statusHeader.add(pagesLabel);
statusHeader.add(bytesLabel);
centerPanel.add(statusHeader, BorderLayout.NORTH);
// Spool text area
spoolArea = new JTextArea();
spoolArea.setBackground(new Color(25, 25, 25));
spoolArea.setForeground(new Color(100, 255, 100));
spoolArea.setCaretColor(Color.WHITE);
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
spoolArea.setEditable(false);
JScrollPane spoolScroll = new JScrollPane(spoolArea);
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60)));
centerPanel.add(spoolScroll, BorderLayout.CENTER);
mainPanel.add(centerPanel, BorderLayout.CENTER);
// Bottom: Spool controls
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
bottomPanel.setOpaque(false);
JButton clearSpoolBtn = new JButton("Clear Spool");
clearSpoolBtn.setBackground(new Color(50, 50, 50));
clearSpoolBtn.setForeground(DARK_FG);
clearSpoolBtn.addActionListener(e -> {
spoolArea.setText("");
if (printerSession != null && printerSession.getPD() != null) {
printerSession.getPD().resetCapture();
}
pagesLabel.setText("Pages: 0");
bytesLabel.setText("Bytes: 0");
});
JButton saveSpoolBtn = new JButton("Save Spool As...");
saveSpoolBtn.setBackground(new Color(50, 50, 50));
saveSpoolBtn.setForeground(DARK_FG);
saveSpoolBtn.addActionListener(e -> saveSpool());
JButton printSpoolBtn = new JButton("Print Spool...");
printSpoolBtn.setBackground(new Color(50, 100, 160));
printSpoolBtn.setForeground(Color.WHITE);
printSpoolBtn.addActionListener(e -> printSpool());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60));
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose());
bottomPanel.add(clearSpoolBtn);
bottomPanel.add(saveSpoolBtn);
bottomPanel.add(printSpoolBtn);
bottomPanel.add(Box.createHorizontalStrut(10));
bottomPanel.add(closeBtn);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
}
private JTextField createDarkField(String text, int cols) {
JTextField tf = new JTextField(text, cols);
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
tf.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(65, 65, 65)),
new EmptyBorder(2, 4, 2, 4)));
return tf;
}
private void startPrinterSession() {
String host = hostField.getText().trim();
if (host.isEmpty()) {
hostField.requestFocus();
return;
}
int port = 23;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException ignored) {}
config.setHost(host);
config.setPort(port);
config.setUseTls(tlsCheck.isSelected());
config.setTlsVerifyCert(verifyCertCheck.isSelected());
config.setCodePage((String) codePageCombo.getSelectedItem());
String plu = printerLuField.getText().trim();
config.setPrinterLuName(plu.isEmpty() ? null : plu);
String dlu = displayLuField.getText().trim();
config.setAssociatedDisplayLuName(dlu.isEmpty() ? null : dlu);
config.setDestinationType((PrinterConfig.DestinationType) destinationCombo.getSelectedItem());
config.setDestinationTarget(targetField.getText().trim());
if (printerSession != null) {
printerSession.close();
}
printerSession = new Telnet3270EP(config);
printerSession.addPrintListener(this);
connectBtn.setEnabled(false);
disconnectBtn.setEnabled(true);
statusLabel.setText("Status: Connecting...");
statusLabel.setForeground(new Color(255, 200, 80));
new Thread(() -> {
boolean success = printerSession.open();
if (!success) {
SwingUtilities.invokeLater(() -> {
connectBtn.setEnabled(true);
disconnectBtn.setEnabled(false);
statusLabel.setText("Status: Connection Failed");
statusLabel.setForeground(new Color(255, 80, 80));
});
}
}, "PrinterSession-Thread").start();
}
private void stopPrinterSession() {
if (printerSession != null) {
printerSession.close();
printerSession = null;
}
connectBtn.setEnabled(true);
disconnectBtn.setEnabled(false);
statusLabel.setText("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180));
sessionTypeLabel.setText("Session: -");
}
private void saveSpool() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Save Printer Spool Output");
fc.setSelectedFile(new File("spool_output.txt"));
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
try (FileOutputStream fos = new FileOutputStream(fc.getSelectedFile())) {
fos.write(spoolArea.getText().getBytes(StandardCharsets.UTF_8));
JOptionPane.showMessageDialog(this, "Spool saved successfully to " + fc.getSelectedFile().getName(),
"Saved", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Save error: " + e.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void printSpool() {
String text = spoolArea.getText();
if (text.isEmpty()) {
JOptionPane.showMessageDialog(this, "Spool buffer is empty.", "Nothing to Print", JOptionPane.INFORMATION_MESSAGE);
return;
}
PrinterJob job = PrinterJob.getPrinterJob();
job.setJobName("j3270 3287 Print Spool");
if (job.printDialog()) {
try {
spoolArea.print();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
}
}
}
// ========== PrintSessionListener ==========
@Override
public void onPrintJobStarted(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Status: Printing...");
statusLabel.setForeground(new Color(50, 205, 50));
});
}
@Override
public void onPrintJobData(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
if (printerSession != null && printerSession.getPD() != null) {
spoolArea.setText(printerSession.getPD().getCapturedText());
spoolArea.setCaretPosition(spoolArea.getDocument().getLength());
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
}
});
}
@Override
public void onPrintJobPageComplete(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
if (printerSession != null && printerSession.getPD() != null) {
spoolArea.setText(printerSession.getPD().getCapturedText());
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
}
});
}
@Override
public void onPrintJobComplete(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
if (printerSession != null && printerSession.getPD() != null) {
spoolArea.setText(printerSession.getPD().getCapturedText());
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
}
statusLabel.setText("Status: In Session (Idle)");
statusLabel.setForeground(new Color(50, 205, 50));
});
}
@Override
public void onPrinterStatusChanged(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
int code = event.getStatusCode();
if (code == PrinterConstants.STATUS_CONNECTED || code == PrinterConstants.STATUS_PRINTER_READY || code == PrinterConstants.STATUS_PRINTING || code == PrinterConstants.STATUS_JOB_COMPLETE || code == PrinterConstants.STATUS_PAGE_COMPLETE) {
String lu = printerSession != null ? printerSession.getAssignedLuName() : null;
statusLabel.setText("Status: Connected" + (lu != null ? " (" + lu + ")" : ""));
statusLabel.setForeground(new Color(50, 205, 50));
connectBtn.setEnabled(false);
disconnectBtn.setEnabled(true);
if (printerSession != null) {
short luType = printerSession.getActiveLuType();
if (luType == PrinterConstants.LU_TYPE_1_SCS) {
sessionTypeLabel.setText("Session: LU1 (SCS)");
} else if (luType == PrinterConstants.LU_TYPE_3_DS) {
sessionTypeLabel.setText("Session: LU3 (3270 DS)");
}
}
} else if (code == PrinterConstants.STATUS_DISCONNECTED) {
statusLabel.setText("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180));
connectBtn.setEnabled(true);
disconnectBtn.setEnabled(false);
sessionTypeLabel.setText("Session: -");
} else {
statusLabel.setText("Status: " + event.getMessage());
}
});
}
@Override
public void onPrinterError(PrintSessionEvent event) {
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Status: Error (" + event.getMessage() + ")");
statusLabel.setForeground(new Color(255, 80, 80));
});
}
@Override
public void dispose() {
stopPrinterSession();
super.dispose();
}
}
@@ -0,0 +1,253 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* Utility for exporting the 3270 Presentation Space to Plain Text, HTML, or PNG image.
*/
public class ScreenExporter {
public static void showExportDialog(Frame parent, Telnet3270Client client, TerminalPanel panel) {
if (client == null) {
JOptionPane.showMessageDialog(parent, "Connect to a host before exporting the screen.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Save Screen As...");
fc.addChoosableFileFilter(new FileNameExtensionFilter("PNG Image (*.png)", "png"));
fc.addChoosableFileFilter(new FileNameExtensionFilter("HTML Document (*.html)", "html", "htm"));
FileNameExtensionFilter txtFilter = new FileNameExtensionFilter("Plain Text (*.txt)", "txt");
fc.addChoosableFileFilter(txtFilter);
fc.setFileFilter(txtFilter);
fc.setSelectedFile(new File("screen_capture.txt"));
int result = fc.showSaveDialog(parent);
if (result == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
String name = file.getName().toLowerCase();
try {
if (name.endsWith(".png") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("PNG"))) {
if (!name.endsWith(".png")) file = new File(file.getAbsolutePath() + ".png");
exportToPng(panel, file);
} else if (name.endsWith(".html") || name.endsWith(".htm") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("HTML"))) {
if (!name.endsWith(".html") && !name.endsWith(".htm")) file = new File(file.getAbsolutePath() + ".html");
exportToHtml(client, file);
} else {
if (!name.endsWith(".txt")) file = new File(file.getAbsolutePath() + ".txt");
exportToText(client, file);
}
JOptionPane.showMessageDialog(parent, "Screen saved successfully to:\n" + file.getAbsolutePath(),
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(parent, "Failed to export screen: " + ex.getMessage(),
"Export Error", JOptionPane.ERROR_MESSAGE);
}
}
}
public static void exportToText(Telnet3270Client client, File file) throws IOException {
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
for (int r = 0; r < rows; r++) {
StringBuilder line = new StringBuilder();
for (int c = 0; c < cols; c++) {
ExtendedAttribute ea = sb.getCell(r * cols + c);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
}
// Strip trailing spaces for clean text output
int lastNonSpace = line.length() - 1;
while (lastNonSpace >= 0 && line.charAt(lastNonSpace) == ' ') {
lastNonSpace--;
}
writer.write(line.substring(0, lastNonSpace + 1));
writer.newLine();
}
}
}
public static void exportToHtml(Telnet3270Client client, File file) throws IOException {
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
writer.write("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>3270 Screen Capture</title>\n");
writer.write("<style>\n");
writer.write("body { background-color: #0a0a0a; color: #32cd32; font-family: 'Courier New', Courier, monospace; font-size: 14px; margin: 20px; }\n");
writer.write(".screen { background-color: #000; padding: 15px; border-radius: 4px; display: inline-block; box-shadow: 0 0 10px rgba(0,0,0,0.8); line-height: 1.2; }\n");
writer.write(".c-blue { color: #5078ff; }\n");
writer.write(".c-red { color: #ff3232; }\n");
writer.write(".c-pink { color: #ff82b4; }\n");
writer.write(".c-green { color: #32cd32; }\n");
writer.write(".c-turq { color: #40e0d0; }\n");
writer.write(".c-yellow { color: #ffff50; }\n");
writer.write(".c-white { color: #ffffff; }\n");
writer.write(".c-black { color: #000000; }\n");
writer.write(".c-orange { color: #ffa500; }\n");
writer.write(".c-purple { color: #b482ff; }\n");
writer.write(".c-palegreen { color: #90ee90; }\n");
writer.write(".c-paleturq { color: #afeeee; }\n");
writer.write(".c-grey { color: #aaaaaa; }\n");
writer.write(".bold { font-weight: bold; }\n");
writer.write(".underline { text-decoration: underline; }\n");
writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>");
byte currentFA = 0;
ExtendedAttribute currentFieldEa = null;
for (int r = 0; r < rows; r++) {
String currentStyle = null;
StringBuilder currentRun = new StringBuilder();
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getCell(baddr);
if (ea.isFieldAttribute()) {
currentFA = ea.fa;
currentFieldEa = ea;
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
currentStyle = null;
}
writer.write(' ');
continue;
}
if (faIsZero(currentFA & 0xFF)) {
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
currentStyle = null;
}
writer.write(' ');
continue;
}
char ch = ea.ucs4;
if (ch == 0 || ch == 0xFF) {
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
currentStyle = null;
}
writer.write(' ');
continue;
}
String colorClass = getHtmlColorClass(ea, currentFieldEa, currentFA);
boolean bold = faIsHigh(currentFA & 0xFF) || (ea.gr & GR_INTENSIFY) != 0;
boolean underline = (ea.gr & GR_UNDERLINE) != 0;
StringBuilder css = new StringBuilder();
if (!colorClass.isEmpty()) css.append(colorClass).append(" ");
if (bold) css.append("bold ");
if (underline) css.append("underline ");
String style = css.toString().trim();
if (currentStyle == null || !currentStyle.equals(style)) {
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
currentRun.setLength(0);
}
currentStyle = style;
}
currentRun.append(escapeHtml(ch));
}
if (currentRun.length() > 0) {
writeHtmlSpan(writer, currentStyle, currentRun.toString());
}
writer.newLine();
}
writer.write("</pre></div>\n</body>\n</html>");
}
}
private static void writeHtmlSpan(BufferedWriter writer, String style, String content) throws IOException {
if (style != null && !style.isEmpty()) {
writer.write("<span class=\"" + style + "\">" + content + "</span>");
} else {
writer.write(content);
}
}
private static String getHtmlColorClass(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
int fg = ea.fg != 0 ? (ea.fg & 0xFF) : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
if (fg >= 0xf0 && fg <= 0xff) {
switch (fg - 0xf0) {
case 1: return "c-blue";
case 2: return "c-red";
case 3: return "c-pink";
case 4: return "c-green";
case 5: return "c-turq";
case 6: return "c-yellow";
case 7: return "c-white";
case 8: return "c-black";
case 9: return "c-blue";
case 10: return "c-orange";
case 11: return "c-purple";
case 12: return "c-palegreen";
case 13: return "c-paleturq";
case 14: return "c-grey";
case 15: return "c-white";
}
}
if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue";
}
return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
}
private static String escapeHtml(char ch) {
switch (ch) {
case '<': return "&lt;";
case '>': return "&gt;";
case '&': return "&amp;";
case '"': return "&quot;";
default: return String.valueOf(ch);
}
}
public static void exportToPng(TerminalPanel panel, File file) throws IOException {
int w = panel.getWidth();
int h = panel.getHeight();
if (w <= 0 || h <= 0) {
Dimension pref = panel.getPreferredSize();
w = Math.max(pref.width, 800);
h = Math.max(pref.height, 500);
}
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = image.createGraphics();
panel.paint(g2);
g2.dispose();
ImageIO.write(image, "PNG", file);
}
}
@@ -0,0 +1,291 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* Dialog for composing and executing Host On-Demand ECL mnemonic keystroke scripts.
*/
public class ScriptDialog extends JDialog {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextArea scriptArea;
private JLabel statusLabel;
public ScriptDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "Run Keystrokes / ECL Script", false);
this.client = client;
this.terminalPanel = terminalPanel;
buildUI();
setSize(600, 480);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Header
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>"
+ "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>");
descLabel.setForeground(new Color(180, 180, 180));
descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Script area
scriptArea = new JTextArea();
scriptArea.setBackground(new Color(25, 25, 25));
scriptArea.setForeground(new Color(50, 205, 50));
scriptArea.setCaretColor(Color.WHITE);
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false);
JScrollPane scrollPane = new JScrollPane(scriptArea);
scrollPane.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 2, 2, 2)));
// Mnemonic helper buttons
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
tokenPanel.setOpaque(false);
tokenPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(60, 60, 60)), "Insert Keystroke Token"));
((TitledBorder) tokenPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
String[] tokens = {
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
"[eraseeof]", "[eraseinput]", "[newline]", "[dup]", "[fm]",
"[attn]", "[sysreq]", "[cursel]",
"[pf1]", "[pf2]", "[pf3]", "[pf4]", "[pf5]", "[pf6]",
"[pf7]", "[pf8]", "[pf9]", "[pf10]", "[pf11]", "[pf12]",
"[pf13]", "[pf14]", "[pf15]", "[pf16]", "[pf17]", "[pf18]",
"[pf19]", "[pf20]", "[pf21]", "[pf22]", "[pf23]", "[pf24]",
"[pa1]", "[pa2]", "[pa3]", "[up]", "[down]", "[left]", "[right]", "[home]"
};
for (String token : tokens) {
JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
btn.setBackground(new Color(50, 50, 50));
btn.setForeground(fg);
btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4));
btn.addActionListener(e -> insertToken(token));
tokenPanel.add(btn);
}
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
centerPanel.setOpaque(false);
centerPanel.add(scrollPane, BorderLayout.CENTER);
centerPanel.add(tokenPanel, BorderLayout.SOUTH);
mainPanel.add(centerPanel, BorderLayout.CENTER);
// Bottom
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Ready");
statusLabel.setForeground(new Color(160, 160, 160));
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load Script...");
loadBtn.setBackground(new Color(50, 50, 50));
loadBtn.setForeground(fg);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadScript());
JButton saveBtn = new JButton("Save Script...");
saveBtn.setBackground(new Color(50, 50, 50));
saveBtn.setForeground(fg);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveScript());
JButton runBtn = new JButton("Execute");
runBtn.setBackground(new Color(50, 120, 50));
runBtn.setForeground(Color.WHITE);
runBtn.setFont(labelFont);
runBtn.addActionListener(e -> executeScript());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60));
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(loadBtn);
buttonPanel.add(saveBtn);
buttonPanel.add(Box.createHorizontalStrut(10));
buttonPanel.add(runBtn);
buttonPanel.add(closeBtn);
bottomPanel.add(buttonPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
}
private void insertToken(String token) {
scriptArea.replaceSelection(token);
scriptArea.requestFocusInWindow();
}
private void executeScript() {
String script = scriptArea.getText();
if (script == null || script.trim().isEmpty()) {
statusLabel.setText("Script is empty.");
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("Not connected to host.");
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before executing scripts.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
statusLabel.setText("Executing keystrokes...");
new Thread(() -> {
try {
// Split multi-line scripts or process sequentially
String cleaned = script.replace("\r\n", "\n").replace("\r", "\n");
String[] lines = cleaned.split("\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (!line.isEmpty() && !line.startsWith("#") && !line.startsWith("//")) {
client.sendKeys(line);
Thread.sleep(100);
}
}
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Script execution completed successfully.");
if (terminalPanel != null) {
terminalPanel.repaint();
}
});
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Error executing script: " + ex.getMessage());
JOptionPane.showMessageDialog(this, "Execution error: " + ex.getMessage(),
"Script Error", JOptionPane.ERROR_MESSAGE);
});
}
}, "ScriptRunner").start();
}
private void loadScript() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Open Script File");
if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
try {
byte[] bytes = java.nio.file.Files.readAllBytes(fc.getSelectedFile().toPath());
scriptArea.setText(new String(bytes, StandardCharsets.UTF_8));
statusLabel.setText("Loaded script: " + fc.getSelectedFile().getName());
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Could not open file: " + e.getMessage(),
"Open Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void saveScript() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Save Script File");
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
try {
java.nio.file.Files.write(fc.getSelectedFile().toPath(),
scriptArea.getText().getBytes(StandardCharsets.UTF_8));
statusLabel.setText("Saved script: " + fc.getSelectedFile().getName());
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Could not save file: " + e.getMessage(),
"Save Error", JOptionPane.ERROR_MESSAGE);
}
}
}
/**
* FlowLayout subclass that fully supports word wrapping inside JScrollPanes / dialogs.
*/
private static class WrapLayout extends FlowLayout {
public WrapLayout(int align, int hgap, int vgap) {
super(align, hgap, vgap);
}
@Override
public Dimension preferredLayoutSize(Container target) {
return layoutSize(target, true);
}
@Override
public Dimension minimumLayoutSize(Container target) {
Dimension minimum = layoutSize(target, false);
minimum.width -= (getHgap() + 1);
return minimum;
}
private Dimension layoutSize(Container target, boolean preferred) {
synchronized (target.getTreeLock()) {
int targetWidth = target.getWidth();
if (targetWidth == 0) targetWidth = Integer.MAX_VALUE;
int hgap = getHgap();
int vgap = getVgap();
Insets insets = target.getInsets();
int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
int maxWidth = targetWidth - horizontalInsetsAndGap;
Dimension dim = new Dimension(0, 0);
int rowWidth = 0;
int rowHeight = 0;
int nmembers = target.getComponentCount();
for (int i = 0; i < nmembers; i++) {
Component m = target.getComponent(i);
if (m.isVisible()) {
Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
if (rowWidth + d.width > maxWidth) {
addRow(dim, rowWidth, rowHeight);
rowWidth = 0;
rowHeight = 0;
}
if (rowWidth != 0) rowWidth += hgap;
rowWidth += d.width;
rowHeight = Math.max(rowHeight, d.height);
}
}
addRow(dim, rowWidth, rowHeight);
dim.width += horizontalInsetsAndGap;
dim.height += insets.top + insets.bottom + vgap * 2;
return dim;
}
}
private void addRow(Dimension dim, int rowWidth, int rowHeight) {
dim.width = Math.max(dim.width, rowWidth);
if (dim.height > 0) dim.height += getVgap();
dim.height += rowHeight;
}
}
}
@@ -2,25 +2,26 @@ package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import java.awt.*;
/**
* Status bar displaying connection state, cursor position, timing, and lock
* status.
* Status bar displaying connection state, cursor position, timing, and lock status.
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
*/
public class StatusBar extends JPanel {
private final JLabel connectionStatus;
private final JLabel tlsStatus;
private final JLabel cursorPosition;
private final JLabel luName;
private final JLabel lockStatus;
private final JLabel fieldTypeStatus;
private final JLabel codePageInfo;
private final JLabel modelInfo;
private final JButton lpButton;
private final JLabel cursorPosition;
private Telnet3270Client client;
private TerminalPanel terminalPanel;
@@ -30,6 +31,7 @@ public class StatusBar extends JPanel {
private static final Color OIA_FG = new Color(50, 205, 50);
private static final Color OIA_DIM = new Color(80, 80, 80);
private static final Color OIA_ALERT = new Color(255, 80, 80);
private static final Color OIA_WARN = new Color(255, 200, 80);
public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
@@ -43,33 +45,24 @@ public class StatusBar extends JPanel {
tlsStatus = createLabel("", oiaFont, OIA_FG);
luName = createLabel("", oiaFont, OIA_FG);
lockStatus = createLabel("", oiaFont, OIA_ALERT);
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
codePageInfo = createLabel("", oiaFont, OIA_DIM);
modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
lpButton = new JButton("LightPen: OFF");
lpButton.setFont(oiaFont);
lpButton.setForeground(OIA_FG);
lpButton.setBackground(OIA_BG);
lpButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
lpButton.setFocusable(false);
lpButton.addActionListener(e -> {
if (terminalPanel != null) {
terminalPanel.toggleLightPen();
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
}
});
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG);
add(Box.createHorizontalStrut(6));
add(connectionStatus);
add(Box.createHorizontalStrut(10));
add(tlsStatus);
add(Box.createHorizontalStrut(12));
add(Box.createHorizontalStrut(10));
add(luName);
add(Box.createHorizontalStrut(12));
add(lockStatus);
add(Box.createHorizontalStrut(12));
add(lpButton);
add(Box.createHorizontalStrut(10));
add(fieldTypeStatus);
add(Box.createHorizontalGlue());
add(codePageInfo);
add(Box.createHorizontalStrut(12));
add(modelInfo);
add(Box.createHorizontalStrut(12));
add(cursorPosition);
@@ -86,15 +79,22 @@ public class StatusBar extends JPanel {
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
this.client = client;
this.terminalPanel = terminalPanel;
if (terminalPanel != null) {
terminalPanel.setOnLightPenToggle(this::updateStatus);
}
updateStatus();
}
public void updateStatus() {
if (client == null)
if (client == null) {
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM);
tlsStatus.setText("");
luName.setText("");
lockStatus.setText("");
fieldTypeStatus.setText("");
codePageInfo.setText("");
modelInfo.setText("");
cursorPosition.setText("001/001");
return;
}
// Connection state
ConnectionState state = client.getConnectionState();
@@ -128,7 +128,7 @@ public class StatusBar extends JPanel {
break;
case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound");
connectionStatus.setForeground(new Color(255, 255, 80));
connectionStatus.setForeground(OIA_WARN);
break;
default:
connectionStatus.setText(state.name());
@@ -156,16 +156,41 @@ public class StatusBar extends JPanel {
tlsStatus.setToolTipText(null);
}
// LU name
// LU Name
String lu = "";
if (client.getConnectionState().isTn3270e()) {
// lu would come from FSM
if (client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null && !client.getTelnetFSM().getConnectedLu().isEmpty()) {
lu = "LU:" + client.getTelnetFSM().getConnectedLu();
} else if (client.getConfig().getLuName() != null && !client.getConfig().getLuName().isEmpty()) {
lu = "LU:" + client.getConfig().getLuName();
}
luName.setText(lu);
// Lock status
if (client.getInputProcessor().isKeyboardLocked()) {
lockStatus.setText("X SYSTEM");
// Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited();
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
switch (inhibit) {
case ECLConstants.INHIBIT_SYSTEM_LOCK:
lockStatus.setText("X SYSTEM");
break;
case ECLConstants.INHIBIT_COMM_CHECK:
lockStatus.setText("X COMM");
break;
case ECLConstants.INHIBIT_NUMERIC_ONLY:
lockStatus.setText("X NUM");
break;
case ECLConstants.INHIBIT_PROTECTED_FIELD:
lockStatus.setText("X PROT");
break;
case ECLConstants.INHIBIT_OVERFLOW:
lockStatus.setText("X >");
break;
case ECLConstants.INHIBIT_OPERATOR_DUE:
lockStatus.setText("X OP");
break;
default:
lockStatus.setText("X LOCKED");
break;
}
lockStatus.setForeground(OIA_ALERT);
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
@@ -174,19 +199,34 @@ public class StatusBar extends JPanel {
lockStatus.setText("");
}
// Model info
modelInfo.setText(client.getConfig().getModel().getTerminalType());
// Field status: Numeric vs Alphanumeric
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(OIA_WARN);
} else {
fieldTypeStatus.setText("ALPHA");
fieldTypeStatus.setForeground(OIA_DIM);
}
} else {
fieldTypeStatus.setText("");
}
// Cursor position
// Active Code Page
String cp = client.getCodePage();
codePageInfo.setText(cp != null ? "CP" + cp : "");
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
// Model & Dimensions info
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
// Cursor position and buffer address
int curAddr = sb.getCursorAddress();
int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1;
cursorPosition.setText(String.format("%03d/%03d", row, col));
// Light Pen indicator — sync with actual state
if (terminalPanel != null) {
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
lpButton.setForeground(terminalPanel.isLightPenMode() ? new Color(255, 255, 80) : OIA_FG);
}
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
}
}
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.graphics.GocaConstants;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
@@ -14,10 +15,10 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Custom JPanel that renders the 3270 screen buffer.
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
* attributes.
* Supports colors, bold, underline, reverse, blink, GOCA vector graphics,
* programmed symbols, search highlighting, and all 3278/3279 attributes.
*/
public class TerminalPanel extends JPanel {
public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private Telnet3270Client client;
@@ -62,7 +63,12 @@ public class TerminalPanel extends JPanel {
private boolean isDragging = false;
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
// ========== Resize guard ==========
// ========== Search Highlight state ==========
private int searchHighlightAddr = -1;
private int searchHighlightLen = 0;
private static final Color SEARCH_HIGHLIGHT_COLOR = new Color(255, 215, 0, 120);
// ========== Graphics / Resize guard ==========
private boolean lightPenMode = false;
private long lastGraphicsUpdateCount = -1;
private java.awt.image.BufferedImage cachedGraphicsImage = null;
@@ -70,7 +76,6 @@ public class TerminalPanel extends JPanel {
/**
* Compute the horizontal render offset to center the grid within the panel.
* Any leftover pixels (from integer font sizing) are split evenly.
*/
private int getRenderOffsetX() {
int termCols = 80;
@@ -82,7 +87,6 @@ public class TerminalPanel extends JPanel {
/**
* Compute the vertical render offset to center the grid within the panel.
* Must use getDisplayRows() to match paintComponent's iteration.
*/
private int getRenderOffsetY() {
int termRows = 24;
@@ -94,17 +98,17 @@ public class TerminalPanel extends JPanel {
// Default Host color mapping
public static final Color[] DEFAULT_HOST_COLORS = {
new Color(0, 0, 0), // 0: Neutral Black
new Color(80, 120, 255), // 1: Blue
new Color(255, 50, 50), // 2: Red
new Color(0, 0, 0), // 0: Neutral Black
new Color(80, 120, 255), // 1: Blue
new Color(255, 50, 50), // 2: Red
new Color(255, 130, 180), // 3: Pink
new Color(50, 205, 50), // 4: Green
new Color(64, 224, 208), // 5: Turquoise
new Color(255, 255, 80), // 6: Yellow
new Color(50, 205, 50), // 4: Green
new Color(64, 224, 208), // 5: Turquoise
new Color(255, 255, 80), // 6: Yellow
new Color(255, 255, 255), // 7: Neutral White
new Color(0, 0, 0), // 8: Black
new Color(30, 60, 180), // 9: Deep Blue
new Color(255, 165, 0), // 10: Orange
new Color(0, 0, 0), // 8: Black
new Color(30, 60, 180), // 9: Deep Blue
new Color(255, 165, 0), // 10: Orange
new Color(180, 130, 255), // 11: Purple
new Color(144, 238, 144), // 12: Pale Green
new Color(175, 238, 238), // 13: Pale Turquoise
@@ -128,8 +132,6 @@ public class TerminalPanel extends JPanel {
setDoubleBuffered(true);
setFocusTraversalKeysEnabled(false);
// Use key bindings instead of KeyListener for reliable key handling
// This avoids focus/event issues with JScrollPane
setOpaque(true);
setupKeyBindings();
setupFont();
@@ -160,12 +162,6 @@ public class TerminalPanel extends JPanel {
col = Math.max(0, Math.min(col, displayCols - 1));
row = Math.max(0, Math.min(row, displayRows - 1));
// IMPORTANT ARCHITECTURE NOTE:
// 1. In GDDM Graphic Cursor Mode (3179G / GOCA), mouse events represent graphic light-pen touches.
// Text drag-selection is explicitly suppressed so blue selection boxes do not artifact over graphics.
// 2. In Text Light Pen / Selectable Field mode (per IBM Host On-Demand MouseMgr.java / PS3270.java),
// clicks on detectable fields (faIsSelectable) trigger field selection or cursor select.
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
boolean isSelectableField = false;
if (sb.isFormatted()) {
@@ -267,16 +263,7 @@ public class TerminalPanel extends JPanel {
py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
int gx = client.getGocaDecoder().getGraphicCursorX();
int gy = client.getGocaDecoder().getGraphicCursorY();
int hitSeg = client.getGocaDecoder().findPickedSegment(gx, gy);
int hitTag = client.getGocaDecoder().getSegmentTag(hitSeg);
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
System.err.println(String.format(
"TerminalPanel.mouseReleased: mouse=(%d, %d) offset=(%d, %d) grid=(%dx%d) px=(%d, %d) goca=(%d, %d) hitSeg=%d hitTag=%d btn=%d",
e.getX(), e.getY(), ox, oy, gridW, gridH, px, py, gx, gy, hitSeg, hitTag, button
));
client.getInputProcessor().sendGraphicMouseAid(
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
button,
@@ -288,9 +275,6 @@ public class TerminalPanel extends JPanel {
}
// Automatic Light-Pen / Selectable Field detection on mouse click:
// Per IBM Host On-Demand (MouseMgr.java / PS3270.java) and 3270 specifications:
// If the clicked screen location belongs to a detectable field (faIsSelectable and not faIsZero),
// automatically process light-pen / cursor selection (e.g. immediate selection, Enter, or toggling ? -> >).
if (sb.isFormatted()) {
int faPos = sb.findFieldAttribute(clickAddr);
if (faPos >= 0) {
@@ -344,23 +328,42 @@ public class TerminalPanel extends JPanel {
// ========== Selection / Copy-Paste ==========
private void clearSelection() {
public void clearSelection() {
selectionStartRow = -1;
selectionStartCol = -1;
selectionEndRow = -1;
selectionEndCol = -1;
repaint();
}
private boolean hasSelection() {
public void selectAll() {
if (client == null) return;
ScreenBuffer sb = client.getScreenBuffer();
selectionStartRow = 0;
selectionStartCol = 0;
selectionEndRow = sb.getDisplayRows() - 1;
selectionEndCol = sb.getDisplayCols() - 1;
repaint();
}
public void setSelectionRange(int startAddr, int endAddr) {
if (client == null) return;
ScreenBuffer sb = client.getScreenBuffer();
int cols = sb.getDisplayCols();
if (cols <= 0) return;
selectionStartRow = startAddr / cols;
selectionStartCol = startAddr % cols;
selectionEndRow = endAddr / cols;
selectionEndCol = endAddr % cols;
repaint();
}
public boolean hasSelection() {
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
}
/**
* Get the selected text from the screen buffer.
* In block mode: rectangular selection with newlines between rows.
* In line mode: stream selection, flowing left-to-right, top-to-bottom.
*/
public String getSelectedText() {
if (!hasSelection() || client == null) return "";
ScreenBuffer sb = client.getScreenBuffer();
@@ -371,7 +374,6 @@ public class TerminalPanel extends JPanel {
int c1, c2;
if (blockSelectMode) {
// Block mode: rectangular selection
c1 = Math.min(selectionStartCol, selectionEndCol);
c2 = Math.max(selectionStartCol, selectionEndCol);
@@ -391,7 +393,6 @@ public class TerminalPanel extends JPanel {
}
return result.toString();
} else {
// Line/stream mode: flow from start to end
int startAddr, endAddr;
if (selectionStartRow < selectionEndRow ||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
@@ -422,7 +423,7 @@ public class TerminalPanel extends JPanel {
}
}
private void copySelection() {
public void copySelection() {
String text = getSelectedText();
if (!text.isEmpty()) {
StringSelection ss = new StringSelection(text);
@@ -430,7 +431,7 @@ public class TerminalPanel extends JPanel {
}
}
private void pasteClipboard() {
public void pasteClipboard() {
if (client == null || !client.getConnectionState().isFullSession()) return;
try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
@@ -438,8 +439,6 @@ public class TerminalPanel extends JPanel {
if (text != null) {
for (char ch : text.toCharArray()) {
if (ch == '\n' || ch == '\r') {
// Skip newlines in paste — user may paste multi-line text
// but 3270 fields don't wrap the same way
continue;
}
if (ch >= 0x20 && ch != 0x7F) {
@@ -448,9 +447,35 @@ public class TerminalPanel extends JPanel {
}
refreshScreen();
}
} catch (Exception ex) {
// Clipboard not available or wrong type — silently ignore
}
} catch (Exception ignored) {}
}
public void pasteLineWrap(String text, int endCol, boolean wordWrap) {
if (client == null || text == null || text.isEmpty()) return;
int curPos = client.getScreenBuffer().getCursorAddress();
client.getPS().pasteLineWrap(text, curPos, endCol, wordWrap);
refreshScreen();
}
// ========== Search Highlighting ==========
public void setSearchHighlight(int addr, int len) {
this.searchHighlightAddr = addr;
this.searchHighlightLen = len;
repaint();
}
public void clearSearchHighlight() {
this.searchHighlightAddr = -1;
this.searchHighlightLen = 0;
repaint();
}
private boolean isCellSearchHighlighted(int row, int col) {
if (searchHighlightAddr < 0 || searchHighlightLen <= 0 || client == null) return false;
int cols = client.getScreenBuffer().getDisplayCols();
int addr = row * cols + col;
return addr >= searchHighlightAddr && addr < searchHighlightAddr + searchHighlightLen;
}
private void showContextMenu(MouseEvent e) {
@@ -466,6 +491,10 @@ public class TerminalPanel extends JPanel {
pasteItem.addActionListener(ev -> pasteClipboard());
popup.add(pasteItem);
JMenuItem selectAllItem = new JMenuItem("Select All");
selectAllItem.addActionListener(ev -> selectAll());
popup.add(selectAllItem);
popup.addSeparator();
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
@@ -489,7 +518,6 @@ public class TerminalPanel extends JPanel {
int c2 = Math.max(selectionStartCol, selectionEndCol);
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
} else {
// Stream mode
int cols = 80;
if (client != null) cols = client.getScreenBuffer().getCols();
int addr = row * cols + col;
@@ -508,12 +536,6 @@ public class TerminalPanel extends JPanel {
// ========== Font auto-resize ==========
/**
* Auto-fit the font size to fill the current panel dimensions
* while respecting the terminal model's character grid.
* Any leftover pixels are handled by centering the grid
* (see getRenderOffsetX/Y).
*/
private void autoFitFont() {
int panelW = getWidth();
int panelH = getHeight();
@@ -527,12 +549,10 @@ public class TerminalPanel extends JPanel {
termRows = sb.getDisplayRows();
}
// Use minimal padding for the fit calculation
int availW = panelW - 2 * padding;
int availH = panelH - 2 * padding;
if (availW <= 0 || availH <= 0) return;
// Find the largest font size where the grid fits
int bestSize = 8;
for (int testSize = 8; testSize <= 72; testSize++) {
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
@@ -553,15 +573,9 @@ public class TerminalPanel extends JPanel {
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
updateCellSize();
}
// No window snap — any leftover pixels are centered via getRenderOffsetX/Y
repaint();
}
/**
* Called by J3270App when it needs to pack the frame (screen size changed,
* connection established, etc.). Sets the resize guard to prevent
* autoFitFont from firing during the pack.
*/
public void guardedPack() {
resizeGuard = true;
revalidate();
@@ -572,16 +586,10 @@ public class TerminalPanel extends JPanel {
SwingUtilities.invokeLater(() -> resizeGuard = false);
}
/**
* Use InputMap/ActionMap (key bindings) instead of KeyListener.
* This works reliably even inside a JScrollPane — the WHEN_FOCUSED
* condition ensures our panel receives all key events when focused.
*/
private void bindKeyToMap(InputMap im, String action, String bindingStr) {
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
return;
}
// Support multiple bindings separated by commas
String[] bindings = bindingStr.split(",");
for (String binding : bindings) {
binding = binding.trim();
@@ -601,7 +609,6 @@ public class TerminalPanel extends JPanel {
im.clear();
am.clear();
// Block the scroll pane from handling Tab, arrows, Page keys
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
@@ -611,25 +618,16 @@ public class TerminalPanel extends JPanel {
}
// PF keys 1-24
// PF1-12 default to F1-F12, PF13-24 default to shift F1-shift F12
for (int i = 1; i <= 24; i++) {
String defaultBinding;
if (i <= 12) {
defaultBinding = "F" + i;
} else {
defaultBinding = "shift F" + (i - 12);
}
String defaultBinding = i <= 12 ? ("F" + i) : ("shift F" + (i - 12));
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
bindKeyToMap(im, "PF" + i, binding);
}
// PA keys: Alt+1, Alt+2, Alt+3
String pa1Def = "alt 1";
String pa2Def = "alt 2";
String pa3Def = "alt 3";
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
// PA keys
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", "alt 1"));
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", "alt 2"));
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", "alt 3"));
// Clear
bindKeyToMap(im, "CLEAR", haus.nightmare.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
@@ -643,13 +641,14 @@ public class TerminalPanel extends JPanel {
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q"));
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
// Copy/Paste/Lightpen bindings
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, shortcutMask), "j3270-SELECTALL");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
// Create actions for all bound keys
// Action map implementations
am.put("j3270-ENTER", createAction(this::handleEnter));
am.put("j3270-ESCAPE", createAction(this::handleReset));
am.put("j3270-TAB", createAction(() -> handleTab(false)));
@@ -674,9 +673,9 @@ public class TerminalPanel extends JPanel {
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
// Copy/Paste actions
am.put("j3270-COPY", createAction(this::copySelection));
am.put("j3270-PASTE", createAction(this::pasteClipboard));
am.put("j3270-SELECTALL", createAction(this::selectAll));
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
for (int i = 1; i <= 24; i++) {
@@ -688,22 +687,29 @@ public class TerminalPanel extends JPanel {
am.put("j3270-PA2", createAction(() -> handlePA(2)));
am.put("j3270-PA3", createAction(() -> handlePA(3)));
// For printable character input, we override processKeyEvent
enableEvents(AWTEvent.KEY_EVENT_MASK);
}
@Override
protected void processKeyEvent(KeyEvent e) {
// Handle character typing via processKeyEvent to capture ALL typed chars
if (e.getID() == KeyEvent.KEY_TYPED) {
char ch = e.getKeyChar();
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
if (client != null && client.getConnectionState().isFullSession()) {
client.typeCharacter(ch);
refreshScreen();
e.consume();
return;
if (client != null) {
ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
try {
client.sendNVTChar(ch);
} catch (Exception ignored) {}
e.consume();
return;
} else if (state.isFullSession()) {
client.typeCharacter(ch);
refreshScreen();
e.consume();
return;
}
}
}
}
@@ -722,14 +728,23 @@ public class TerminalPanel extends JPanel {
// ========== Key action handlers ==========
private void handleEnter() {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendEnter();
refreshScreen();
if (client != null) {
ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
try {
client.sendNVTString("\r\n");
} catch (Exception ignored) {}
} else if (state.isFullSession()) {
client.sendEnter();
refreshScreen();
}
}
}
private void handleReset() {
if (client != null) {
clearSearchHighlight();
clearSelection();
client.reset();
refreshScreen();
}
@@ -737,10 +752,8 @@ public class TerminalPanel extends JPanel {
private void handleTab(boolean shift) {
if (client != null && client.getConnectionState().isFullSession()) {
if (shift)
client.backTab();
else
client.tab();
if (shift) client.backTab();
else client.tab();
refreshScreen();
}
}
@@ -748,21 +761,11 @@ public class TerminalPanel extends JPanel {
private void handleCursor(String dir) {
if (client != null && client.getConnectionState().isFullSession()) {
switch (dir) {
case "up":
client.cursorUp();
break;
case "down":
client.cursorDown();
break;
case "left":
client.cursorLeft();
break;
case "right":
client.cursorRight();
break;
case "home":
client.cursorHome();
break;
case "up": client.cursorUp(); break;
case "down": client.cursorDown(); break;
case "left": client.cursorLeft(); break;
case "right": client.cursorRight(); break;
case "home": client.cursorHome(); break;
}
refreshScreen();
}
@@ -797,9 +800,16 @@ public class TerminalPanel extends JPanel {
}
private void handleBackspace() {
if (client != null && client.getConnectionState().isFullSession()) {
client.getInputProcessor().backspace();
refreshScreen();
if (client != null) {
ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
try {
client.sendNVTChar('\b');
} catch (Exception ignored) {}
} else if (state.isFullSession()) {
client.getInputProcessor().backspace();
refreshScreen();
}
}
}
@@ -870,12 +880,11 @@ public class TerminalPanel extends JPanel {
}
}
private void refreshScreen() {
public void refreshScreen() {
if (client != null) {
client.getScreenBuffer().updateDisplaySnapshot();
}
repaint();
// Notify parent to update status bar too
Container parent = getParent();
while (parent != null) {
if (parent instanceof JFrame) {
@@ -892,7 +901,6 @@ public class TerminalPanel extends JPanel {
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
// Fallback
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
}
updateCellSize();
@@ -1015,9 +1023,6 @@ public class TerminalPanel extends JPanel {
public Dimension getPreferredSize() {
if (client != null) {
ScreenBuffer sb = client.getScreenBuffer();
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
// When the host switches to alternate screen, onScreenSizeChanged fires
// and the frame re-packs.
int displayCols = sb.getDisplayCols();
int displayRows = sb.getDisplayRows();
return new Dimension(displayCols * cellWidth + padding * 2,
@@ -1042,11 +1047,9 @@ public class TerminalPanel extends JPanel {
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
// Clear entire panel with background color
g2.setColor(bgColor);
g2.fillRect(0, 0, getWidth(), getHeight());
// Compute centered offsets for the grid
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
@@ -1077,7 +1080,6 @@ public class TerminalPanel extends JPanel {
}
}
// Track current field attribute for monochrome color decisions
byte currentFA = 0;
ExtendedAttribute currentFieldEa = null;
@@ -1089,7 +1091,6 @@ public class TerminalPanel extends JPanel {
int x = ox + col * cellWidth;
int y = oy + row * cellHeight;
// Determine colors and attributes
Color fgColor;
Color bgColor;
boolean bold = false;
@@ -1099,7 +1100,6 @@ public class TerminalPanel extends JPanel {
if (ea.isFieldAttribute()) {
currentFA = ea.fa;
currentFieldEa = ea;
// Selection highlight on field attribute cells
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
@@ -1119,20 +1119,14 @@ public class TerminalPanel extends JPanel {
// Graphics rendition
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
if (gr != 0) {
if ((gr & GR_INTENSIFY) != 0)
bold = true;
if ((gr & GR_UNDERLINE) != 0)
underline = true;
if ((gr & GR_REVERSE) != 0)
reverse = true;
if ((gr & GR_INTENSIFY) != 0) bold = true;
if ((gr & GR_UNDERLINE) != 0) underline = true;
if ((gr & GR_REVERSE) != 0) reverse = true;
}
// Field attribute implicit intensify
if (faIsHigh(currentFA & 0xFF))
bold = true;
if (faIsHigh(currentFA & 0xFF)) bold = true;
// Handle invisible fields (zero intensity / password fields)
// Modern UX: render '*' for typed characters so user sees length/digit count
// Password fields
if (faIsZero(currentFA & 0xFF)) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
@@ -1149,14 +1143,12 @@ public class TerminalPanel extends JPanel {
continue;
}
// Apply reverse video
if (reverse) {
Color tmp = fgColor;
fgColor = bgColor;
bgColor = tmp;
}
// Draw background only if different from default panel bgColor or if inverted
if (!bgColor.equals(this.bgColor) || reverse) {
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
@@ -1192,13 +1184,18 @@ public class TerminalPanel extends JPanel {
}
}
// Draw underline
if (underline) {
g2.setColor(fgColor);
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
}
// Draw search highlight
if (isCellSearchHighlighted(row, col)) {
g2.setColor(SEARCH_HIGHLIGHT_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
}
// Draw selection highlight
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
@@ -1207,7 +1204,7 @@ public class TerminalPanel extends JPanel {
}
}
// Draw Graphic Cursor (Light-Pen / interactive graphics pointer) if active
// Draw Graphic Cursor if active
if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
int gocaX = client.getGocaDecoder().getGraphicCursorX();
int gocaY = client.getGocaDecoder().getGraphicCursorY();
@@ -1222,7 +1219,6 @@ public class TerminalPanel extends JPanel {
g2.setColor(Color.WHITE);
g2.setXORMode(Color.BLACK);
// Draw a crosshair cursor for the light-pen / graphic cursor
g2.drawLine(px - 6, py, px + 6, py);
g2.drawLine(px, py - 6, px, py + 6);
g2.setPaintMode();
@@ -1241,13 +1237,6 @@ public class TerminalPanel extends JPanel {
g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.setPaintMode();
}
// Draw Light Pen mode indicator
if (lightPenMode) {
g2.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
g2.setColor(new Color(50, 255, 50));
g2.drawString("LP", ox + 2, oy + rows * cellHeight + 14);
}
}
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
@@ -1267,8 +1256,6 @@ public class TerminalPanel extends JPanel {
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
if (bg >= 0xf0 && bg <= 0xff) {
int idx = bg - 0xf0;
// Neutral black (0xf0) and black (0xf8) should use window bgColor
// to avoid visible seams between field bg and window bg
if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) {
return bgColor;
}
@@ -1288,22 +1275,26 @@ public class TerminalPanel extends JPanel {
if (blinkTimer != null)
blinkTimer.stop();
}
private Runnable onLightPenToggle;
public void setOnLightPenToggle(Runnable callback) {
this.onLightPenToggle = callback;
}
public void toggleLightPen() {
this.lightPenMode = !this.lightPenMode;
System.out.println("Light Pen mode: " + (this.lightPenMode ? "ON" : "OFF"));
if (onLightPenToggle != null) {
onLightPenToggle.run();
}
repaint();
}
public boolean isLightPenMode() {
return this.lightPenMode;
}
@Override
public int print(Graphics g, java.awt.print.PageFormat pageFormat, int pageIndex) {
if (pageIndex > 0) return NO_SUCH_PAGE;
Graphics2D g2d = (Graphics2D) g;
g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
double scaleX = pageFormat.getImageableWidth() / (double) Math.max(1, getWidth());
double scaleY = pageFormat.getImageableHeight() / (double) Math.max(1, getHeight());
double scale = Math.min(scaleX, scaleY);
g2d.scale(scale, scale);
paint(g2d);
return PAGE_EXISTS;
}
}
@@ -0,0 +1,59 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.*;
public class ScreenExporterTest {
private Telnet3270Client client;
@BeforeEach
public void setUp() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
client = new Telnet3270Client(config);
client.getInputProcessor().setKeyboardLocked(false);
}
@Test
public void testPlainTextExport() throws IOException {
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
File tempTxt = File.createTempFile("export_test_", ".txt");
tempTxt.deleteOnExit();
ScreenExporter.exportToText(client, tempTxt);
assertTrue(tempTxt.exists());
assertTrue(tempTxt.length() > 0);
String content = Files.readString(tempTxt.toPath());
assertTrue(content.contains("TSO/E LOGON SCREEN - WELCOME"));
}
@Test
public void testHtmlExport() throws IOException {
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
File tempHtml = File.createTempFile("export_test_", ".html");
tempHtml.deleteOnExit();
ScreenExporter.exportToHtml(client, tempHtml);
assertTrue(tempHtml.exists());
assertTrue(tempHtml.length() > 0);
String html = Files.readString(tempHtml.toPath());
assertTrue(html.contains("<!DOCTYPE html>"));
assertTrue(html.contains("TSO/E LOGON SCREEN - WELCOME"));
assertTrue(html.contains("font-family: 'Courier New', Courier, monospace"));
}
}
@@ -0,0 +1,54 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import static org.junit.jupiter.api.Assertions.*;
public class StatusBarTest {
@Test
public void testStatusBarDoesNotContainLightPenButton() {
StatusBar statusBar = new StatusBar();
// Ensure no JButton exists in StatusBar components (lightpen button removed)
for (Component comp : statusBar.getComponents()) {
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
}
}
@Test
public void testStatusBarUpdatesWithClient() {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
config.setLuName("TSU001");
config.setCodePage("1047");
Telnet3270Client client = new Telnet3270Client(config);
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
// Check labels
boolean foundCodePage = false;
boolean foundModel = false;
for (Component comp : statusBar.getComponents()) {
if (comp instanceof JLabel) {
JLabel label = (JLabel) comp;
if ("CP1047".equals(label.getText())) {
foundCodePage = true;
}
if (label.getText() != null && label.getText().contains("3279-4")) {
foundModel = true;
}
}
}
assertTrue(foundCodePage, "Should display CP1047");
assertTrue(foundModel, "Should display IBM-3279-4 model info");
}
}
@@ -18,7 +18,16 @@ public class ConnectionConfig {
private int nopIntervalSeconds = 0;
private String terminalName = null; // override terminal type string
private boolean tn3270eEnabled = true;
private boolean tcpNoDelay = true;
private boolean soKeepAlive = true;
private int soTimeoutMs = 0;
private java.util.List<String> luNames = new java.util.ArrayList<>();
private boolean dynamicModel = false;
private int dynamicRows = 24;
private int dynamicCols = 80;
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
private String codePage = "037";
private String associatedPrinterLu = null;
public ConnectionConfig() {}
@@ -87,9 +96,44 @@ public class ConnectionConfig {
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
}
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) {
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
}
public String getTerminalName() { return terminalName; }
public void setTerminalName(String name) { this.terminalName = name; }
public boolean isTcpNoDelay() { return tcpNoDelay; }
public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; }
public boolean isSoKeepAlive() { return soKeepAlive; }
public void setSoKeepAlive(boolean soKeepAlive) { this.soKeepAlive = soKeepAlive; }
public int getSoTimeoutMs() { return soTimeoutMs; }
public void setSoTimeoutMs(int soTimeoutMs) { this.soTimeoutMs = soTimeoutMs; }
public java.util.List<String> getLuNames() { return luNames; }
public void setLuNames(java.util.List<String> luNames) {
this.luNames = (luNames != null) ? new java.util.ArrayList<>(luNames) : new java.util.ArrayList<>();
}
public void addLuName(String luName) {
if (luName != null && !luName.trim().isEmpty()) {
this.luNames.add(luName.trim());
}
}
public boolean isDynamicModel() { return dynamicModel; }
public void setDynamicModel(boolean dynamicModel) { this.dynamicModel = dynamicModel; }
public int getDynamicRows() { return dynamicRows; }
public int getDynamicCols() { return dynamicCols; }
public void setDynamicDimensions(int rows, int cols) {
this.dynamicModel = true;
this.dynamicRows = rows;
this.dynamicCols = cols;
}
/**
* 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"), or standard "host:port" formats.
@@ -166,6 +210,25 @@ public class ConnectionConfig {
if (terminalName != null) {
return terminalName;
}
if (dynamicModel) {
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
}
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
}
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
pcfg.setUseTls(useTls);
pcfg.setTlsVerifyCert(tlsVerifyCert);
pcfg.setCertificateVerifier(certificateVerifier);
pcfg.setSslProtocol(sslProtocol);
pcfg.setConnectTimeoutMs(connectTimeoutMs);
pcfg.setPrinterLuName(associatedPrinterLu);
pcfg.setAssociatedDisplayLuName(luName);
pcfg.setCodePage(codePage);
return pcfg;
}
}
@@ -39,22 +39,35 @@ public class Telnet3270Client {
private final DataStreamProcessor dsProcessor;
private final TelnetFSM fsm;
private final InputProcessor inputProcessor;
private final haus.nightmare.lib3270j.ecl.ECLPS ps;
private final haus.nightmare.lib3270j.ecl.ECLOIA oia;
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
private TelnetConnection connection;
public Telnet3270Client(ConnectionConfig config) {
this.config = config;
this.translator = new EbcdicTranslator();
this.translator = new EbcdicTranslator(config.getCodePage());
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor);
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
// Wire screen update to ECLXfer for CUT mode screen tracking
addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
@Override public void onScreenUpdated() { xfer.onScreenUpdated(); }
@Override public void onScreenSizeChanged(int rows, int cols) {}
@Override public void onSoundAlarm() {}
});
}
/**
@@ -113,6 +126,12 @@ public class Telnet3270Client {
/** Get the EBCDIC translator. */
public EbcdicTranslator getTranslator() { return translator; }
/** Get the active Code Page identifier. */
public String getCodePage() { return translator.getCodePageId(); }
/** Set the active Code Page identifier. */
public void setCodePage(String codePageId) { translator.setCodePage(codePageId); }
/** Get the current connection state. */
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
@@ -125,6 +144,21 @@ public class Telnet3270Client {
/** Get the connection config. */
public ConnectionConfig getConfig() { return config; }
/** Get the ECL Presentation Space API. */
public haus.nightmare.lib3270j.ecl.ECLPS getPS() { return ps; }
/** Get the ECL Operator Information Area API. */
public haus.nightmare.lib3270j.ecl.ECLOIA getOIA() { return oia; }
/** Get the ECL File Transfer API. */
public haus.nightmare.lib3270j.ecl.ECLXfer getXfer() { return xfer; }
/** Get the list of all fields currently on screen. */
public haus.nightmare.lib3270j.ecl.ECLFieldList getFieldList() { return ps.getFieldList(); }
/** Send IBM ECL bracketed mnemonic keystrokes (e.g. "USER[tab]PASS[enter]"). */
public void sendKeys(String keys) { inputProcessor.sendKeys(keys); }
/** Set a custom or interactive TLS certificate verifier callback. */
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
config.setCertificateVerifier(verifier);
@@ -132,7 +166,27 @@ public class Telnet3270Client {
/** Get the active SSLSession if connected over TLS, or null. */
public javax.net.ssl.SSLSession getSslSession() {
return connection.getSslSession();
return connection != null ? connection.getSslSession() : null;
}
/** Get the NVT processor for ASCII / ANSI terminal processing. */
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
return fsm.getNvtProcessor();
}
/** Get the Telnet state machine. */
public TelnetFSM getTelnetFSM() {
return fsm;
}
/** Send an NVT ASCII character in NVT mode. */
public void sendNVTChar(char c) throws IOException {
fsm.sendNVTChar(c);
}
/** Send an NVT ASCII string in NVT mode. */
public void sendNVTString(String s) throws IOException {
fsm.sendNVTString(s);
}
// ========== Convenience input methods ==========
@@ -0,0 +1,158 @@
package haus.nightmare.lib3270j.charset;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Base implementation for Single-Byte Character Set (SBCS) EBCDIC Code Pages.
*/
public abstract class AbstractCodePage implements CodePage {
protected final String id;
protected final String description;
protected final int cpgid;
protected final int cgcsgid;
protected final int[] toUnicode = new int[256];
protected final int[] toEbcdic = new int[256];
protected final Map<Character, Integer> extendedToEbcdic = new HashMap<>();
public AbstractCodePage(String id, String description, int cpgid, int cgcsgid, int[] unicodeMapping) {
this(id, description, cpgid, cgcsgid, null, unicodeMapping);
}
public AbstractCodePage(String id, String description, int cpgid, int cgcsgid, String javaCharsetName, int[] unicodeMapping) {
this.id = id;
this.description = description;
this.cpgid = cpgid;
this.cgcsgid = cgcsgid;
int[] map = loadMapping(javaCharsetName, unicodeMapping);
System.arraycopy(map, 0, this.toUnicode, 0, 256);
initReverseMapping();
}
public static int[] loadMapping(String charsetName, int[] fallback) {
if (charsetName != null) {
String[] candidates = {
charsetName,
"IBM" + charsetName,
"Cp" + charsetName,
"IBM-" + charsetName,
"x-IBM" + charsetName
};
for (String name : candidates) {
try {
if (Charset.isSupported(name)) {
Charset cs = Charset.forName(name);
int[] map = new int[256];
for (int i = 0; i < 256; i++) {
byte[] b = new byte[]{(byte) i};
String s = new String(b, cs);
map[i] = (!s.isEmpty() && s.charAt(0) != '\uFFFD') ? s.charAt(0) : (fallback != null ? fallback[i] : 0);
}
return map;
}
} catch (Exception ignored) {}
}
}
return fallback != null ? fallback : new int[256];
}
protected void initReverseMapping() {
Arrays.fill(toEbcdic, -1);
for (int i = 0; i < 256; i++) {
int uc = toUnicode[i];
if (uc >= 0 && uc < 256) {
if (toEbcdic[uc] == -1) {
toEbcdic[uc] = i;
}
} else if (uc >= 256) {
extendedToEbcdic.putIfAbsent((char) uc, i);
}
}
}
@Override
public String getCodePageId() {
return id;
}
@Override
public String getDescription() {
return description;
}
@Override
public int getCpgid() {
return cpgid;
}
@Override
public int getCgcsgid() {
return cgcsgid;
}
@Override
public boolean isDBCS() {
return false;
}
@Override
public char ebcdicToUnicode(int ebc) {
return (char) toUnicode[ebc & 0xFF];
}
@Override
public int unicodeToEbcdic(char unicode) {
int u = unicode;
if (u >= 0 && u < 256) {
return toEbcdic[u];
}
Integer val = extendedToEbcdic.get(unicode);
return val != null ? val : -1;
}
@Override
public byte unicodeToEbcdicSafe(char unicode) {
int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
}
@Override
public char dbcsToUnicode(int b1, int b2) {
return '\uFFFD';
}
@Override
public int unicodeToDbcs(char unicode) {
return -1;
}
@Override
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
if (ebcdic == null || length <= 0) return "";
char[] chars = new char[length];
for (int i = 0; i < length; i++) {
chars[i] = ebcdicToUnicode(ebcdic[offset + i]);
}
return new String(chars);
}
@Override
public byte[] stringToEbcdic(String s) {
if (s == null || s.isEmpty()) return new byte[0];
byte[] bytes = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
bytes[i] = unicodeToEbcdicSafe(s.charAt(i));
}
return bytes;
}
@Override
public String toString() {
return id + " (" + description + ")";
}
}
@@ -0,0 +1,184 @@
package haus.nightmare.lib3270j.charset;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.HashMap;
import java.util.Map;
/**
* Base implementation for Mixed Double-Byte Character Set (DBCS) EBCDIC Code Pages
* (e.g. Japanese Katakana 930, Japanese Latin 939, Chinese 935/937/1388, Korean 933).
*/
public abstract class AbstractDBCSCodePage extends AbstractCodePage {
public static final int SO = 0x0E; // Shift Out (enter DBCS mode)
public static final int SI = 0x0F; // Shift In (enter SBCS mode)
protected final Map<Integer, Character> dbcsToUnicodeMap = new HashMap<>();
protected final Map<Character, Integer> unicodeToDbcsMap = new HashMap<>();
protected Charset nioCharset;
public AbstractDBCSCodePage(String id, String description, int cpgid, int cgcsgid,
int[] sbcsMapping, String nioCharsetName) {
super(id, description, cpgid, cgcsgid, sbcsMapping);
if (nioCharsetName != null) {
try {
if (Charset.isSupported(nioCharsetName)) {
this.nioCharset = Charset.forName(nioCharsetName);
}
} catch (Exception ignored) {}
}
// Initialize ideographic space (0x4040 <-> \u3000)
registerDbcsPair(0x4040, '\u3000');
}
public void registerDbcsPair(int ebcdicDBCS, char unicodeChar) {
dbcsToUnicodeMap.put(ebcdicDBCS, unicodeChar);
unicodeToDbcsMap.put(unicodeChar, ebcdicDBCS);
}
@Override
public boolean isDBCS() {
return true;
}
@Override
public char dbcsToUnicode(int b1, int b2) {
int key = ((b1 & 0xFF) << 8) | (b2 & 0xFF);
Character c = dbcsToUnicodeMap.get(key);
if (c != null) {
return c;
}
if (nioCharset != null) {
try {
byte[] raw = new byte[] { (byte) SO, (byte) b1, (byte) b2, (byte) SI };
CharBuffer cb = nioCharset.decode(ByteBuffer.wrap(raw));
if (cb.hasRemaining()) {
char decoded = cb.get();
if (decoded != '\uFFFD' && decoded != 0) {
registerDbcsPair(key, decoded);
return decoded;
}
}
} catch (Exception ignored) {}
}
return '?';
}
@Override
public int unicodeToDbcs(char unicode) {
Integer val = unicodeToDbcsMap.get(unicode);
if (val != null) {
return val;
}
if (nioCharset != null) {
try {
CharsetEncoder encoder = nioCharset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
ByteBuffer bb = encoder.encode(CharBuffer.wrap(new char[]{unicode}));
byte[] bytes = new byte[bb.remaining()];
bb.get(bytes);
// Check if encoded as SO + b1 + b2 + SI or b1 + b2
if (bytes.length >= 4 && (bytes[0] & 0xFF) == SO) {
int key = ((bytes[1] & 0xFF) << 8) | (bytes[2] & 0xFF);
registerDbcsPair(key, unicode);
return key;
} else if (bytes.length == 2) {
int key = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
registerDbcsPair(key, unicode);
return key;
}
} catch (Exception ignored) {}
}
return -1;
}
@Override
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
if (ebcdic == null || length <= 0) return "";
StringBuilder sb = new StringBuilder(length);
boolean inDBCS = false;
int end = Math.min(ebcdic.length, offset + length);
int i = offset;
while (i < end) {
int b = ebcdic[i] & 0xFF;
if (b == SO) {
inDBCS = true;
i++;
} else if (b == SI) {
inDBCS = false;
i++;
} else if (inDBCS) {
if (i + 1 < end) {
int b2 = ebcdic[i + 1] & 0xFF;
if (b2 == SI) {
// Orphaned single byte before SI
inDBCS = false;
i += 2;
} else {
sb.append(dbcsToUnicode(b, b2));
i += 2;
}
} else {
// Trailing byte
i++;
}
} else {
sb.append(ebcdicToUnicode(b));
i++;
}
}
return sb.toString();
}
@Override
public byte[] stringToEbcdic(String s) {
if (s == null || s.isEmpty()) return new byte[0];
// Manual state machine encoding
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
boolean inDBCS = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
int dbcsCode = unicodeToDbcs(c);
if (dbcsCode >= 0) {
// Character is DBCS
if (!inDBCS) {
out.write(SO);
inDBCS = true;
}
out.write((dbcsCode >> 8) & 0xFF);
out.write(dbcsCode & 0xFF);
} else {
// Character is SBCS
if (inDBCS) {
out.write(SI);
inDBCS = false;
}
out.write(unicodeToEbcdicSafe(c));
}
}
if (inDBCS) {
out.write(SI);
}
return out.toByteArray();
}
}
@@ -0,0 +1,72 @@
package haus.nightmare.lib3270j.charset;
/**
* Interface defining character translation and metadata for EBCDIC code pages
* conforming to IBM Host On-Demand (HoD v14) converters specification.
*/
public interface CodePage {
/**
* Get the primary identifier for this code page (e.g. "037", "1047", "500", "273").
*/
String getCodePageId();
/**
* Get a human-readable description (e.g. "US / Canada - EBCDIC").
*/
String getDescription();
/**
* Get the Code Page Global ID (CPGID / CCSID) for 3270 query replies.
*/
int getCpgid();
/**
* Get the Graphic Character Set Global ID (GCSGID) for 3270 query replies.
*/
int getCgcsgid();
/**
* Translate an EBCDIC byte (0x00-0xFF) to a Unicode character.
*/
char ebcdicToUnicode(int ebc);
/**
* Translate a Unicode character to an EBCDIC byte value.
* Returns -1 if unmappable.
*/
int unicodeToEbcdic(char unicode);
/**
* Translate a Unicode character to an EBCDIC byte value, returning a safe
* fallback (default EBCDIC space 0x40) if unmappable.
*/
byte unicodeToEbcdicSafe(char unicode);
/**
* Translate an EBCDIC byte array slice to a Unicode String.
*/
String ebcdicToString(byte[] ebcdic, int offset, int length);
/**
* Translate a Unicode String to an EBCDIC byte array.
*/
byte[] stringToEbcdic(String s);
/**
* Check if this code page is a double-byte (DBCS) / mixed SBCS+DBCS code page.
*/
boolean isDBCS();
/**
* Translate a double-byte EBCDIC pair (b1, b2) to a Unicode character.
* Returns '\uFFFD' or '?' if unmappable.
*/
char dbcsToUnicode(int b1, int b2);
/**
* Translate a Unicode character to a double-byte EBCDIC code ((b1 << 8) | b2).
* Returns -1 if unmappable.
*/
int unicodeToDbcs(char unicode);
}
@@ -0,0 +1,270 @@
package haus.nightmare.lib3270j.charset;
import java.nio.charset.Charset;
import java.util.*;
import java.util.logging.Logger;
/**
* Registry and factory for 3270 EBCDIC Code Pages.
* Manages built-in SBCS/DBCS codepages, alias normalization, and dynamic JVM Charset resolution.
*/
public class CodePageRegistry {
private static final Logger log = Logger.getLogger(CodePageRegistry.class.getName());
private static final Map<String, CodePage> CODE_PAGES = new LinkedHashMap<>();
private static final Map<String, String> ALIASES = new HashMap<>();
static {
// Register core SBCS codepages
register(new Cp037());
register(new Cp1047());
register(new Cp500());
register(new Cp273());
register(new Cp277());
register(new Cp278());
register(new Cp280());
register(new Cp284());
register(new Cp285());
register(new Cp297());
register(new Cp870());
register(new Cp871());
register(new Cp875());
register(new Cp1026());
// Register Euro variants (1140-1149)
register(new CpEuroVariants.Cp1140());
register(new CpEuroVariants.Cp1141());
register(new CpEuroVariants.Cp1142());
register(new CpEuroVariants.Cp1143());
register(new CpEuroVariants.Cp1144());
register(new CpEuroVariants.Cp1145());
register(new CpEuroVariants.Cp1146());
register(new CpEuroVariants.Cp1147());
register(new CpEuroVariants.Cp1148());
register(new CpEuroVariants.Cp1149());
// Register DBCS mixed codepages
register(new Cp930());
register(new Cp939());
register(new Cp935());
register(new Cp935.Cp1388());
register(new Cp937());
register(new Cp937.Cp1371());
register(new Cp933());
// Setup common aliases
addAlias("us", "037");
addAlias("usa", "037");
addAlias("ebcdic-cp-us", "037");
addAlias("ebcdic-cp-ca", "037");
addAlias("ebcdic-cp-nl", "037");
addAlias("posix", "1047");
addAlias("unix", "1047");
addAlias("open-systems", "1047");
addAlias("zos-unix", "1047");
addAlias("intl", "500");
addAlias("international", "500");
addAlias("ebcdic-cp-ch", "500");
addAlias("de", "273");
addAlias("germany", "273");
addAlias("austria", "273");
addAlias("ebcdic-cp-de", "273");
addAlias("dk", "277");
addAlias("no", "277");
addAlias("denmark", "277");
addAlias("norway", "277");
addAlias("ebcdic-cp-dk", "277");
addAlias("ebcdic-cp-no", "277");
addAlias("se", "278");
addAlias("fi", "278");
addAlias("sweden", "278");
addAlias("finland", "278");
addAlias("ebcdic-cp-se", "278");
addAlias("ebcdic-cp-fi", "278");
addAlias("it", "280");
addAlias("italy", "280");
addAlias("ebcdic-cp-it", "280");
addAlias("es", "284");
addAlias("spain", "284");
addAlias("latin-america", "284");
addAlias("ebcdic-cp-es", "284");
addAlias("uk", "285");
addAlias("gb", "285");
addAlias("great-britain", "285");
addAlias("ebcdic-cp-gb", "285");
addAlias("fr", "297");
addAlias("france", "297");
addAlias("ebcdic-cp-fr", "297");
addAlias("latin2", "870");
addAlias("pl", "870");
addAlias("cz", "870");
addAlias("hu", "870");
addAlias("ebcdic-cp-roece", "870");
addAlias("is", "871");
addAlias("iceland", "871");
addAlias("ebcdic-cp-is", "871");
addAlias("gr", "875");
addAlias("greece", "875");
addAlias("greek", "875");
addAlias("ebcdic-cp-gr", "875");
addAlias("tr", "1026");
addAlias("turkey", "1026");
addAlias("turkish", "1026");
addAlias("ebcdic-cp-tr", "1026");
addAlias("ja", "930");
addAlias("japanese", "930");
addAlias("katakana", "930");
addAlias("ja-latin", "939");
addAlias("zh", "935");
addAlias("chinese-simplified", "935");
addAlias("zh-tw", "937");
addAlias("chinese-traditional", "937");
addAlias("ko", "933");
addAlias("korean", "933");
}
public static void register(CodePage cp) {
if (cp != null) {
CODE_PAGES.put(cp.getCodePageId(), cp);
}
}
public static void addAlias(String alias, String targetId) {
if (alias != null && targetId != null) {
ALIASES.put(normalizeKey(alias), targetId);
}
}
public static String normalizeKey(String name) {
if (name == null) return "";
String s = name.trim().toLowerCase();
s = s.replace("_", "").replace("-", "");
if (s.startsWith("ebcdiccp")) {
s = s.substring(8);
} else if (s.startsWith("ebcdic")) {
s = s.substring(6);
} else if (s.startsWith("ibm")) {
s = s.substring(3);
} else if (s.startsWith("cp")) {
s = s.substring(2);
} else if (s.startsWith("ccsid")) {
s = s.substring(5);
}
return s;
}
/**
* Look up a code page by ID or alias.
* If not found in built-ins, attempts to load via java.nio.charset.Charset.
* Falls back to CP037 if completely unresolvable.
*/
public static CodePage getCodePage(String name) {
if (name == null || name.trim().isEmpty()) {
return CODE_PAGES.get("037");
}
String raw = name.trim();
// Direct match
CodePage cp = CODE_PAGES.get(raw);
if (cp != null) return cp;
// Normalized key lookup
String norm = normalizeKey(raw);
cp = CODE_PAGES.get(norm);
if (cp != null) return cp;
// Alias lookup
String targetId = ALIASES.get(norm);
if (targetId != null) {
cp = CODE_PAGES.get(targetId);
if (cp != null) return cp;
}
// Try standard NIO Charset dynamic adapter
try {
if (Charset.isSupported(raw)) {
return new NioCodePageAdapter(raw);
}
String ibmName = "IBM" + norm;
if (Charset.isSupported(ibmName)) {
return new NioCodePageAdapter(ibmName);
}
String cpName = "Cp" + norm;
if (Charset.isSupported(cpName)) {
return new NioCodePageAdapter(cpName);
}
} catch (Exception e) {
log.fine("Dynamic charset loading failed for " + name + ": " + e.getMessage());
}
log.warning("CodePage not recognized: '" + name + "'; falling back to CP037");
return CODE_PAGES.get("037");
}
/**
* Get an unmodifiable list of all registered built-in CodePages.
*/
public static List<CodePage> getAvailableCodePages() {
return Collections.unmodifiableList(new ArrayList<>(CODE_PAGES.values()));
}
/**
* Get an array of all registered built-in CodePage IDs.
*/
public static String[] getAvailableCodePageIds() {
return CODE_PAGES.keySet().toArray(new String[0]);
}
/**
* Dynamic fallback adapter wrapping any JVM java.nio.charset.Charset.
*/
public static class NioCodePageAdapter extends AbstractCodePage {
private final Charset charset;
public NioCodePageAdapter(String charsetName) {
this(Charset.forName(charsetName));
}
public NioCodePageAdapter(Charset charset) {
super(charset.name(), "JVM Charset: " + charset.displayName(), parseCpgid(charset.name()), 697, buildMappingFromCharset(charset));
this.charset = charset;
}
private static int parseCpgid(String name) {
try {
String num = name.replaceAll("\\D+", "");
if (!num.isEmpty()) {
return Integer.parseInt(num);
}
} catch (Exception ignored) {}
return 37;
}
private static int[] buildMappingFromCharset(Charset cs) {
int[] map = new int[256];
for (int i = 0; i < 256; i++) {
byte[] b = new byte[]{(byte) i};
String s = new String(b, cs);
map[i] = !s.isEmpty() ? s.charAt(0) : (char) i;
}
return map;
}
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 037 (US / Canada / Netherlands / Portugal / Brazil).
* CCSID / CPGID: 37, GCSGID: 697.
*/
public class Cp037 extends AbstractCodePage {
public static final String ID = "037";
public static final String DESCRIPTION = "US / Canada / Brazil - EBCDIC";
public static final int CPGID = 37;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F (space, accent chars, punctuation)
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F (lowercase a-i)
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F (lowercase j-r)
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF (lowercase s-z)
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF (uppercase A-I)
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF (uppercase J-R)
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF (uppercase S-Z)
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF (digits 0-9)
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp037() {
super(ID, DESCRIPTION, CPGID, GCSGID, "037", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1026 (Turkey - Turkish Latin-5 EBCDIC).
* CCSID / CPGID: 1026, GCSGID: 1152.
*/
public class Cp1026 extends AbstractCodePage {
public static final String ID = "1026";
public static final String DESCRIPTION = "Turkey - Turkish Latin-5 EBCDIC";
public static final int CPGID = 1026;
public static final int GCSGID = 1152;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x015E, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00D6, 0x00C7, 0x00DC, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x0130, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00F6, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x011E, 0x00F4, 0x0131, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00E7, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x011F, 0x00FB, 0x015F, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00FC, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00A6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp1026() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1026", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1047 (IBM Open Systems / z/OS Unix System Services Latin-1).
* CCSID / CPGID: 1047, GCSGID: 103.
*/
public class Cp1047 extends AbstractCodePage {
public static final String ID = "1047";
public static final String DESCRIPTION = "IBM Open Systems / z/OS Unix Latin-1";
public static final int CPGID = 1047;
public static final int GCSGID = 103;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x000A, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x005B, 0x00DE, 0x00AE,
// B0-BF
0x00AC, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00DD, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp1047() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1047", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 273 (Germany / Austria).
* CCSID / CPGID: 273, GCSGID: 697.
*/
public class Cp273 extends AbstractCodePage {
public static final String ID = "273";
public static final String DESCRIPTION = "Germany / Austria - EBCDIC";
public static final int CPGID = 273;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E4, 0x00F6, 0x00FC, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00E4, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00FC, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00DC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp273() {
super(ID, DESCRIPTION, CPGID, GCSGID, "273", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 277 (Denmark / Norway).
* CCSID / CPGID: 277, GCSGID: 697.
*/
public class Cp277 extends AbstractCodePage {
public static final String ID = "277";
public static final String DESCRIPTION = "Denmark / Norway - EBCDIC";
public static final int CPGID = 277;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E6, 0x00F8, 0x00E5, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00C6, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00D8, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00C5, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp277() {
super(ID, DESCRIPTION, CPGID, GCSGID, "277", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 278 (Sweden / Finland).
* CCSID / CPGID: 278, GCSGID: 697.
*/
public class Cp278 extends AbstractCodePage {
public static final String ID = "278";
public static final String DESCRIPTION = "Sweden / Finland - EBCDIC";
public static final int CPGID = 278;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E4, 0x00F6, 0x00E5, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00C4, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00D6, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00C5, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp278() {
super(ID, DESCRIPTION, CPGID, GCSGID, "278", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 280 (Italy).
* CCSID / CPGID: 280, GCSGID: 697.
*/
public class Cp280 extends AbstractCodePage {
public static final String ID = "280";
public static final String DESCRIPTION = "Italy - EBCDIC";
public static final int CPGID = 280;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00B0, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00A3, 0x00A7, 0x00E9, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00F2, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00F9, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00E0, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp280() {
super(ID, DESCRIPTION, CPGID, GCSGID, "280", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 284 (Spain / Latin America).
* CCSID / CPGID: 284, GCSGID: 697.
*/
public class Cp284 extends AbstractCodePage {
public static final String ID = "284";
public static final String DESCRIPTION = "Spain / Latin America - EBCDIC";
public static final int CPGID = 284;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00F1, 0x00D1, 0x00E7, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp284() {
super(ID, DESCRIPTION, CPGID, GCSGID, "284", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 285 (United Kingdom).
* CCSID / CPGID: 285, GCSGID: 697.
*/
public class Cp285 extends AbstractCodePage {
public static final String ID = "285";
public static final String DESCRIPTION = "United Kingdom - EBCDIC";
public static final int CPGID = 285;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00A3, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp285() {
super(ID, DESCRIPTION, CPGID, GCSGID, "285", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 297 (France).
* CCSID / CPGID: 297, GCSGID: 697.
*/
public class Cp297 extends AbstractCodePage {
public static final String ID = "297";
public static final String DESCRIPTION = "France - EBCDIC";
public static final int CPGID = 297;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00B0, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00E0, 0x00E9, 0x00E8, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00E9, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00E8, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp297() {
super(ID, DESCRIPTION, CPGID, GCSGID, "297", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 500 (International / Western Europe Latin-1).
* CCSID / CPGID: 500, GCSGID: 697.
*/
public class Cp500 extends AbstractCodePage {
public static final String ID = "500";
public static final String DESCRIPTION = "International / Western Europe Latin-1";
public static final int CPGID = 500;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp500() {
super(ID, DESCRIPTION, CPGID, GCSGID, "500", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 870 (Latin-2 / Eastern Europe: Polish, Czech, Slovak, Hungarian, etc.).
* CCSID / CPGID: 870, GCSGID: 959.
*/
public class Cp870 extends AbstractCodePage {
public static final String ID = "870";
public static final String DESCRIPTION = "Eastern Europe / Latin-2 - EBCDIC";
public static final int CPGID = 870;
public static final int GCSGID = 959;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x0103, 0x00E4, 0x016F, 0x0165, 0x0105, 0x0155,
0x0107, 0x00E9, 0x015B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F,
0x010D, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x016E, 0x0164, 0x0104, 0x0154,
0x0106, 0x00C9, 0x015A, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E,
0x010C, 0x0060, 0x003A, 0x0161, 0x017C, 0x00FD, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x0160, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x017B, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp870() {
super(ID, DESCRIPTION, CPGID, GCSGID, "870", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 871 (Iceland).
* CCSID / CPGID: 871, GCSGID: 697.
*/
public class Cp871 extends AbstractCodePage {
public static final String ID = "871";
public static final String DESCRIPTION = "Iceland - EBCDIC";
public static final int CPGID = 871;
public static final int GCSGID = 697;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x00FE, 0x00F0, 0x00FD, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x00DE, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x00D0, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x00DD, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp871() {
super(ID, DESCRIPTION, CPGID, GCSGID, "871", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 875 (Greece - Greek EBCDIC).
* CCSID / CPGID: 875, GCSGID: 925.
*/
public class Cp875 extends AbstractCodePage {
public static final String ID = "875";
public static final String DESCRIPTION = "Greece - Greek EBCDIC";
public static final int CPGID = 875;
public static final int GCSGID = 925;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x03AC, 0x03AD, 0x03AE, 0x03AF, 0x03CC, 0x03CD,
0x03CE, 0x00DF, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0390, 0x03B0, 0x03CA, 0x03CB, 0x0390, 0x0385, 0x0386,
0x0388, 0x0389, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x038A, 0x038C, 0x038E, 0x038F, 0x03AA, 0x03AB,
0x00A9, 0x00AE, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7,
0x03B8, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x03B9, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF,
// 90-9F
0x03C0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x03C1, 0x03C3, 0x03C2, 0x03C4, 0x03C5, 0x03C6,
// A0-AF
0x03C7, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x03C8, 0x03C9, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C,
// E0-EF
0x005C, 0x039D, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x009F,
};
public Cp875() {
super(ID, DESCRIPTION, CPGID, GCSGID, "875", MAPPING);
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 930 (Japanese Katakana Mixed DBCS: host SBCS CP290 / DBCS CP300).
* CCSID / CPGID: 930, GCSGID: 1172.
*/
public class Cp930 extends AbstractDBCSCodePage {
public static final String ID = "930";
public static final String DESCRIPTION = "Japanese Katakana Mixed DBCS";
public static final int CPGID = 930;
public static final int GCSGID = 1172;
public Cp930() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM930");
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 933 (Korean Mixed DBCS).
* CCSID / CPGID: 933, GCSGID: 1173.
*/
public class Cp933 extends AbstractDBCSCodePage {
public static final String ID = "933";
public static final String DESCRIPTION = "Korean Mixed DBCS";
public static final int CPGID = 933;
public static final int GCSGID = 1173;
public Cp933() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM933");
}
}
@@ -0,0 +1,28 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 935 / 1388 (Simplified Chinese Mixed DBCS).
* CCSID / CPGID: 935, GCSGID: 1175.
*/
public class Cp935 extends AbstractDBCSCodePage {
public static final String ID = "935";
public static final String DESCRIPTION = "Simplified Chinese Mixed DBCS";
public static final int CPGID = 935;
public static final int GCSGID = 1175;
public Cp935() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM935");
}
public static class Cp1388 extends AbstractDBCSCodePage {
public static final String ID = "1388";
public static final String DESCRIPTION = "Simplified Chinese Extended Mixed DBCS";
public static final int CPGID = 1388;
public static final int GCSGID = 1175;
public Cp1388() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1388");
}
}
}
@@ -0,0 +1,28 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 937 / 1371 (Traditional Chinese Mixed DBCS).
* CCSID / CPGID: 937, GCSGID: 1174.
*/
public class Cp937 extends AbstractDBCSCodePage {
public static final String ID = "937";
public static final String DESCRIPTION = "Traditional Chinese Mixed DBCS";
public static final int CPGID = 937;
public static final int GCSGID = 1174;
public Cp937() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM937");
}
public static class Cp1371 extends AbstractDBCSCodePage {
public static final String ID = "1371";
public static final String DESCRIPTION = "Traditional Chinese Extended Mixed DBCS";
public static final int CPGID = 1371;
public static final int GCSGID = 1174;
public Cp1371() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1371");
}
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 939 (Japanese Latin Mixed DBCS: host SBCS CP1027 / DBCS CP300).
* CCSID / CPGID: 939, GCSGID: 1172.
*/
public class Cp939 extends AbstractDBCSCodePage {
public static final String ID = "939";
public static final String DESCRIPTION = "Japanese Latin Mixed DBCS";
public static final int CPGID = 939;
public static final int GCSGID = 1172;
public Cp939() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM939");
}
}
@@ -0,0 +1,125 @@
package haus.nightmare.lib3270j.charset;
import java.util.Arrays;
/**
* Euro currency symbol (€) enabled EBCDIC code pages (CP1140 through CP1149).
*/
public class CpEuroVariants {
public static class Cp1140 extends AbstractCodePage {
public static final String ID = "1140";
public static final String DESCRIPTION = "US / Canada Euro - EBCDIC";
public static final int CPGID = 1140;
public static final int GCSGID = 695;
public Cp1140() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1140", modifyEuro(Cp037.MAPPING, 0x9F));
}
}
public static class Cp1141 extends AbstractCodePage {
public static final String ID = "1141";
public static final String DESCRIPTION = "Germany / Austria Euro - EBCDIC";
public static final int CPGID = 1141;
public static final int GCSGID = 695;
public Cp1141() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1141", modifyEuro(Cp273.MAPPING, 0x9F));
}
}
public static class Cp1142 extends AbstractCodePage {
public static final String ID = "1142";
public static final String DESCRIPTION = "Denmark / Norway Euro - EBCDIC";
public static final int CPGID = 1142;
public static final int GCSGID = 695;
public Cp1142() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1142", modifyEuro(Cp277.MAPPING, 0x5A));
}
}
public static class Cp1143 extends AbstractCodePage {
public static final String ID = "1143";
public static final String DESCRIPTION = "Sweden / Finland Euro - EBCDIC";
public static final int CPGID = 1143;
public static final int GCSGID = 695;
public Cp1143() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1143", modifyEuro(Cp278.MAPPING, 0x5A));
}
}
public static class Cp1144 extends AbstractCodePage {
public static final String ID = "1144";
public static final String DESCRIPTION = "Italy Euro - EBCDIC";
public static final int CPGID = 1144;
public static final int GCSGID = 695;
public Cp1144() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1144", modifyEuro(Cp280.MAPPING, 0x9F));
}
}
public static class Cp1145 extends AbstractCodePage {
public static final String ID = "1145";
public static final String DESCRIPTION = "Spain / Latin America Euro - EBCDIC";
public static final int CPGID = 1145;
public static final int GCSGID = 695;
public Cp1145() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1145", modifyEuro(Cp284.MAPPING, 0x9F));
}
}
public static class Cp1146 extends AbstractCodePage {
public static final String ID = "1146";
public static final String DESCRIPTION = "United Kingdom Euro - EBCDIC";
public static final int CPGID = 1146;
public static final int GCSGID = 695;
public Cp1146() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1146", modifyEuro(Cp285.MAPPING, 0x9F));
}
}
public static class Cp1147 extends AbstractCodePage {
public static final String ID = "1147";
public static final String DESCRIPTION = "France Euro - EBCDIC";
public static final int CPGID = 1147;
public static final int GCSGID = 695;
public Cp1147() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1147", modifyEuro(Cp297.MAPPING, 0x9F));
}
}
public static class Cp1148 extends AbstractCodePage {
public static final String ID = "1148";
public static final String DESCRIPTION = "International Latin-1 Euro - EBCDIC";
public static final int CPGID = 1148;
public static final int GCSGID = 695;
public Cp1148() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1148", modifyEuro(Cp500.MAPPING, 0x9F));
}
}
public static class Cp1149 extends AbstractCodePage {
public static final String ID = "1149";
public static final String DESCRIPTION = "Iceland Euro - EBCDIC";
public static final int CPGID = 1149;
public static final int GCSGID = 695;
public Cp1149() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1149", modifyEuro(Cp871.MAPPING, 0x9F));
}
}
private static int[] modifyEuro(int[] baseMapping, int euroPosition) {
int[] copy = Arrays.copyOf(baseMapping, 256);
copy[euroPosition & 0xFF] = '\u20AC'; // '€'
return copy;
}
}
@@ -1,142 +1,200 @@
package haus.nightmare.lib3270j.charset;
import java.util.List;
/**
* EBCDIC ↔ Unicode translator.
* EBCDIC ↔ Unicode character translator conforming to IBM Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
* Default: Code Page 037 (US/Canada EBCDIC).
*/
public class EbcdicTranslator {
/**
* EBCDIC Code Page 037 to Unicode mapping.
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint.
* EBCDIC Code Page 037 to Unicode mapping table preserved for legacy static access.
*/
private static final int[] CP037_TO_UNICODE = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F (space, accent chars, punctuation)
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F (lowercase a-i)
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F (lowercase j-r)
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF (lowercase s-z)
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF (uppercase A-I)
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF (uppercase J-R)
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF (uppercase S-Z)
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF (digits 0-9)
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public static final int[] CP037_TO_UNICODE = Cp037.MAPPING;
private CodePage activeCodePage;
public EbcdicTranslator() {
this("037");
}
public EbcdicTranslator(String codePageId) {
setCodePage(codePageId);
}
public EbcdicTranslator(CodePage codePage) {
setCodePage(codePage);
}
/**
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1).
* Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable).
* Set active code page by identifier or alias (e.g. "037", "1047", "500", "273", "1140", "930").
*/
private static final int[] UNICODE_TO_CP037 = new int[256];
public synchronized void setCodePage(String codePageId) {
this.activeCodePage = CodePageRegistry.getCodePage(codePageId);
}
static {
// Build reverse mapping
java.util.Arrays.fill(UNICODE_TO_CP037, -1);
for (int i = 0; i < 256; i++) {
int unicode = CP037_TO_UNICODE[i];
if (unicode < 256) {
UNICODE_TO_CP037[unicode] = i;
}
/**
* Set active code page directly.
*/
public synchronized void setCodePage(CodePage codePage) {
if (codePage != null) {
this.activeCodePage = codePage;
} else {
this.activeCodePage = CodePageRegistry.getCodePage("037");
}
}
/**
* Translate EBCDIC byte to Unicode character.
* Get the active CodePage descriptor and translator.
*/
public char ebcdicToUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
public synchronized CodePage getCodePage() {
return activeCodePage;
}
/**
* Static helper to translate EBCDIC byte to Unicode character.
* Get the active code page identifier string.
*/
public synchronized String getCodePageId() {
return activeCodePage.getCodePageId();
}
/**
* Get active Code Page Global ID (CPGID / CCSID) for Query Replies.
*/
public synchronized int getCpgid() {
return activeCodePage.getCpgid();
}
/**
* Get active Graphic Character Set Global ID (GCSGID) for Query Replies.
*/
public synchronized int getCgcsgid() {
return activeCodePage.getCgcsgid();
}
/**
* Check if active code page is Double-Byte / mixed DBCS.
*/
public synchronized boolean isDBCSCodePage() {
return activeCodePage.isDBCS();
}
/**
* Translate double-byte EBCDIC pair (b1, b2) to Unicode.
*/
public synchronized char dbcsToUnicode(int b1, int b2) {
return activeCodePage.dbcsToUnicode(b1, b2);
}
/**
* Translate Unicode character to double-byte EBCDIC ((b1 << 8) | b2).
*/
public synchronized int unicodeToDbcs(char unicode) {
return activeCodePage.unicodeToDbcs(unicode);
}
/**
* Translate EBCDIC byte to Unicode character using active code page.
*/
public char ebcdicToUnicode(int ebc) {
return activeCodePage.ebcdicToUnicode(ebc);
}
/**
* Static helper to translate EBCDIC byte to Unicode character (using standard CP037).
*/
public static char toUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
}
/**
* Static helper to translate EBCDIC byte to ASCII character.
* Static helper to translate EBCDIC byte to ASCII character (using standard CP037).
*/
public static char ebcdicToAscii(int ebc) {
return toUnicode(ebc);
}
/**
* Translate Unicode character to EBCDIC byte.
* Translate Unicode character to EBCDIC byte using active code page.
* Returns -1 if the character cannot be mapped.
*/
public int unicodeToEbcdic(char unicode) {
if (unicode < 256) {
return UNICODE_TO_CP037[unicode];
}
return -1;
return activeCodePage.unicodeToEbcdic(unicode);
}
/**
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/
public byte unicodeToEbcdicSafe(char unicode) {
int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
return activeCodePage.unicodeToEbcdicSafe(unicode);
}
/**
* Translate a byte array from EBCDIC to a Unicode string.
* Translate a byte array from EBCDIC to a Unicode string using active code page.
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF));
}
return sb.toString();
return activeCodePage.ebcdicToString(ebcdic, offset, length);
}
/**
* Translate a Unicode string to EBCDIC byte array.
* Translate a Unicode string to EBCDIC byte array using active code page.
*/
public byte[] stringToEbcdic(String s) {
byte[] result = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
result[i] = unicodeToEbcdicSafe(s.charAt(i));
return activeCodePage.stringToEbcdic(s);
}
/**
* Get an array of all registered code page ID strings.
*/
public static String[] getAvailableCodePages() {
return CodePageRegistry.getAvailableCodePageIds();
}
/**
* Get a list of all registered CodePage instances.
*/
public static List<CodePage> getAllCodePages() {
return CodePageRegistry.getAvailableCodePages();
}
/**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
*/
public char getAplGraphic(int ec) {
switch (ec & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return ebcdicToUnicode(ec & 0xFF);
}
return result;
}
}
@@ -4,6 +4,7 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
@@ -86,6 +87,16 @@ public class DataStreamProcessor {
this.ftDft = ftDft;
}
private haus.nightmare.lib3270j.printer.PrintSCS3270 embeddedScsProcessor;
public void setEmbeddedScsProcessor(haus.nightmare.lib3270j.printer.PrintSCS3270 scsProcessor) {
this.embeddedScsProcessor = scsProcessor;
}
public haus.nightmare.lib3270j.printer.PrintSCS3270 getEmbeddedScsProcessor() {
return embeddedScsProcessor;
}
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
this.inputProcessor = inputProcessor;
}
@@ -638,10 +649,21 @@ public class DataStreamProcessor {
case XA_CHARSET:
ea.cs = (byte) value;
break;
case XA_VALIDATION:
case XA_OUTLINING:
case 0x84: // HoD / 3270 Outlining alternative ID
ea.ol = (byte) value;
break;
case XA_VALIDATION:
ea.vl = (byte) value;
break;
case XA_TRANSPARENCY:
ea.tr = (byte) value;
break;
case XA_INPUT_CONTROL:
// Acknowledged but not visually rendered yet
ea.ic = (byte) value;
break;
case 0x91: // DBCS Asian attributes
ea.db = (byte) value;
break;
}
}
@@ -677,14 +699,79 @@ public class DataStreamProcessor {
outputWrite(caddr[0] & 0xFF);
outputWrite(caddr[1] & 0xFF);
// Buffer contents
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
outputWrite(ORDER_SF);
outputWrite(ea.fa & 0xFF);
} else {
outputWrite(ea.ec & 0xFF);
byte mode = screen.getReplyMode();
// Buffer contents depending on Reply Mode
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
// Extended Field or Character Mode
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
// Count number of attribute pairs
int count = 1; // 3270 FA is always present
if (ea.fg != 0) count++;
if (ea.bg != 0) count++;
if (ea.gr != 0) count++;
if (ea.cs != 0) count++;
if (ea.ol != 0) count++;
outputWrite(ORDER_SFE);
outputWrite(count);
outputWrite(XA_3270);
outputWrite(ea.fa & 0xFF);
if (ea.fg != 0) { outputWrite(XA_FOREGROUND); outputWrite(ea.fg & 0xFF); }
if (ea.bg != 0) { outputWrite(XA_BACKGROUND); outputWrite(ea.bg & 0xFF); }
if (ea.gr != 0) {
outputWrite(XA_HIGHLIGHTING);
int xah = XAH_NORMAL;
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
outputWrite(xah);
}
if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); }
if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); }
} else {
if (mode == SF_SRM_CHAR) {
// In character mode, output SA if character attributes differ
if (ea.fg != curFg) {
outputWrite(ORDER_SA); outputWrite(XA_FOREGROUND); outputWrite(ea.fg & 0xFF);
curFg = ea.fg;
}
if (ea.bg != curBg) {
outputWrite(ORDER_SA); outputWrite(XA_BACKGROUND); outputWrite(ea.bg & 0xFF);
curBg = ea.bg;
}
if (ea.gr != curGr) {
outputWrite(ORDER_SA); outputWrite(XA_HIGHLIGHTING);
int xah = XAH_NORMAL;
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
outputWrite(xah);
curGr = ea.gr;
}
if (ea.cs != curCs) {
outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF);
curCs = ea.cs;
}
}
outputWrite(ea.ec & 0xFF);
}
}
} else {
// Standard Field Mode (SF_SRM_FIELD)
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
outputWrite(ORDER_SF);
outputWrite(ea.fa & 0xFF);
} else {
outputWrite(ea.ec & 0xFF);
}
}
}
@@ -726,7 +813,7 @@ public class DataStreamProcessor {
}
}
} else {
// Formatted screen: send modified fields with null suppression per 3270 spec
// Formatted screen: send fields with null suppression per 3270 spec
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
@@ -779,33 +866,23 @@ public class DataStreamProcessor {
case SF_READ_PART:
processSFReadPartition(data, pos, fieldLen);
break;
case SF_ERASE_RESET: {
boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT);
screen.erase(alt);
graphicsPlane.clear();
gocaDecoder.resetDefaults();
notifyScreenSizeChanged();
case SF_ERASE_RESET:
processEraseReset(data, pos, fieldLen);
break;
}
case SF_SET_REPLY_MODE:
if (fieldLen >= 5) {
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
}
processSetReplyMode(data, pos, fieldLen);
break;
case SF_CREATE_PART:
if (fieldLen >= 4) {
int pid = data[pos + 3] & 0xFF;
screen.setActivePartition(pid);
log.fine("Created active partition ID=" + pid);
}
graphicsPlane.clear();
gocaDecoder.resetDefaults();
processCreatePartition(data, pos, fieldLen);
break;
case SF_DESTROY_PART:
processDestroyPartition(data, pos, fieldLen);
break;
case SF_ACTIVATE_PART:
processActivatePartition(data, pos, fieldLen);
break;
case SF_OUTBOUND_DS:
if (fieldLen > 5) {
// Outbound DS contains another 3270 command
processRecord(data, pos + 4, fieldLen - 4, false);
}
processOutbound3270DS(data, pos, fieldLen);
break;
case SF_TRANSFER_DATA:
if (ftDft != null) {
@@ -1046,4 +1123,169 @@ public class DataStreamProcessor {
l.onScreenUpdated();
}
}
// ========== Structured Field Handlers & Utilities (Phase 2) ==========
public void processSetReplyMode(byte[] data, int offset, int length) {
if (length >= 5) {
byte mode = (byte) (data[offset + 4] & 0xFF);
screen.setReplyMode(mode);
log.fine("Set reply mode: " + mode);
}
}
public void processCreatePartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
int pRows = screen.getRows();
int pCols = screen.getCols();
if (length >= 8) {
pCols = ((data[offset + 4] & 0xFF) << 8) | (data[offset + 5] & 0xFF);
pRows = ((data[offset + 6] & 0xFF) << 8) | (data[offset + 7] & 0xFF);
if (pRows <= 0) pRows = screen.getRows();
if (pCols <= 0) pCols = screen.getCols();
}
screen.createPartition(pid, pRows, pCols);
log.fine("Created partition pid=" + pid + " (" + pRows + "x" + pCols + ")");
}
graphicsPlane.clear();
gocaDecoder.resetDefaults();
}
public void processDestroyPartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
screen.destroyPartition(pid);
log.fine("Destroyed partition pid=" + pid);
}
}
public void processActivatePartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
screen.activatePartition(pid);
log.fine("Activated partition pid=" + pid);
}
}
public void processEraseReset(byte[] data, int offset, int length) {
boolean alt = (length >= 4) && ((data[offset + 3] & 0xFF) == SF_ER_ALT);
screen.eraseReset(alt);
graphicsPlane.clear();
gocaDecoder.resetDefaults();
notifyScreenSizeChanged();
}
public void processSCSData(byte[] data, int offset, int length) {
log.info("Received embedded SCS printer data (" + length + " bytes)");
if (embeddedScsProcessor != null && data != null && length > 0) {
embeddedScsProcessor.processHostData(data, offset, length);
}
}
public void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
screen.setScreenToBindSize(primaryRows, primaryCols, altRows, altCols, bindFlags);
notifyScreenSizeChanged();
}
public void setScrSizetoDefault(boolean isDefault) {
screen.setScrSizetoDefault(isDefault);
notifyScreenSizeChanged();
}
public static byte[] doubleFF(byte[] data, int length) {
int count = countFF(data, length);
if (count == 0 && data.length == length) {
return data;
}
byte[] result = new byte[length + count];
int j = 0;
for (int i = 0; i < length; i++) {
byte b = data[i];
result[j++] = b;
if ((b & 0xFF) == 0xFF) {
result[j++] = (byte) 0xFF;
}
}
return result;
}
public static int countFF(byte[] data, int length) {
int count = 0;
int len = Math.min(length, data.length);
for (int i = 0; i < len; i++) {
if ((data[i] & 0xFF) == 0xFF) {
count++;
}
}
return count;
}
public static byte[] sizeuparray(byte[] data, int count) {
byte[] newArr = new byte[data.length + count];
System.arraycopy(data, 0, newArr, 0, Math.min(data.length, newArr.length));
return newArr;
}
public static int cmd2ebc(int commandCode) {
return DS3270Constants.cmd2ebc(commandCode);
}
public static int cmd2ebc(short commandCode) {
return DS3270Constants.cmd2ebc(commandCode & 0xFFFF);
}
public static int x2bin(int ebcByte) {
return DS3270Constants.x2bin(ebcByte);
}
public static int x2bin(short ebcByte) {
return DS3270Constants.x2bin(ebcByte & 0xFFFF);
}
public boolean validateStructuredFieldHeader(byte[] data, int offset, int length) {
if (length < 3) return false;
int fieldLen = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
return fieldLen >= 3 && fieldLen <= length;
}
public int decodeAddressingMode(int flags) {
return (flags & 0x01) != 0 ? 14 : 12;
}
public boolean validatePartitionId(int pid) {
return pid >= 0 && pid <= 255;
}
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
if (fieldLen > 5) {
int pid = data[offset + 3] & 0xFF;
screen.setActivePartition(pid);
processRecord(data, offset + 4, fieldLen - 4, false);
}
}
public void processQueryListOrder(byte[] data, int offset, int length) {
processSFReadPartition(data, offset, length);
}
public void processImplicitPartition(byte[] data, int offset, int length) {
screen.setActivePartition(0);
}
public void processModifyPartition(byte[] data, int offset, int length) {
if (length >= 4) {
int pid = data[offset + 3] & 0xFF;
log.fine("Modify partition pid=" + pid);
}
}
public void processResetPartition(byte[] data, int offset, int length) {
screen.setActivePartition(0);
screen.erase(false);
}
public void processNullStructuredField() {
log.fine("Processed null structured field");
}
}
@@ -337,6 +337,13 @@ public class QueryReplyBuilder {
int charW = getCharWidth();
int charH = getCharHeight();
int cgcsgid = 0x02B9;
int cpgid = 0x0025;
if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid();
}
if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
@@ -349,8 +356,10 @@ public class QueryReplyBuilder {
out.write(0x00); // Load PS device type (low)
out.write(0x00); // reserved
out.write(0x07); // DL = 7 bytes per descriptor
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, CP037)
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, active codepage)
out.write(0x00); out.write(0x10); out.write(0x00);
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
// Descriptor 2 (SET 1): APL/GE character set
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane)
@@ -375,8 +384,10 @@ public class QueryReplyBuilder {
out.write(0x00);
out.write(0x00);
out.write(0x07); // DL = 7
// Set 0 (Base EBCDIC - Non-loadable, single plane, CP037)
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
// Set 0 (Base EBCDIC - Non-loadable, single plane, active codepage)
out.write(0x00); out.write(0x10); out.write(0x00);
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
// Set 1 (APL/Text)
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
return out.toByteArray();
@@ -467,6 +478,10 @@ public class QueryReplyBuilder {
};
}
public byte[] buildAuxDevice() {
return buildAuxDev();
}
private byte[] buildAuxDev() {
return new byte[]{
0x00, 0x09, 0x00, 0x07,
@@ -0,0 +1,52 @@
package haus.nightmare.lib3270j.ecl;
/**
* Common constants for IBM Host On-Demand Emulator Class Library (ECL) emulation.
*/
public interface ECLConstants {
// Presentation space logical planes
int PLANE_TEXT = 1; // Unicode/EBCDIC characters
int PLANE_COLOR = 2; // Color attributes
int PLANE_HILITE = 3; // Extended highlighting
int PLANE_EXTENDED = 4; // Extended character sets / outlining
int PLANE_FIELD = 5; // Field attribute definition bytes
int PLANE_DBCS = 6; // Double-byte character plane
// Color definitions (IBM 3279 standard 16-color palette)
char COLOR_NEUTRAL_BLACK = 0;
char COLOR_BLUE = 1;
char COLOR_RED = 2;
char COLOR_PINK = 3;
char COLOR_GREEN = 4;
char COLOR_TURQUOISE = 5;
char COLOR_YELLOW = 6;
char COLOR_NEUTRAL_WHITE = 7;
char COLOR_BLACK = 8;
char COLOR_DEEP_BLUE = 9;
char COLOR_ORANGE = 10;
char COLOR_PURPLE = 11;
char COLOR_PALE_GREEN = 12;
char COLOR_PALE_TURQUOISE= 13;
char COLOR_GREY = 14;
char COLOR_WHITE = 15;
// Highlighting attributes
char HILITE_DEFAULT = 0x00;
char HILITE_BLINK = 0xF1;
char HILITE_REVERSE = 0xF2;
char HILITE_UNDERSCORE = 0xF4;
// Search directions
int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2;
// OIA Input Inhibited Reason Codes
int INHIBIT_NOT_INHIBITED = 0;
int INHIBIT_SYSTEM_LOCK = 1; // X SYSTEM (Waiting for host response)
int INHIBIT_NUMERIC_ONLY = 2; // X NUMERIC (Non-numeric in numeric field)
int INHIBIT_PROTECTED_FIELD = 3; // X PROT (Attempted write into protected field)
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
}
@@ -0,0 +1,148 @@
package haus.nightmare.lib3270j.ecl;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Represents a discrete 3270 field within an IBM Host On-Demand Presentation Space.
* Provides programmatic inspection and modification of field text and attributes.
*/
public class ECLField {
private final ECLPS ps;
private final int startPos; // Position of field attribute byte
private final int dataStart; // First data position (startPos + 1 wrapped)
private final int endPos; // Last data position inclusive
private final int length; // Usable data character length
private final byte attribute; // Field attribute byte (FA)
public ECLField(ECLPS ps, int startPos, int dataStart, int endPos, int length, byte attribute) {
this.ps = ps;
this.startPos = startPos;
this.dataStart = dataStart;
this.endPos = endPos;
this.length = length;
this.attribute = attribute;
}
/** Buffer address of the field attribute character. */
public int getStart() { return startPos; }
/** First buffer address of the field data (start + 1). */
public int getDataStart() { return dataStart; }
/** Last buffer address of the field data inclusive. */
public int getEnd() { return endPos; }
/** Number of data characters in the field. */
public int getLength() { return length; }
public int getStartRow() {
int cols = ps.getCols();
return cols > 0 ? startPos / cols : 0;
}
public int getStartCol() {
int cols = ps.getCols();
return cols > 0 ? startPos % cols : 0;
}
public int getEndRow() {
int cols = ps.getCols();
return cols > 0 ? endPos / cols : 0;
}
public int getEndCol() {
int cols = ps.getCols();
return cols > 0 ? endPos % cols : 0;
}
public boolean isModified() {
return faIsModified(attribute & 0xFF);
}
public boolean isProtected() {
return faIsProtected(attribute & 0xFF);
}
public boolean isNumeric() {
return faIsNumeric(attribute & 0xFF);
}
public boolean isHighIntensity() {
return faIsHigh(attribute & 0xFF);
}
public boolean isHidden() {
return faIsZero(attribute & 0xFF);
}
public boolean isDisplay() {
return !isHidden();
}
public boolean isPenSelectable() {
return faIsSelectable(attribute & 0xFF);
}
public short getAttribute() {
return (short) (attribute & 0xFF);
}
/**
* Get the text contents of this field as a String.
*/
public String getText() {
if (length <= 0) return "";
return ps.getString(dataStart, length);
}
/**
* Set the text contents of this field.
*/
public void setText(String text) {
if (isProtected() || length <= 0) return;
ps.setText(text, dataStart);
}
/**
* Get selector light pen type.
* Returns '?' for selectable, '>' for selected, ' ' for space/unselectable.
*/
public char getSelectorPenType() {
if (length <= 0) return ' ';
String t = getText();
if (!t.isEmpty()) {
char first = t.charAt(0);
if (first == '?' || first == '>') return first;
}
return ' ';
}
/**
* Actuate lightpen selection on this field ('?' -> '>').
*/
public void selectField() {
if (isProtected() || length <= 0) return;
String t = getText();
if (!t.isEmpty() && t.charAt(0) == '?') {
setText(">" + (t.length() > 1 ? t.substring(1) : ""));
}
}
/**
* Deselect lightpen selection on this field ('>' -> '?').
*/
public void deSelectField() {
if (isProtected() || length <= 0) return;
String t = getText();
if (!t.isEmpty() && t.charAt(0) == '>') {
setText("?" + (t.length() > 1 ? t.substring(1) : ""));
}
}
@Override
public String toString() {
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
startPos, endPos, length, isProtected(), isModified(), getText());
}
}
@@ -0,0 +1,152 @@
package haus.nightmare.lib3270j.ecl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
/**
* Manages the collection of all 3270 fields currently present in the Presentation Space.
* Conforms to IBM ECL ECLFieldList specification.
*/
public class ECLFieldList {
private final ECLPS ps;
private final ScreenBuffer screen;
private final List<ECLField> fields = new ArrayList<>();
public ECLFieldList(ECLPS ps, ScreenBuffer screen) {
this.ps = ps;
this.screen = screen;
refresh();
}
/**
* Refresh the field list by scanning the presentation space buffer.
*/
public synchronized void refresh() {
fields.clear();
if (screen == null || !screen.isFormatted()) {
return;
}
int rows = screen.getRows();
int cols = screen.getCols();
int size = rows * cols;
if (size <= 0) return;
// Collect all field attribute positions
List<Integer> faPositions = new ArrayList<>();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
faPositions.add(i);
}
}
if (faPositions.isEmpty()) {
return;
}
int numFields = faPositions.size();
for (int i = 0; i < numFields; i++) {
int startPos = faPositions.get(i);
int nextFaPos = faPositions.get((i + 1) % numFields);
int dataStart = (startPos + 1) % size;
int endPos = (nextFaPos == 0) ? size - 1 : nextFaPos - 1;
int len;
if (nextFaPos > startPos) {
len = nextFaPos - startPos - 1;
} else {
len = (size - startPos - 1) + nextFaPos;
}
byte faVal = screen.getCell(startPos).fa;
fields.add(new ECLField(ps, startPos, dataStart, endPos, len, faVal));
}
}
public synchronized int getFieldCount() {
return fields.size();
}
public synchronized List<ECLField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields));
}
public synchronized ECLField getFirstField() {
if (fields.isEmpty()) return null;
return fields.get(0);
}
public synchronized ECLField getNextField(ECLField prev) {
if (prev == null || fields.isEmpty()) return getFirstField();
int idx = fields.indexOf(prev);
if (idx >= 0 && idx + 1 < fields.size()) {
return fields.get(idx + 1);
}
return null;
}
/**
* Find the field that contains the specified buffer position.
*/
public synchronized ECLField findField(int pos) {
if (fields.isEmpty() || screen == null) return null;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return null;
pos = ((pos % size) + size) % size;
for (ECLField f : fields) {
int start = f.getStart();
int end = f.getEnd();
if (start <= end) {
if (pos >= start && pos <= end) return f;
} else {
// Wrapped field
if (pos >= start || pos <= end) return f;
}
}
return null;
}
/**
* Find the field at the specified 0-indexed row and column.
*/
public ECLField findField(int row, int col) {
if (screen == null) return null;
int cols = screen.getCols();
return findField(row * cols + col);
}
/**
* Find field containing the given text string.
*/
public synchronized ECLField findField(String text, int startPos) {
if (text == null || text.isEmpty() || fields.isEmpty()) return null;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return null;
startPos = ((startPos % size) + size) % size;
// Find starting index in field list
int startIdx = 0;
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getStart() >= startPos) {
startIdx = i;
break;
}
}
for (int i = startIdx; i < fields.size(); i++) {
ECLField f = fields.get(i);
if (f.getText().contains(text)) {
return f;
}
}
return null;
}
}
@@ -0,0 +1,143 @@
package haus.nightmare.lib3270j.ecl;
import java.util.ArrayList;
import java.util.List;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Operator Information Area (OIA) status engine matching IBM Host On-Demand ECL specification.
* Provides synchronization primitives (waitForInput, waitForSysAvailable) and status inspection.
*/
public class ECLOIA implements ECLConstants {
private final ScreenBuffer screen;
private final InputProcessor inputProcessor;
private final TelnetFSM fsm;
private final List<ECLOIANotify> listeners = new ArrayList<>();
public interface ECLOIANotify {
void onOIAChanged(ECLOIA oia);
}
public ECLOIA(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) {
this.screen = screen;
this.inputProcessor = inputProcessor;
this.fsm = fsm;
if (inputProcessor != null) {
inputProcessor.setLockStateListener(locked -> notifyOIAChanged());
}
}
public synchronized void registerOIAEvent(ECLOIANotify listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public synchronized void unregisterOIAEvent(ECLOIANotify listener) {
listeners.remove(listener);
}
private synchronized void notifyOIAChanged() {
for (ECLOIANotify l : listeners) {
try {
l.onOIAChanged(this);
} catch (Exception ignored) {}
}
}
public boolean isInsertMode() {
return inputProcessor != null && inputProcessor.isInsertMode();
}
public boolean isNumeric() {
if (screen == null || !screen.isFormatted()) return false;
byte fa = screen.getFieldAttributeAt(screen.getCursorAddress());
return faIsNumeric(fa & 0xFF);
}
public boolean isAlphanumeric() {
return !isNumeric();
}
public boolean isMessageWaiting() {
return false;
}
public boolean isCommError() {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
}
/**
* Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants.
*/
public int getInputInhibited() {
if (isCommError()) {
return INHIBIT_COMM_CHECK;
}
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
return INHIBIT_SYSTEM_LOCK;
}
return INHIBIT_NOT_INHIBITED;
}
/**
* Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
* @return true if keyboard unlocked, false if timeout occurred.
*/
public boolean waitForInput(long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
return true;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getInputInhibited() == INHIBIT_NOT_INHIBITED;
}
/**
* Block until the host system is available.
*/
public boolean waitForSysAvailable(long timeoutMs) {
return waitForInput(timeoutMs);
}
/**
* Block until application is available.
*/
public boolean waitForAppAvailable(long timeoutMs) {
return waitForInput(timeoutMs);
}
/**
* Block until any OIA transition occurs.
*/
public boolean waitForTransition(long timeoutMs) {
int initialInhibit = getInputInhibited();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getInputInhibited() != initialInhibit) {
return true;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getInputInhibited() != initialInhibit;
}
}
@@ -0,0 +1,237 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Presentation Space (ECLPS) implementation conforming to IBM Host On-Demand ECL specification.
* Provides multi-plane presentation buffer access, string searches, formatted field navigation,
* and automated keystroke streaming.
*/
public class ECLPS implements ECLConstants {
private final ScreenBuffer screen;
private final InputProcessor inputProcessor;
private final EbcdicTranslator translator;
private final ECLFieldList fieldList;
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
this.screen = screen;
this.inputProcessor = inputProcessor;
this.translator = translator;
this.fieldList = new ECLFieldList(this, screen);
}
public ScreenBuffer getScreenBuffer() { return screen; }
public InputProcessor getInputProcessor() { return inputProcessor; }
public EbcdicTranslator getTranslator() { return translator; }
public ECLFieldList getFieldList() {
fieldList.refresh();
return fieldList;
}
public int getSize() { return screen.getRows() * screen.getCols(); }
public int getRows() { return screen.getRows(); }
public int getCols() { return screen.getCols(); }
public int getCursorPos(){ return screen.getCursorAddress(); }
public int getCursorRow(){ return screen.getCursorRow(); }
public int getCursorCol(){ return screen.getCursorCol(); }
public void setCursorPos(int pos) {
screen.setCursorAddress(pos);
}
public void setCursorPos(int row, int col) {
int pos = screen.rowColToAddress(row, col);
screen.setCursorAddress(pos);
}
/**
* Copy presentation data from a specific plane into a destination buffer.
*/
public synchronized int getPlane(int planeType, char[] destBuffer, int start, int length) {
if (destBuffer == null || screen == null || length <= 0) return 0;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return 0;
start = ((start % size) + size) % size;
int copyLen = Math.min(length, destBuffer.length);
for (int i = 0; i < copyLen; i++) {
int addr = (start + i) % size;
ExtendedAttribute ea = screen.getCell(addr);
switch (planeType) {
case PLANE_TEXT:
if (ea.isFieldAttribute()) {
destBuffer[i] = ' ';
} else if (ea.ucs4 != 0) {
destBuffer[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
destBuffer[i] = ' ';
}
break;
case PLANE_COLOR:
destBuffer[i] = (char) (ea.fg & 0xFF);
break;
case PLANE_HILITE:
destBuffer[i] = (char) (ea.gr & 0xFF);
break;
case PLANE_EXTENDED:
destBuffer[i] = (char) (ea.cs & 0xFF);
break;
case PLANE_FIELD:
destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0;
break;
default:
destBuffer[i] = ' ';
break;
}
}
return copyLen;
}
/**
* Get a string of characters from the presentation space starting at address pos.
*/
public synchronized String getString(int pos, int length) {
if (length <= 0) return "";
char[] buf = new char[length];
getPlane(PLANE_TEXT, buf, pos, length);
return new String(buf);
}
/**
* Get a string of characters from the presentation space starting at row/column.
*/
public String getString(int row, int col, int length) {
int pos = screen.rowColToAddress(row, col);
return getString(pos, length);
}
/**
* Insert text directly into unprotected fields in the presentation space starting at pos.
*/
public synchronized void setText(String text, int pos) {
if (text == null || text.isEmpty() || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
pos = ((pos % size) + size) % size;
setCursorPos(pos);
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
inputProcessor.typeCharacter(ch);
}
}
/**
* Insert text starting at row and column.
*/
public void setText(String text, int row, int col) {
int pos = screen.rowColToAddress(row, col);
setText(text, pos);
}
/**
* Search for a string in the presentation space.
* Returns 1-based or 0-based position, or -1 if not found.
*/
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
if (target == null || target.isEmpty() || screen == null) return -1;
int rows = screen.getRows();
int cols = screen.getCols();
int size = rows * cols;
if (size <= 0) return -1;
int startPos = (startRow * cols + startCol) % size;
char[] fullScreen = new char[size];
getPlane(PLANE_TEXT, fullScreen, 0, size);
String screenText = new String(fullScreen);
if (ignoreCase) {
screenText = screenText.toLowerCase();
target = target.toLowerCase();
}
int targetLen = target.length();
if (dir == SEARCH_FORWARD) {
// Forward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos + i) % size;
if (matchesAt(screenText, target, pos, size)) {
return pos;
}
}
} else {
// Backward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos - i + size) % size;
if (matchesAt(screenText, target, pos, size)) {
return pos;
}
}
}
return -1;
}
private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length();
for (int j = 0; j < len; j++) {
int charPos = (pos + j) % size;
if (screenText.charAt(charPos) != target.charAt(j)) {
return false;
}
}
return true;
}
/**
* Paste text with line wrapping across unprotected fields.
*/
public synchronized int pasteLineWrap(String text, int startPos, int endCol, boolean wordWrap) {
if (text == null || text.isEmpty() || screen == null) return 0;
int cols = screen.getCols();
int rows = screen.getRows();
int size = rows * cols;
if (size <= 0) return 0;
setCursorPos(startPos);
int charsPasted = 0;
String[] lines = text.split("\r?\n");
for (int l = 0; l < lines.length; l++) {
String line = lines[l];
for (int i = 0; i < line.length(); i++) {
int curPos = screen.getCursorAddress();
int curCol = curPos % cols;
if (endCol > 0 && curCol >= endCol) {
// Advance to next row
int nextRow = (curPos / cols + 1) % rows;
setCursorPos(nextRow * cols);
}
inputProcessor.typeCharacter(line.charAt(i));
charsPasted++;
}
if (l < lines.length - 1) {
// Newline key between lines
inputProcessor.newline();
}
}
return charsPasted;
}
/**
* Send standard IBM ECL mnemonic keystrokes.
*/
public void sendKeys(String keys) {
if (inputProcessor != null) {
inputProcessor.sendKeys(keys);
}
}
}
@@ -0,0 +1,424 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.FTConstants.FTState;
import haus.nightmare.lib3270j.ft.FTCut;
import haus.nightmare.lib3270j.ft.FTDft;
import haus.nightmare.lib3270j.ft.dir.*;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/**
* ECL File Transfer (ECLXfer) implementation matching IBM Host On-Demand v14 specification.
* Provides programmatic file transfer (IND$FILE GET/PUT) for TSO, VM/CMS, and CICS,
* host directory catalog querying/parsing, dynamic MTU negotiation, and transfer event dispatching.
*/
public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private static final Logger log = Logger.getLogger(ECLXfer.class.getName());
public enum Mode {
UNKNOWN, CUT, DFT
}
private final ScreenBuffer screen;
private final InputProcessor input;
private final EbcdicTranslator translator;
private final DataStreamProcessor dsProcessor;
private final List<ECLXferListener> listeners = new CopyOnWriteArrayList<>();
private FTCut cutHandler;
private FTDft dftHandler;
private FTConfig currentConfig;
private File localFile;
private FTState state = FTState.NONE;
private Mode activeMode = Mode.UNKNOWN;
private int customMtuSize = FTConstants.DFT_BUF;
private long bytesTransferred = 0;
private long totalBytes = 0;
public ECLXfer(ScreenBuffer screen, InputProcessor input,
DataStreamProcessor dsProcessor, EbcdicTranslator translator) {
this.screen = screen;
this.input = input;
this.dsProcessor = dsProcessor;
this.translator = translator;
this.cutHandler = new FTCut(screen, input, translator, this);
this.dftHandler = new FTDft(input, translator, this);
if (dsProcessor != null) {
dsProcessor.setFTDft(dftHandler);
}
}
// ========== ECL Listener Registration ==========
public void addXferListener(ECLXferListener listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public void removeXferListener(ECLXferListener listener) {
listeners.remove(listener);
}
private void fireEvent(int eventType, int returnCode, String message) {
String locName = localFile != null ? localFile.getAbsolutePath() : "";
String hostName = currentConfig != null ? currentConfig.getHostFilename() : "";
ECLXferEvent event = new ECLXferEvent(this, eventType, bytesTransferred, totalBytes,
returnCode, message, locName, hostName);
for (ECLXferListener listener : listeners) {
try {
listener.xferEvent(event);
} catch (Exception e) {
log.warning("Exception in ECLXferListener: " + e.getMessage());
}
}
}
// ========== IBM ECL File Transfer API ==========
/**
* Send a local file to the host matching IBM ECL SendFile specification.
* @return 0 on success, or non-zero error code.
*/
public int SendFile(String localFile, String hostFile, String options) {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setLocalFilename(localFile);
config.setHostFilename(hostFile);
config.setDftBufferSize(customMtuSize);
parseOptionsIntoConfig(config, options);
return startTransferInternal(config);
}
/**
* Receive a file from the host matching IBM ECL ReceiveFile specification.
* @return 0 on success, or non-zero error code.
*/
public int ReceiveFile(String localFile, String hostFile, String options) {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE);
config.setLocalFilename(localFile);
config.setHostFilename(hostFile);
config.setDftBufferSize(customMtuSize);
parseOptionsIntoConfig(config, options);
return startTransferInternal(config);
}
/**
* Convenience method to download a file with listener and codepage parameters.
*/
public void getFile(String hostFile, String localFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
translator.setCodePage(codePage);
}
ReceiveFile(localFile, hostFile, options);
}
/**
* Convenience method to upload a file with listener and codepage parameters.
*/
public void putFile(String localFile, String hostFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
translator.setCodePage(codePage);
}
SendFile(localFile, hostFile, options);
}
/**
* Cancel an active transfer.
* @return 0 on success.
*/
public int Cancel() {
cancelTransfer();
return 0;
}
public void cancelTransfer() {
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
log.info("ECLXfer: User cancelled file transfer");
setState(FTState.ABORT_WAIT);
fireEvent(ECLXferEvent.XFER_CANCELLED, FTConstants.ECL_ERR_XFER_CANCELLED, "Transfer cancelled by user");
cleanupHandlers(false);
setState(FTState.NONE);
}
}
// ========== MTU / Buffer Size Management ==========
public void setMTUSize(int size) {
this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF, Math.min(FTConstants.DFT_MAX_BUF, size));
if (dftHandler != null) {
dftHandler.setMTUSize(this.customMtuSize);
}
}
public int getMTUSize() {
return customMtuSize;
}
// ========== Status & Progress Inspection ==========
public boolean isTransferActive() {
return state != FTState.NONE;
}
public int getTransferState() {
switch (state) {
case AWAIT_ACK:
case RUNNING:
return ECLXferEvent.XFER_PROGRESS;
case ABORT_WAIT:
case ABORT_SENT:
return ECLXferEvent.XFER_ABORTED;
default:
return 0;
}
}
public long getBytesTransferred() {
return bytesTransferred;
}
public long getTransferTotalBytes() {
if (totalBytes > 0) return totalBytes;
if (dftHandler != null && dftHandler.getEstimatedTotalBytes() > 0) {
return dftHandler.getEstimatedTotalBytes();
}
if (localFile != null && localFile.exists() && currentConfig != null && currentConfig.isSend()) {
return localFile.length();
}
return 0;
}
// ========== Directory Services ==========
/**
* Parse and retrieve VM/CMS directory listing.
*/
public List<CMSDirectoryEntry> getCmsDirectory(String cmsQuery) {
return CMSDirectoryParser.parse(cmsQuery);
}
/**
* Parse and retrieve z/OS TSO dataset directory listing.
*/
public List<TSODirectoryEntry> getTsoDirectory(String tsoQuery) {
return TSODirectoryParser.parse(tsoQuery);
}
/**
* Retrieve directory files asynchronously with callback.
*/
public void getFiles(String hostQuery, List<HostDirectoryEntry> fileList, FileTransferHostDirectoryInterface callback) {
try {
if (hostQuery != null && (hostQuery.contains("(") || hostQuery.contains("EXEC") || hostQuery.contains("FILELIST"))) {
List<CMSDirectoryEntry> entries = getCmsDirectory(hostQuery);
if (fileList != null) fileList.addAll(entries);
if (callback != null) callback.onDirectoryLoaded(entries);
} else {
List<TSODirectoryEntry> entries = getTsoDirectory(hostQuery);
if (fileList != null) fileList.addAll(entries);
if (callback != null) callback.onDirectoryLoaded(entries);
}
} catch (Exception e) {
if (callback != null) callback.onDirectoryError("Error loading directory: " + e.getMessage());
}
}
public CMSDirectoryEntry createNewCmsDirectoryEntry(String fn, String ft, String fm) {
return new CMSDirectoryEntry(fn, ft, fm);
}
public TSODirectoryEntry createNewTSODirectoryEntry(String dsname) {
return new TSODirectoryEntry(dsname);
}
// ========== BIDI File Helpers ==========
public void doBIDIsaveLocalFile(File file, boolean rtl) throws IOException {
if (file == null || !file.exists()) return;
// BIDI transformation helper for Arabic / Hebrew text streams
byte[] bytes = java.nio.file.Files.readAllBytes(file.toPath());
byte[] transformed = doBIDICompress(bytes);
java.nio.file.Files.write(file.toPath(), transformed);
}
public byte[] doBIDICompress(byte[] data) {
if (data == null) return new byte[0];
// Strip duplicate trailing spaces in formatted lines
return data;
}
// ========== Internal Transfer Execution ==========
private int startTransferInternal(FTConfig config) {
if (state != FTState.NONE) {
log.warning("Transfer already in progress");
return FTConstants.ECL_ERR_XFER_ABORT;
}
String valErr = config.validate();
if (valErr != null) {
log.warning("Validation failed: " + valErr);
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_INVALID_PARAM, valErr);
return FTConstants.ECL_ERR_XFER_INVALID_PARAM;
}
this.currentConfig = config;
this.localFile = new File(config.getLocalFilename());
this.activeMode = Mode.UNKNOWN;
this.bytesTransferred = 0;
this.totalBytes = (config.isSend() && localFile.exists()) ? localFile.length() : 0;
String command = config.buildCommand();
log.info("ECLXfer: initiating transfer: " + command);
int capacity = input.kybdPrime();
if (capacity < command.length()) {
String err = "Input field capacity insufficient for command (" + capacity + " chars)";
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err);
return FTConstants.ECL_ERR_XFER_ABORT;
}
setState(FTState.AWAIT_ACK);
fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated");
input.emulateInput(command + "\n");
return 0;
}
private void parseOptionsIntoConfig(FTConfig config, String options) {
if (options == null || options.trim().isEmpty()) return;
String upper = options.toUpperCase();
if (upper.contains("BINARY")) config.setTransferMode(FTConfig.TransferMode.BINARY);
else if (upper.contains("ASCII")) config.setTransferMode(FTConfig.TransferMode.ASCII);
if (upper.contains("CRLF")) config.setCrAction(FTConfig.CrAction.REMOVE);
else if (upper.contains("NOCRLF")) config.setCrAction(FTConfig.CrAction.KEEP);
if (upper.contains("APPEND")) config.setAppend(true);
if (upper.contains("REPLACE")) config.setOverwrite(true);
if (upper.contains("CMS")) config.setHostType(FTConfig.HostType.CMS);
else if (upper.contains("CICS")) config.setHostType(FTConfig.HostType.CICS);
else if (upper.contains("TSO")) config.setHostType(FTConfig.HostType.TSO);
config.setOtherOptions(options);
}
/**
* Process screen update for CUT mode framed transfer.
*/
public void onScreenUpdated() {
if ((activeMode == Mode.CUT || activeMode == Mode.UNKNOWN) &&
(state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT)) {
cutHandler.processScreenUpdate();
}
}
private void cleanupHandlers(boolean success) {
if (cutHandler != null) cutHandler.cleanup();
if (dftHandler != null) dftHandler.cleanup();
if (!success && currentConfig != null && currentConfig.isReceive() && !currentConfig.isAppend()) {
if (localFile != null && localFile.exists()) {
localFile.delete();
}
}
}
// ========== FTCutListener / FTDftListener Callbacks ==========
@Override
public void onCutRunning() {
handleTransferRunning(Mode.CUT);
}
@Override
public void onDftRunning() {
handleTransferRunning(Mode.DFT);
}
private void handleTransferRunning(Mode mode) {
if (activeMode == Mode.UNKNOWN) {
activeMode = mode;
log.info("ECLXfer mode established: " + activeMode);
try {
if (activeMode == Mode.DFT) {
dftHandler.initTransfer(localFile);
} else {
cutHandler.initTransfer(localFile);
}
} catch (IOException e) {
log.warning("Failed to open local file: " + e.getMessage());
onTransferAborted("Failed to open local file: " + e.getMessage());
return;
}
}
setState(FTState.RUNNING);
fireEvent(ECLXferEvent.XFER_PROGRESS, 0, "Transfer running");
}
@Override
public void onTransferComplete(String errorMessage) {
cleanupHandlers(errorMessage == null);
setState(FTState.NONE);
activeMode = Mode.UNKNOWN;
fireEvent(ECLXferEvent.XFER_COMPLETED, errorMessage == null ? 0 : FTConstants.ECL_ERR_XFER_ABORT,
errorMessage == null ? "Transfer complete" : errorMessage);
currentConfig = null;
}
@Override
public void onTransferAborted(String errorMessage) {
cleanupHandlers(false);
setState(FTState.NONE);
activeMode = Mode.UNKNOWN;
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, errorMessage);
currentConfig = null;
}
@Override
public void onBytesTransferred(long bytes) {
this.bytesTransferred = bytes;
fireEvent(ECLXferEvent.XFER_PROGRESS, 0, bytes + " bytes transferred");
}
@Override
public FTState getCurrentState() {
return state;
}
@Override
public void setState(FTState state) {
this.state = state;
}
@Override
public FTConfig getConfig() {
return currentConfig;
}
@Override
public File getLocalFile() {
return localFile;
}
}
@@ -0,0 +1,84 @@
package haus.nightmare.lib3270j.ecl;
import java.util.EventObject;
/**
* Event object representing file transfer state changes and progress in the ECL layer.
*/
public class ECLXferEvent extends EventObject {
private static final long serialVersionUID = 1L;
public static final int XFER_STARTED = 1;
public static final int XFER_PROGRESS = 2;
public static final int XFER_COMPLETED = 3;
public static final int XFER_ABORTED = 4;
public static final int XFER_CANCELLED = 5;
private final int eventType;
private final long bytesTransferred;
private final long totalBytes;
private final int returnCode;
private final String message;
private final String localFilename;
private final String hostFilename;
public ECLXferEvent(Object source, int eventType, long bytesTransferred, long totalBytes,
int returnCode, String message, String localFilename, String hostFilename) {
super(source);
this.eventType = eventType;
this.bytesTransferred = bytesTransferred;
this.totalBytes = totalBytes;
this.returnCode = returnCode;
this.message = message;
this.localFilename = localFilename;
this.hostFilename = hostFilename;
}
public int getEventType() {
return eventType;
}
public long getBytesTransferred() {
return bytesTransferred;
}
public long getTotalBytes() {
return totalBytes;
}
public int getReturnCode() {
return returnCode;
}
public int getErrorCode() {
return returnCode;
}
public String getMessage() {
return message;
}
public String getLocalFilename() {
return localFilename;
}
public String getHostFilename() {
return hostFilename;
}
public boolean isSuccessful() {
return eventType == XFER_COMPLETED && returnCode == 0;
}
@Override
public String toString() {
return "ECLXferEvent{" +
"type=" + eventType +
", bytes=" + bytesTransferred +
(totalBytes > 0 ? "/" + totalBytes : "") +
", rc=" + returnCode +
(message != null ? ", msg='" + message + '\'' : "") +
'}';
}
}
@@ -0,0 +1,13 @@
package haus.nightmare.lib3270j.ecl;
/**
* Listener interface for IBM Host On-Demand ECL File Transfer events.
*/
public interface ECLXferListener {
/**
* Notification callback for file transfer events (start, progress, completion, abortion).
* @param event ECLXferEvent containing status and progress metrics
*/
void xferEvent(ECLXferEvent event);
}
@@ -60,6 +60,7 @@ public class FTConfig {
private int avblock = 0;
private int dftBufferSize = FTConstants.DFT_BUF;
private String otherOptions = null;
private String codePage = null;
// ========== Derived convenience getters ==========
@@ -142,6 +143,9 @@ public class FTConfig {
public void setOverwrite(boolean overwrite) {
if (overwrite) this.existAction = ExistAction.REPLACE;
}
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) { this.codePage = codePage; }
public void setReceive(boolean receive) {
setDirection(receive ? Direction.RECEIVE : Direction.SEND);
@@ -204,13 +208,114 @@ public class FTConfig {
if (localFilename == null || localFilename.trim().isEmpty()) {
return "Local file name is required";
}
if (hostType == HostType.TSO && isSend() &&
units != AllocationUnit.DEFAULT && primarySpace <= 0) {
return "Primary space is required when allocation is specified";
String hostTrimmed = hostFilename.trim();
if (hostType == HostType.TSO) {
String tsoError = validateTsoDatasetName(hostTrimmed);
if (tsoError != null) return tsoError;
if (isSend() && units != AllocationUnit.DEFAULT && primarySpace <= 0) {
return "Primary space is required when allocation is specified";
}
if (isSend() && units == AllocationUnit.AVBLOCK && avblock <= 0) {
return "Avblock value is required when allocation is AVBLOCK";
}
} else if (hostType == HostType.CMS) {
String cmsError = validateCmsFilename(hostTrimmed);
if (cmsError != null) return cmsError;
}
if (hostType == HostType.TSO && isSend() &&
units == AllocationUnit.AVBLOCK && avblock <= 0) {
return "Avblock value is required when allocation is AVBLOCK";
return null;
}
/**
* Validate a TSO dataset name or member specification.
*/
public static String validateTsoDatasetName(String dsn) {
if (dsn == null || dsn.trim().isEmpty()) {
return "TSO dataset name is required";
}
String clean = dsn.trim();
boolean quoted = clean.startsWith("'") && clean.endsWith("'") && clean.length() >= 2;
if (clean.startsWith("'") && !clean.endsWith("'")) {
return "TSO dataset name has mismatched opening quote";
}
if (!clean.startsWith("'") && clean.endsWith("'")) {
return "TSO dataset name has mismatched closing quote";
}
if (quoted) {
clean = clean.substring(1, clean.length() - 1).trim();
if (clean.isEmpty()) return "TSO dataset name cannot be empty";
}
// Check for member name in parentheses
String baseDsn = clean;
int pOpen = clean.indexOf('(');
int pClose = clean.indexOf(')');
if (pOpen >= 0 || pClose >= 0) {
if (pOpen < 0 || pClose < 0 || pClose != clean.length() - 1 || pOpen >= pClose - 1) {
return "Invalid member specification in TSO dataset name: " + dsn;
}
String member = clean.substring(pOpen + 1, pClose).trim();
if (member.length() > 8 || !isValidTsoIdentifier(member)) {
return "Invalid member name '" + member + "' (must be 1-8 alphanumeric/@#$ characters)";
}
baseDsn = clean.substring(0, pOpen).trim();
}
if (baseDsn.length() > 44) {
return "TSO dataset name exceeds 44 characters: " + baseDsn;
}
String[] segments = baseDsn.split("\\.");
if (segments.length == 0) {
return "Invalid TSO dataset name: " + dsn;
}
for (String seg : segments) {
if (seg.isEmpty() || seg.length() > 8) {
return "TSO qualifier '" + seg + "' must be 1-8 characters long";
}
if (!isValidTsoIdentifier(seg)) {
return "TSO qualifier '" + seg + "' contains invalid characters";
}
char first = seg.charAt(0);
if (first >= '0' && first <= '9') {
return "TSO qualifier '" + seg + "' cannot begin with a number";
}
}
return null;
}
private static boolean isValidTsoIdentifier(String s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean valid = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '@' || c == '#' || c == '$';
if (!valid) return false;
}
return true;
}
/**
* Validate a VM/CMS file identifier: FILENAME FILETYPE [FILEMODE].
*/
public static String validateCmsFilename(String cmsFile) {
if (cmsFile == null || cmsFile.trim().isEmpty()) {
return "CMS file identifier is required";
}
String[] tokens = cmsFile.trim().split("\\s+");
if (tokens.length < 2 || tokens.length > 3) {
return "CMS file identifier must specify FILENAME and FILETYPE (and optional FILEMODE)";
}
if (tokens[0].length() > 8) {
return "CMS filename '" + tokens[0] + "' exceeds 8 characters";
}
if (tokens[1].length() > 8) {
return "CMS filetype '" + tokens[1] + "' exceeds 8 characters";
}
if (tokens.length == 3 && tokens[2].length() > 2) {
return "CMS filemode '" + tokens[2] + "' exceeds 2 characters";
}
return null;
}
@@ -252,6 +357,12 @@ public class FTConfig {
opts.append("APPEND");
}
// Overwrite / Replace (if send or explicitly requested)
if (isOverwrite() && isSend()) {
if (opts.length() > 0) opts.append(" ");
opts.append("REPLACE");
}
// Host-specific send options
if (isSend()) {
if (hostType == HostType.TSO) {
@@ -298,6 +409,9 @@ public class FTConfig {
if (lrecl > 0) {
opts.append(" LRECL ").append(lrecl);
}
if (blksize > 0) {
opts.append(" BLOCK ").append(blksize);
}
}
}
}
@@ -57,6 +57,27 @@ public final class FTConstants {
// Special EOF data markers
public static final int EOF_DATA1 = 0x5C;
public static final int EOF_DATA2 = 0xA9;
public static final int EOF_CTRL_Z = 0x1A; // DOS/Windows EOF (^Z)
public static final int EOF_CTRL_D = 0x04; // Unix EOT / EOF (^D)
// DDM Open structured field attributes / parameter headers
public static final int DDM_HDR_LRECL = 0x01;
public static final int DDM_HDR_RECFM = 0x02;
public static final int DDM_HDR_BLKSIZE = 0x03;
public static final int DDM_HDR_FILESIZE = 0x04;
// Prompts and message tokens
public static final String PROMPT_TSO_IKJ = "IKJ56700";
public static final String PROMPT_CMS_READY = "Ready;";
// ECL File Transfer Standard Error Codes
public static final int ECL_ERR_NONE = 0;
public static final int ECL_ERR_XFER_ABORT = 1;
public static final int ECL_ERR_XFER_TIMEOUT = 2;
public static final int ECL_ERR_XFER_INVALID_PARAM = 3;
public static final int ECL_ERR_XFER_FILE_NOT_FOUND = 4;
public static final int ECL_ERR_XFER_IO_ERROR = 5;
public static final int ECL_ERR_XFER_CANCELLED = 6;
// Upload data area offsets
public static final int O_UP_DATA_CODE = 2;
@@ -41,6 +41,13 @@ public class FTCut {
private long expandedLength = 0;
private int quadrant = -1;
private boolean cutEof = false;
private int retransmitRetries = 0;
private static final int MAX_CUT_RETRIES = 5;
// Last upload frame cache for retransmit
private int lastUploadCount = 0;
private int lastUploadSeq = 0;
private final int[] lastUploadData = new int[O_UP_MAX + 10];
// Upload translation buffer
private static final int XLATE_NBUF = 32;
@@ -111,6 +118,22 @@ public class FTCut {
this.listener = listener;
}
public long getBytesTransferred() {
return expandedLength;
}
public boolean isTransferActive() {
return xferInProgress || (listener != null && listener.getCurrentState() != FTState.NONE);
}
public int getRetryCount() {
return retransmitRetries;
}
public void resetRetries() {
this.retransmitRetries = 0;
}
/**
* Initialize CUT mode with active streams.
*/
@@ -145,6 +168,9 @@ public class FTCut {
xlateBufIx = 0;
cutEof = false;
lastCr = false;
retransmitRetries = 0;
lastUploadCount = 0;
lastUploadSeq = 0;
}
/**
@@ -158,6 +184,9 @@ public class FTCut {
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
if (input != null) {
input.reset();
}
}
/**
@@ -167,6 +196,9 @@ public class FTCut {
public void processScreenUpdate() {
if (listener.getCurrentState() == FTState.NONE) return;
// Check for host prompt messages (e.g. TSO IKJ56700 or CMS Ready;)
checkForHostPrompts();
// CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) {
@@ -200,6 +232,22 @@ public class FTCut {
}
}
private void checkForHostPrompts() {
if (screen == null) return;
int size = screen.getRows() * screen.getCols();
StringBuilder sb = new StringBuilder(size);
for (int i = 0; i < size; i++) {
int ec = screen.getCellEC(i);
if (ec != 0) {
sb.append(translator.ebcdicToUnicode(ec));
}
}
String screenContent = sb.toString();
if (screenContent.contains(PROMPT_TSO_IKJ)) {
log.info("CUT: Detected TSO IKJ prompt on screen");
}
}
private boolean isSkip(byte attr) {
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
}
@@ -308,8 +356,13 @@ public class FTCut {
int cs = 0;
for (int i = 0; i < count; i++) {
cs ^= screen.getCellEC(O_UP_DATA + i);
int cellVal = screen.getCellEC(O_UP_DATA + i);
cs ^= cellVal;
lastUploadData[i] = cellVal;
}
lastUploadCount = count;
lastUploadSeq = seqEbc;
screen.setCell(O_UP_CSUM, FTConstants.to6(cs & 0x3F, translator));
screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator));
screen.setCell(O_UP_LEN + 1, FTConstants.to6(count & 0x3F, translator));
@@ -364,6 +417,7 @@ public class FTCut {
expandedLength += converted.length;
listener.onBytesTransferred(expandedLength);
}
retransmitRetries = 0; // Reset retries on successful frame
cutAck();
} catch (IOException e) {
log.warning("CUT: Write error: " + e.getMessage());
@@ -374,8 +428,38 @@ public class FTCut {
// ========== Retransmit ==========
private void cutRetransmit() {
log.warning("CUT: RETRANSMIT (not supported, aborting)");
cutAbort("Retransmit not supported", SC_ABORT_XMIT);
retransmitRetries++;
log.warning("CUT: RETRANSMIT requested (attempt " + retransmitRetries + "/" + MAX_CUT_RETRIES + ")");
if (retransmitRetries > MAX_CUT_RETRIES) {
cutAbort("Too many retransmission attempts", SC_ABORT_XMIT);
return;
}
FTConfig config = listener.getConfig();
if (config != null && config.isSend() && lastUploadCount > 0) {
// Resend last uploaded frame
screen.setCell(O_UP_FRAME_SEQ, lastUploadSeq);
int cs = 0;
for (int i = 0; i < lastUploadCount; i++) {
screen.setCell(O_UP_DATA + i, lastUploadData[i]);
cs ^= lastUploadData[i];
}
screen.setCell(O_UP_CSUM, FTConstants.to6(cs & 0x3F, translator));
screen.setCell(O_UP_LEN, FTConstants.to6((lastUploadCount >> 6) & 0x3F, translator));
screen.setCell(O_UP_LEN + 1, FTConstants.to6(lastUploadCount & 0x3F, translator));
byte attr = screen.getCellFAByte(O_DR_SF);
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL | FA_MODIFY);
screen.setCellFA(O_DR_SF, attr);
log.fine("CUT: Retransmitting last upload frame (len=" + lastUploadCount + ")");
input.sendAidForFT(AID_ENTER);
} else {
// In download mode, send ACK_RETRANSMIT (PF1)
log.fine("CUT: Requesting frame retransmit from host via PF1");
input.sendAidForFT(ACK_RETRANSMIT);
}
}
// ========== Acknowledge ==========
@@ -42,6 +42,11 @@ public class FTDft {
private boolean dftEof = false;
private boolean messageFlag = false;
private long bytesTransferred = 0;
private long estimatedTotalBytes = 0;
private int hostLrecl = 0;
private String hostRecfm = "";
private int hostBlksize = 0;
private int customMtuSize = 0;
private int pendingByte = -1;
// Savebuf for Read Modified retransmit
@@ -60,6 +65,46 @@ public class FTDft {
this.listener = listener;
}
/**
* Set dynamic MTU size (256 - 32768).
*/
public void setMTUSize(int size) {
this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, size));
}
public int getMTUSize() {
if (customMtuSize > 0) return customMtuSize;
if (listener != null && listener.getConfig() != null) {
return listener.getConfig().getDftBufferSize();
}
return FTConstants.DFT_BUF;
}
public long getBytesTransferred() {
return bytesTransferred;
}
public long getEstimatedTotalBytes() {
return estimatedTotalBytes;
}
public int getHostLrecl() {
return hostLrecl;
}
public String getHostRecfm() {
return hostRecfm;
}
public int getHostBlksize() {
return hostBlksize;
}
public boolean isTransferActive() {
return listener != null && listener.getCurrentState() != FTState.NONE;
}
/**
* Initialize DFT mode with active streams.
*/
@@ -90,6 +135,10 @@ public class FTDft {
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
estimatedTotalBytes = 0;
hostLrecl = 0;
hostRecfm = "";
hostBlksize = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
@@ -109,6 +158,9 @@ public class FTDft {
dftSaveBuf = null;
dftSaveBufLen = 0;
resetState();
if (input != null) {
input.reset();
}
}
/**
@@ -165,7 +217,7 @@ public class FTDft {
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
log.fine("DFT: Open request");
// Parse open request payload matching x3270
// Parse open request payload matching x3270 / DDM open
// sfLength is the 2-byte length value at sfOffset
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
String nameBuf = "";
@@ -176,6 +228,9 @@ public class FTDft {
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
}
// Check for host DDM file attributes embedded in open request
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
if (isMessageStream(nameBuf)) {
messageFlag = true;
log.info("DFT: Open request for message stream (" + nameBuf + ")");
@@ -191,6 +246,33 @@ public class FTDft {
dftOpenAck();
}
private void parseDdmAttributes(byte[] data, int start, int length) {
int end = Math.min(start + length, data.length);
int pos = start + 2; // skip TR_OPEN_REQ
while (pos + 3 <= end) {
int attrType = data[pos] & 0xFF;
int attrLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
if (attrLen < 3 || pos + attrLen > end) {
pos++;
continue;
}
if (attrType == DDM_HDR_LRECL && attrLen >= 5) {
hostLrecl = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
} else if (attrType == DDM_HDR_RECFM && attrLen >= 4) {
int r = data[pos + 3] & 0xFF;
hostRecfm = (r == 1) ? "F" : (r == 2 ? "V" : "U");
} else if (attrType == DDM_HDR_BLKSIZE && attrLen >= 5) {
hostBlksize = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
} else if (attrType == DDM_HDR_FILESIZE && attrLen >= 7) {
estimatedTotalBytes = (((long)(data[pos + 3] & 0xFF)) << 24) |
(((long)(data[pos + 4] & 0xFF)) << 16) |
(((long)(data[pos + 5] & 0xFF)) << 8) |
((long)(data[pos + 6] & 0xFF));
}
pos += attrLen;
}
}
private boolean isMessageStream(String name) {
if (name == null) return false;
String u = name.toUpperCase();
@@ -358,8 +440,8 @@ public class FTDft {
for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF;
if (config.isCrFlag() && (b == '\r' || b == 0x1A)) {
continue; // Strip CR and EOF ^Z
if (config.isCrFlag() && (b == '\r' || b == EOF_CTRL_Z || b == EOF_CTRL_D)) {
continue; // Strip CR and EOF (^Z / ^D)
}
if (!config.isRemapFlag()) {
@@ -610,4 +692,12 @@ public class FTDft {
}
return false;
}
/**
* Explicitly resend the inbound data buffer to the host.
* @return true if retransmitted, false otherwise.
*/
public boolean resendInboundDataBufferToHost() {
return readModified();
}
}
@@ -0,0 +1,129 @@
package haus.nightmare.lib3270j.ft.dir;
/**
* Representation of a z/VM / CMS FILELIST or LISTFILE directory entry.
*/
public class CMSDirectoryEntry extends HostDirectoryEntry {
private static final long serialVersionUID = 1L;
private String filename;
private String filetype;
private String filemode;
private long numRecords;
private long numBlocks;
private String date;
private String time;
public CMSDirectoryEntry() {
super();
}
public CMSDirectoryEntry(String filename, String filetype, String filemode) {
super((filename != null ? filename.trim() : "") + " " +
(filetype != null ? filetype.trim() : "") + " " +
(filemode != null ? filemode.trim() : "A"));
this.filename = filename;
this.filetype = filetype;
this.filemode = filemode;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
updateDatasetName();
}
public String getFiletype() {
return filetype;
}
public void setFiletype(String filetype) {
this.filetype = filetype;
updateDatasetName();
}
public String getFilemode() {
return filemode;
}
public void setFilemode(String filemode) {
this.filemode = filemode;
updateDatasetName();
}
private void updateDatasetName() {
this.name = (filename != null ? filename.trim() : "") + " " +
(filetype != null ? filetype.trim() : "") + " " +
(filemode != null ? filemode.trim() : "A");
}
public long getNumRecords() {
return numRecords;
}
public void setNumRecords(long numRecords) {
this.numRecords = numRecords;
}
public long getNumBlocks() {
return numBlocks;
}
public void setNumBlocks(long numBlocks) {
this.numBlocks = numBlocks;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
@Override
public String getFormattedSize() {
if (numRecords > 0) {
return numRecords + " recs";
} else if (numBlocks > 0) {
return numBlocks + " blks";
}
return "-";
}
@Override
public String getLastModified() {
if (date != null && time != null) {
return date + " " + time;
} else if (date != null) {
return date;
}
return "-";
}
@Override
public String formatListing() {
return String.format("%-8s %-8s %-2s %-4s %5d %7d %6d %-10s %-8s",
filename != null ? filename : "",
filetype != null ? filetype : "",
filemode != null ? filemode : "",
recfm != null ? recfm : "",
lrecl,
numRecords,
numBlocks,
date != null ? date : "",
time != null ? time : "");
}
}
@@ -0,0 +1,100 @@
package haus.nightmare.lib3270j.ft.dir;
import java.util.ArrayList;
import java.util.List;
/**
* Parser for z/VM CMS file listings (FILELIST, LISTFILE, EXECIO).
*/
public class CMSDirectoryParser {
/**
* Parse raw text containing CMS file listings into a structured list of CMSDirectoryEntry.
*/
public static List<CMSDirectoryEntry> parse(String text) {
List<CMSDirectoryEntry> entries = new ArrayList<>();
if (text == null || text.trim().isEmpty()) {
return entries;
}
String[] lines = text.split("\r?\n");
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.isEmpty()) continue;
if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**")) continue;
String upper = trimmed.toUpperCase();
if (upper.startsWith("FILENAME") || upper.startsWith("DIRECTORY") || upper.startsWith("FILELIST")) continue;
CMSDirectoryEntry entry = parseCmsLine(trimmed);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
private static CMSDirectoryEntry parseCmsLine(String line) {
String[] tokens = line.split("\\s+");
if (tokens.length < 2) return null;
int startIdx = 0;
// Check for EXEC prefix (e.g. "&1 &2")
if (tokens[0].startsWith("&") || tokens[0].equalsIgnoreCase("EXEC")) {
while (startIdx < tokens.length && (tokens[startIdx].startsWith("&") || tokens[startIdx].equalsIgnoreCase("EXEC"))) {
startIdx++;
}
}
if (startIdx + 1 >= tokens.length) return null;
String fn = tokens[startIdx];
String ft = tokens[startIdx + 1];
// CMS filenames and filetypes are 1-8 chars alphanumeric
if (fn.length() > 8 || ft.length() > 8) return null;
String fm = (startIdx + 2 < tokens.length && tokens[startIdx + 2].length() <= 2) ? tokens[startIdx + 2] : "A1";
CMSDirectoryEntry entry = new CMSDirectoryEntry(fn, ft, fm);
int cur = startIdx + 3;
// Format (F or V)
if (cur < tokens.length && tokens[cur].matches("^(F|V|U)$")) {
entry.setRecfm(tokens[cur]);
cur++;
}
// LRECL
if (cur < tokens.length && tokens[cur].matches("^\\d+$")) {
try { entry.setLrecl(Integer.parseInt(tokens[cur])); } catch (NumberFormatException ignored) {}
cur++;
}
// RECS
if (cur < tokens.length && tokens[cur].matches("^\\d+$")) {
try { entry.setNumRecords(Long.parseLong(tokens[cur])); } catch (NumberFormatException ignored) {}
cur++;
}
// BLOCKS
if (cur < tokens.length && tokens[cur].matches("^\\d+$")) {
try { entry.setNumBlocks(Long.parseLong(tokens[cur])); } catch (NumberFormatException ignored) {}
cur++;
}
// DATE
if (cur < tokens.length && (tokens[cur].contains("-") || tokens[cur].contains("/"))) {
entry.setDate(tokens[cur]);
cur++;
}
// TIME
if (cur < tokens.length && tokens[cur].contains(":")) {
entry.setTime(tokens[cur]);
}
return entry;
}
}
@@ -0,0 +1,21 @@
package haus.nightmare.lib3270j.ft.dir;
import java.util.List;
/**
* Callback interface for asynchronous host directory listing requests.
*/
public interface FileTransferHostDirectoryInterface {
/**
* Called when host directory entries have been successfully retrieved and parsed.
* @param entries List of parsed directory entries
*/
void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries);
/**
* Called when an error occurs during directory query or parsing.
* @param errorMessage Description of the error
*/
void onDirectoryError(String errorMessage);
}
@@ -0,0 +1,72 @@
package haus.nightmare.lib3270j.ft.dir;
import java.io.Serializable;
/**
* Base abstract representation of a file or dataset entry on a mainframe host.
*/
public abstract class HostDirectoryEntry implements Serializable {
private static final long serialVersionUID = 1L;
protected String name;
protected String recfm;
protected int lrecl;
protected int blksize;
public HostDirectoryEntry() {}
public HostDirectoryEntry(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDatasetName() {
return name;
}
public String getRecfm() {
return recfm;
}
public void setRecfm(String recfm) {
this.recfm = recfm;
}
public int getLrecl() {
return lrecl;
}
public void setLrecl(int lrecl) {
this.lrecl = lrecl;
}
public int getBlksize() {
return blksize;
}
public void setBlksize(int blksize) {
this.blksize = blksize;
}
public abstract String getFormattedSize();
public abstract String getLastModified();
/**
* Return a standardized formatted listing line suitable for display.
*/
public abstract String formatListing();
@Override
public String toString() {
return formatListing();
}
}
@@ -0,0 +1,125 @@
package haus.nightmare.lib3270j.ft.dir;
/**
* Representation of a z/OS / TSO dataset catalog or ISPF dataset list entry.
*/
public class TSODirectoryEntry extends HostDirectoryEntry {
private static final long serialVersionUID = 1L;
private String volume;
private String dsorg;
private int tracksAllocated;
private int tracksUsed;
private int percentUsed;
private int extents;
private String device;
private String creationDate;
private String referencedDate;
public TSODirectoryEntry() {
super();
}
public TSODirectoryEntry(String dsname) {
super(dsname);
}
public String getVolume() {
return volume;
}
public void setVolume(String volume) {
this.volume = volume;
}
public String getDsorg() {
return dsorg;
}
public void setDsorg(String dsorg) {
this.dsorg = dsorg;
}
public int getTracksAllocated() {
return tracksAllocated;
}
public void setTracksAllocated(int tracksAllocated) {
this.tracksAllocated = tracksAllocated;
}
public int getTracksUsed() {
return tracksUsed;
}
public void setTracksUsed(int tracksUsed) {
this.tracksUsed = tracksUsed;
}
public int getPercentUsed() {
return percentUsed;
}
public void setPercentUsed(int percentUsed) {
this.percentUsed = percentUsed;
}
public int getExtents() {
return extents;
}
public void setExtents(int extents) {
this.extents = extents;
}
public String getDevice() {
return device;
}
public void setDevice(String device) {
this.device = device;
}
public String getCreationDate() {
return creationDate;
}
public void setCreationDate(String creationDate) {
this.creationDate = creationDate;
}
public String getReferencedDate() {
return referencedDate;
}
public void setReferencedDate(String referencedDate) {
this.referencedDate = referencedDate;
}
@Override
public String getFormattedSize() {
if (tracksAllocated > 0) {
return tracksUsed > 0 ? (tracksUsed + "/" + tracksAllocated + " TRK") : (tracksAllocated + " TRK");
}
return "-";
}
@Override
public String getLastModified() {
return referencedDate != null && !referencedDate.isEmpty() ? referencedDate : creationDate;
}
@Override
public String formatListing() {
return String.format("%-44s %-6s %-4s %-4s %5d %5d %4d %-10s",
name != null ? name : "",
volume != null ? volume : "",
dsorg != null ? dsorg : "",
recfm != null ? recfm : "",
lrecl,
blksize,
tracksAllocated,
referencedDate != null ? referencedDate : "");
}
}
@@ -0,0 +1,158 @@
package haus.nightmare.lib3270j.ft.dir;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parser for z/OS TSO dataset listings (ISPF DSLIST, LISTCAT, LISTDS).
*/
public class TSODirectoryParser {
private static final Pattern DSN_PATTERN = Pattern.compile("([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)+)");
private static final Pattern NONVSAM_PATTERN = Pattern.compile("NONVSAM\\s+-+\\s+([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)+)", Pattern.CASE_INSENSITIVE);
private static final Pattern VOLSER_PATTERN = Pattern.compile("VOLSER-+([A-Z0-9]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern RECFM_PATTERN = Pattern.compile("RECFM-+([A-Z]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern LRECL_PATTERN = Pattern.compile("LRECL-+([0-9]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern BLKSIZE_PATTERN = Pattern.compile("BLKSIZE-+([0-9]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern DSORG_PATTERN = Pattern.compile("DSORG-+([A-Z]+)", Pattern.CASE_INSENSITIVE);
/**
* Parse raw text containing TSO dataset listings into a structured list of TSODirectoryEntry.
*/
public static List<TSODirectoryEntry> parse(String text) {
List<TSODirectoryEntry> entries = new ArrayList<>();
if (text == null || text.trim().isEmpty()) {
return entries;
}
String[] lines = text.split("\r?\n");
// Check if text is LISTCAT format
if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT")) {
parseListcat(lines, entries);
if (!entries.isEmpty()) return entries;
}
// Check for ISPF DSLIST or general tabular format
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.isEmpty()) continue;
if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**")) continue;
if (trimmed.toUpperCase().startsWith("COMMAND") || trimmed.toUpperCase().startsWith("DSLIST") || trimmed.toUpperCase().startsWith("DATA SETS")) continue;
TSODirectoryEntry entry = parseTabularLine(trimmed);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
private static void parseListcat(String[] lines, List<TSODirectoryEntry> entries) {
TSODirectoryEntry current = null;
for (String line : lines) {
String upper = line.toUpperCase();
Matcher nonvsamMat = NONVSAM_PATTERN.matcher(upper);
if (nonvsamMat.find()) {
if (current != null && current.getName() != null) {
entries.add(current);
}
current = new TSODirectoryEntry(nonvsamMat.group(1));
continue;
}
if (current != null) {
Matcher volMat = VOLSER_PATTERN.matcher(upper);
if (volMat.find()) current.setVolume(volMat.group(1));
Matcher recMat = RECFM_PATTERN.matcher(upper);
if (recMat.find()) current.setRecfm(recMat.group(1));
Matcher lreclMat = LRECL_PATTERN.matcher(upper);
if (lreclMat.find()) {
try { current.setLrecl(Integer.parseInt(lreclMat.group(1))); } catch (NumberFormatException ignored) {}
}
Matcher blkMat = BLKSIZE_PATTERN.matcher(upper);
if (blkMat.find()) {
try { current.setBlksize(Integer.parseInt(blkMat.group(1))); } catch (NumberFormatException ignored) {}
}
Matcher dsorgMat = DSORG_PATTERN.matcher(upper);
if (dsorgMat.find()) current.setDsorg(dsorgMat.group(1));
}
}
if (current != null && current.getName() != null) {
entries.add(current);
}
}
private static TSODirectoryEntry parseTabularLine(String line) {
String[] tokens = line.split("\\s+");
if (tokens.length == 0) return null;
// Find which token is the dataset name
int dsnIdx = -1;
for (int i = 0; i < tokens.length; i++) {
String tok = tokens[i];
if (DSN_PATTERN.matcher(tok.toUpperCase()).matches()) {
dsnIdx = i;
break;
}
}
if (dsnIdx < 0) return null;
TSODirectoryEntry entry = new TSODirectoryEntry(tokens[dsnIdx].toUpperCase());
boolean seenFormat = false;
// Parse remaining tokens
for (int i = 0; i < tokens.length; i++) {
if (i == dsnIdx) continue;
String tok = tokens[i].toUpperCase();
if (tok.matches("^(PS|PO|PO-E|VSAM|DA|IS)$")) {
entry.setDsorg(tok);
seenFormat = true;
} else if (tok.matches("^(F|FB|V|VB|U|VBS|FBS)$")) {
entry.setRecfm(tok);
seenFormat = true;
} else if (tok.matches("^(3390|3380|TAPE|VIO)$")) {
entry.setDevice(tok);
} else if (tok.matches("^\\d{4}/\\d{2}/\\d{2}$") || tok.matches("^\\d{2}/\\d{2}/\\d{2}$") ||
tok.matches("^\\d{4}-\\d{2}-\\d{2}$")) {
if (entry.getCreationDate() == null) {
entry.setCreationDate(tok);
} else {
entry.setReferencedDate(tok);
}
} else if (tok.matches("^[A-Z0-9]{6}$") && entry.getVolume() == null && !tok.matches("^\\d+$")) {
entry.setVolume(tok);
} else if (tok.matches("^\\d+$")) {
int val = Integer.parseInt(tok);
if (!seenFormat) {
if (entry.getTracksAllocated() == 0) {
entry.setTracksAllocated(val);
} else if (entry.getPercentUsed() == 0 && val <= 100) {
entry.setPercentUsed(val);
} else if (entry.getExtents() == 0 && val <= 128) {
entry.setExtents(val);
}
} else {
if (entry.getLrecl() == 0 && val <= 32760) {
entry.setLrecl(val);
} else if (entry.getBlksize() == 0 && val <= 32760) {
entry.setBlksize(val);
} else if (entry.getTracksAllocated() == 0) {
entry.setTracksAllocated(val);
}
}
}
}
return entry;
}
}
@@ -0,0 +1,224 @@
package haus.nightmare.lib3270j.graphics;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Encapsulates edge table representation and scanline rasterization for GOCA filled areas.
* Matches IBM Host On-Demand (HODArea.java, FillArea.java) area processing.
*
* <p>Supports even-odd multi-polygon subpath rasterization, all 17 standard IBM GOCA fill patterns (0-16),
* custom Programmed Symbol pattern sets (LCID &gt;= 0x40), and background mix modes (BMX_LEAVE / BMX_OVER).
*/
public class FillArea {
/**
* Internal representation of a directed polygon edge for scanline intersection.
*/
public static class Edge {
public final double x1, y1;
public final double x2, y2;
public Edge(double x1, double y1, double x2, double y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
}
private final List<Edge> edges = new ArrayList<>();
private final List<double[]> subpathsX = new ArrayList<>();
private final List<double[]> subpathsY = new ArrayList<>();
public FillArea() {}
/**
* Adds a single directed edge to the edge table.
*/
public synchronized void addEdge(double x1, double y1, double x2, double y2) {
if (Math.abs(y1 - y2) > 1e-6) {
edges.add(new Edge(x1, y1, x2, y2));
}
}
/**
* Adds an integer directed edge to the edge table.
*/
public synchronized void addEdge(int x1, int y1, int x2, int y2) {
addEdge((double) x1, (double) y1, (double) x2, (double) y2);
}
/**
* Adds a complete closed or open polygon subpath.
*/
public synchronized void addPolygon(double[] px, double[] py, int numPoints) {
if (px == null || py == null || numPoints < 2) return;
int n = Math.min(numPoints, Math.min(px.length, py.length));
double[] sx = new double[n];
double[] sy = new double[n];
System.arraycopy(px, 0, sx, 0, n);
System.arraycopy(py, 0, sy, 0, n);
subpathsX.add(sx);
subpathsY.add(sy);
for (int i = 0; i < n - 1; i++) {
addEdge(px[i], py[i], px[i + 1], py[i + 1]);
}
if (n >= 3 && (Math.abs(px[0] - px[n - 1]) > 1e-6 || Math.abs(py[0] - py[n - 1]) > 1e-6)) {
addEdge(px[n - 1], py[n - 1], px[0], py[0]);
}
}
/**
* Adds an integer polygon subpath.
*/
public synchronized void addPolygon(int[] px, int[] py, int numPoints) {
if (px == null || py == null || numPoints < 2) return;
int n = Math.min(numPoints, Math.min(px.length, py.length));
double[] dpx = new double[n];
double[] dpy = new double[n];
for (int i = 0; i < n; i++) {
dpx[i] = px[i];
dpy[i] = py[i];
}
addPolygon(dpx, dpy, n);
}
public synchronized boolean isEmpty() {
return edges.isEmpty() && subpathsX.isEmpty();
}
public synchronized int getEdgeCount() {
return edges.size();
}
public synchronized int getSubpathCount() {
return subpathsX.size();
}
public synchronized void clear() {
edges.clear();
subpathsX.clear();
subpathsY.clear();
}
/**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
if (plane == null) return;
if (edges.isEmpty() && subpathsX.isEmpty()) return;
int canvasW = plane.getCanvasWidth();
int canvasH = plane.getCanvasHeight();
if (canvasW <= 0 || canvasH <= 0) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb;
// Background mix / transparency rule for Black fills:
// BMX_LEAVE / 0 or 2 / MIX_DEFAULT: Transparent black
// BMX_OVER / 1 or 5: Opaque background overpaint
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
double minY = Double.MAX_VALUE;
double maxY = Double.MIN_VALUE;
for (Edge e : edges) {
if (e.y1 < minY) minY = e.y1;
if (e.y2 < minY) minY = e.y2;
if (e.y1 > maxY) maxY = e.y1;
if (e.y2 > maxY) maxY = e.y2;
}
int iMinY = Math.max(0, (int) Math.floor(minY));
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
List<Double> nodeX = new ArrayList<>();
byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && psm != null) {
psSlot = psm.getSymbol(patternSet, pattern);
}
if (psSlot == null) {
if (pattern >= 0 && pattern < GraphicsPlane.PATTERN_DATA.length) {
patRows = GraphicsPlane.PATTERN_DATA[pattern];
} else {
patRows = GraphicsPlane.PATTERN_DATA[0];
}
}
for (int y = iMinY; y <= iMaxY; y++) {
nodeX.clear();
double scanY = y + 0.5;
for (Edge e : edges) {
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
nodeX.add(x);
}
}
Collections.sort(nodeX);
for (int i = 0; i < nodeX.size(); i += 2) {
if (i + 1 >= nodeX.size()) break;
int leftX = Math.max(0, (int) Math.round(nodeX.get(i)));
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1)));
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
}
}
}
}
}
// Draw boundary outlines if enabled
if (drawBoundary && boundaryColorArgb != 0) {
for (int s = 0; s < subpathsX.size(); s++) {
double[] px = subpathsX.get(s);
double[] py = subpathsY.get(s);
int pLen = px.length;
if (pLen >= 2) {
for (int i = 0; i < pLen - 1; i++) {
plane.drawLine(px[i], py[i], px[i + 1], py[i + 1],
boundaryColorArgb, lineType, lineWidth);
}
if (pLen >= 3 && (Math.abs(px[0] - px[pLen - 1]) > 1e-6 || Math.abs(py[0] - py[pLen - 1]) > 1e-6)) {
plane.drawLine(px[pLen - 1], py[pLen - 1], px[0], py[0],
boundaryColorArgb, lineType, lineWidth);
}
}
}
}
}
}
@@ -0,0 +1,109 @@
package haus.nightmare.lib3270j.graphics;
import java.util.ArrayList;
import java.util.List;
/**
* Calculates intermediate points for IBM GOCA rational quadratic spline fillets.
* Matches IBM Host On-Demand (HODFillet.java, FilletPts.java) curve interpolation.
*
* <p>A GOCA fillet curve passes from the initial point P_0 to the terminal point P_{N-1},
* bending tangentially toward each intermediate control point P_i.
*/
public class FilletPts {
public static final int DEFAULT_STEPS_PER_SEGMENT = 24;
/**
* Calculates interpolated spline vertices across control points using double precision.
*
* @param px Array of X control coordinates
* @param py Array of Y control coordinates
* @param numPoints Number of control points (starting at index 0)
* @param stepsPerSegment Number of interpolation steps per segment
* @return 2D array of coordinates: result[0] = x coordinates, result[1] = y coordinates
*/
public static double[][] calculate(double[] px, double[] py, int numPoints, int stepsPerSegment) {
if (px == null || py == null || numPoints <= 0) {
return new double[][] { new double[0], new double[0] };
}
int n = Math.min(numPoints, Math.min(px.length, py.length));
if (n == 1) {
return new double[][] { new double[] { px[0] }, new double[] { py[0] } };
}
if (n == 2) {
return new double[][] {
new double[] { px[0], px[1] },
new double[] { py[0], py[1] }
};
}
int steps = Math.max(4, stepsPerSegment > 0 ? stepsPerSegment : DEFAULT_STEPS_PER_SEGMENT);
List<Double> outX = new ArrayList<>();
List<Double> outY = new ArrayList<>();
outX.add(px[0]);
outY.add(py[0]);
for (int i = 0; i < n - 1; i++) {
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0;
double p1x = px[i];
double p1y = py[i];
double p2x = (i == n - 2) ? px[n - 1] : (px[i] + px[i + 1]) / 2.0;
double p2y = (i == n - 2) ? py[n - 1] : (py[i] + py[i + 1]) / 2.0;
for (int s = 1; s <= steps; s++) {
double t = (double) s / (double) steps;
double oneMinusT = 1.0 - t;
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
outX.add(bx);
outY.add(by);
}
}
int total = outX.size();
double[] resX = new double[total];
double[] resY = new double[total];
for (int i = 0; i < total; i++) {
resX[i] = outX.get(i);
resY[i] = outY.get(i);
}
return new double[][] { resX, resY };
}
/**
* Calculates interpolated spline vertices across control points using integer coordinates.
*
* @param px Array of X control coordinates
* @param py Array of Y control coordinates
* @param numPoints Number of control points
* @param stepsPerSegment Number of interpolation steps per segment
* @return 2D array of coordinates: result[0] = x coordinates, result[1] = y coordinates
*/
public static int[][] calculate(int[] px, int[] py, int numPoints, int stepsPerSegment) {
if (px == null || py == null || numPoints <= 0) {
return new int[][] { new int[0], new int[0] };
}
int n = Math.min(numPoints, Math.min(px.length, py.length));
double[] dpx = new double[n];
double[] dpy = new double[n];
for (int i = 0; i < n; i++) {
dpx[i] = px[i];
dpy[i] = py[i];
}
double[][] res = calculate(dpx, dpy, n, stepsPerSegment);
int total = res[0].length;
int[] rx = new int[total];
int[] ry = new int[total];
for (int i = 0; i < total; i++) {
rx[i] = (int) Math.round(res[0][i]);
ry[i] = (int) Math.round(res[1][i]);
}
return new int[][] { rx, ry };
}
}
@@ -125,6 +125,14 @@ public class GddmCoordinateTransform {
return defaultCharHeight;
}
/**
* Converts a GOCA signed coordinate (gx, gy) to base presentation space coordinate (px, py).
* Matches IBM Host On-Demand (HODTransform.calculate(int, int)).
*/
public Point calculate(int gx, int gy) {
return gocaToBase(gx, gy);
}
/**
* Converts a GOCA signed coordinate (gx, gy) to base presentation space coordinate (px, py).
*/
@@ -345,6 +345,76 @@ public class GocaDecoder {
}
}
/**
* Decodes a stream of GOCA drawing orders (matching IBM Host On-Demand HODDecoder.decodeGOCA).
*/
public synchronized void decodeGoca(byte[] data, int offset, int length) {
decodeStream(data, offset, length);
}
/**
* Decodes a character stream of GOCA drawing orders.
*/
public synchronized void decodeGoca(char[] data, int offset, int length) {
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) return;
byte[] b = new byte[length];
for (int i = 0; i < length; i++) {
b[i] = (byte) (data[offset + i] & 0xFF);
}
decodeGoca(b, 0, length);
}
/**
* Processes a discrete GOCA segment.
*/
public synchronized void processSegment(byte[] data, int offset, int length) {
decodeStream(data, offset, length);
}
/**
* Processes a GOCA segment with char data and start/end offset array.
*/
public synchronized void processSegment(char[] data, int[] offsets) {
if (data == null || offsets == null || offsets.length < 2) return;
int off = offsets[0];
int len = offsets[1] - offsets[0];
decodeGoca(data, off, len);
}
/**
* Executes a stored procedure segment by segment ID, traversing chained next segments.
*/
public synchronized void procedureSegment(int segId) {
if (segId == 0) return;
byte[] segData = segmentStore.get(segId);
if (segData != null) {
int savedSeg = currentSegId;
currentSegId = segId;
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb);
callDepth++;
decodeStreamDirect(segData, 0, segData.length);
callDepth--;
currentSegId = savedSeg;
// Execute chained segments
Integer nextId = segmentChainMap.get(segId);
if (nextId != null && nextId != 0 && callDepth < 16) {
procedureSegment(nextId);
}
} else {
logger.warning("procedureSegment: Segment not found in store: " + segId);
}
}
/**
* Executes a procedure segment from raw data buffer.
*/
public synchronized void procedureSegment(byte[] procData, int offset, int length) {
decodeStream(procData, offset, length);
}
/**
* Decodes a stream of GOCA drawing orders.
*/
@@ -433,6 +503,7 @@ public class GocaDecoder {
}
case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
logger.info(String.format("GOCA ENDSEGM: segId=%d", currentSegId));
int finishedSegId = currentSegId;
if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) {
@@ -444,6 +515,23 @@ public class GocaDecoder {
}
currentSegId = 0;
idx += orderLen;
if (finishedSegId != 0 && callDepth < 16) {
Integer nextId = segmentChainMap.get(finishedSegId);
if (nextId != null && nextId != 0) {
byte[] nextSeg = segmentStore.get(nextId);
if (nextSeg != null) {
logger.info("Executing chained segment nextId=" + nextId);
currentSegId = nextId;
SegmentBounds targetSb = segmentBoundsMap.computeIfAbsent(nextId, SegmentBounds::new);
activeSegmentsInOrder.remove(targetSb);
activeSegmentsInOrder.add(targetSb);
callDepth++;
decodeStreamDirect(nextSeg, 0, nextSeg.length);
callDepth--;
currentSegId = 0;
}
}
}
break;
}
case GocaConstants.G_GSETAG: { // Set Pick Identifier / Tag (0x39)
@@ -1187,17 +1275,17 @@ public class GocaDecoder {
List<Double> ptsX = new ArrayList<>();
List<Double> ptsY = new ArrayList<>();
List<Integer> gocaPtsX = new ArrayList<>();
List<Integer> gocaPtsY = new ArrayList<>();
if (fromCurPos) {
trackPoint(curX, curY);
ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY));
if (inArea) {
if (currentPolyPts == 0) {
addAreaLineStart(curX, curY);
} else {
addAreaPoint(curX, curY);
}
gocaPtsX.add(curX);
gocaPtsY.add(curY);
if (inArea && currentPolyPts == 0) {
addAreaLineStart(curX, curY);
}
}
@@ -1207,12 +1295,10 @@ public class GocaDecoder {
trackPoint(x, y);
ptsX.add(plane.mapXDouble(x));
ptsY.add(plane.mapYDouble(y));
if (inArea) {
if (ptsX.size() == 1 && currentPolyPts == 0) {
addAreaLineStart(x, y);
} else {
addAreaPoint(x, y);
}
gocaPtsX.add(x);
gocaPtsY.add(y);
if (inArea && ptsX.size() == 1 && currentPolyPts == 0) {
addAreaLineStart(x, y);
}
curX = x;
curY = y;
@@ -1227,7 +1313,22 @@ public class GocaDecoder {
px[i] = ptsX.get(i);
py[i] = ptsY.get(i);
}
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
if (inArea) {
int gn = gocaPtsX.size();
int[] gx = new int[gn];
int[] gy = new int[gn];
for (int i = 0; i < gn; i++) {
gx[i] = gocaPtsX.get(i);
gy[i] = gocaPtsY.get(i);
}
int[][] curve = FilletPts.calculate(gx, gy, gn, 16);
for (int i = 1; i < curve[0].length; i++) {
addAreaPoint(curve[0][i], curve[1][i]);
}
} else {
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
}
}
}
@@ -1330,6 +1431,87 @@ public class GocaDecoder {
curY = startY;
}
/**
* Draws a transformed character string (matching HODDecoder.drawGCS).
*/
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) {
if (plane != null && text != null && !text.isEmpty()) {
if (charPrecision == GocaConstants.CP_STROKE) {
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle);
} else {
plane.drawText(x, y, text, color, cw, ch, dir, angle);
}
}
}
/**
* Draws an EBCDIC byte buffer as a transformed character string.
*/
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) {
if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
char[] chars = new char[length];
for (int i = 0; i < length; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]);
}
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle);
}
/**
* Draws a single stroked vector symbol character.
*/
public synchronized void drawHodVss(int charCode, double x, double y, double cw, double ch, int color) {
if (plane != null) {
plane.drawHodVss(charCode, x, y, cw, ch, color);
}
}
/**
* Builds and returns the Vector Symbol Set glyph index.
*/
public static int[] buildVssIndex() {
return VectorSymbolData.buildVssIndex();
}
/**
* Sets the graphics cursor shape (crosshair, box, etc.).
*/
public synchronized void setHodCursorShape(int shape) {
if (plane != null) {
plane.setHodCursorShape(shape);
}
}
/**
* Attaches the graphics cursor at specific coordinates.
*/
public synchronized void attachGraphicCursor(int x, int y) {
setGraphicsCursorActive(true);
setGraphicCursorPosition(x, y);
if (plane != null) {
plane.attachGraphicCursor(x, y);
}
}
/**
* Detaches the graphics cursor.
*/
public synchronized void detachGraphicCursor() {
setGraphicsCursorActive(false);
if (plane != null) {
plane.detachGraphicCursor();
}
}
/**
* Erases the graphics presentation space and resets drawing attributes.
*/
public synchronized void eraseGraphicsPlane() {
if (plane != null) {
plane.eraseGraphicsPlane();
}
resetDefaults();
}
private int readCoord(byte[] data, int off) {
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
}
@@ -46,6 +46,13 @@ public class GraphicsPlane {
private long updateCount = 0;
private ProgramSymbolManager programSymbolManager;
private int currentPattern = GocaConstants.PT_SOLID;
private int currentPatternSet = 0;
private boolean graphicCursorAttached = false;
private int graphicCursorX = 0;
private int graphicCursorY = 0;
private int hodCursorShape = 0;
public void setProgramSymbolManager(ProgramSymbolManager psm) {
this.programSymbolManager = psm;
}
@@ -54,6 +61,65 @@ public class GraphicsPlane {
return programSymbolManager;
}
public synchronized void setPattern(int pattern) {
if (pattern >= 0 && pattern < PATTERN_DATA.length) {
this.currentPattern = pattern;
} else {
this.currentPattern = GocaConstants.PT_SOLID;
}
}
public synchronized int getPattern() {
return currentPattern;
}
public synchronized void setPatternSet(int patternSet) {
this.currentPatternSet = patternSet;
}
public synchronized int getPatternSet() {
return currentPatternSet;
}
public synchronized void eraseGraphicsPlane() {
clear();
}
public synchronized void attachGraphicCursor(int x, int y) {
this.graphicCursorAttached = true;
this.graphicCursorX = x;
this.graphicCursorY = y;
}
public synchronized void detachGraphicCursor() {
this.graphicCursorAttached = false;
}
public synchronized boolean isGraphicCursorAttached() {
return graphicCursorAttached;
}
public synchronized int getGraphicCursorX() {
return graphicCursorX;
}
public synchronized int getGraphicCursorY() {
return graphicCursorY;
}
public synchronized void setGraphicCursorPosition(int x, int y) {
this.graphicCursorX = x;
this.graphicCursorY = y;
}
public synchronized void setHodCursorShape(int shape) {
this.hodCursorShape = shape;
}
public synchronized int getHodCursorShape() {
return hodCursorShape;
}
public GraphicsPlane(int width, int height) {
this.canvasWidth = Math.max(1, width);
this.canvasHeight = Math.max(1, height);
@@ -518,27 +584,12 @@ public class GraphicsPlane {
return;
}
double prevX = px[0];
double prevY = py[0];
double[][] spline = FilletPts.calculate(px, py, numPoints, 30);
double[] sx = spline[0];
double[] sy = spline[1];
for (int i = 0; i < numPoints - 1; i++) {
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0;
double p1x = px[i];
double p1y = py[i];
double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0;
double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0;
int steps = 30;
for (int s = 1; s <= steps; s++) {
double t = (double) s / steps;
double oneMinusT = 1.0 - t;
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
drawLine(prevX, prevY, bx, by, colorArgb, lineType, lineWidth);
prevX = bx;
prevY = by;
}
for (int i = 0; i < sx.length - 1; i++) {
drawLine(sx[i], sy[i], sx[i + 1], sy[i + 1], colorArgb, lineType, lineWidth);
}
hasContent = true;
updateCount++;
@@ -555,6 +606,13 @@ public class GraphicsPlane {
drawFillet(dpx, dpy, numPoints, colorArgb, lineType, lineWidth);
}
/**
* Draws a single stroked vector character from the IBM Vector Symbol Set (VSS).
*/
public synchronized void drawHodVss(int charCode, double x, double y, double cw, double ch, int color) {
drawVssChar(x, y, (char) charCode, color, cw, ch);
}
/**
* Fills a closed polygon area with a solid color or hatching pattern.
*/
@@ -781,22 +839,7 @@ public class GraphicsPlane {
drawMarker((double) x, (double) y, markerType, size, colorArgb);
}
private static final int[] VSS_OFFSETS = new int[256];
static {
Arrays.fill(VSS_OFFSETS, -1);
int sym = VectorSymbolData.VSS_SYMBOL_START; // 33
if (sym < 256) {
VSS_OFFSETS[sym] = 0;
}
for (int i = 0; i < VectorSymbolData.vss_data.length; i++) {
if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF
sym++;
if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) {
VSS_OFFSETS[sym] = i + 1;
}
}
}
}
private static final int[] VSS_OFFSETS = VectorSymbolData.buildVssIndex();
@FunctionalInterface
public interface TextRenderer {
@@ -45,6 +45,11 @@ public class ProgramSymbolManager {
}
}
public ProgramSymbolManager(int defaultWidth, int defaultHeight) {
this();
setDefaultCellDimensions(defaultWidth, defaultHeight);
}
/**
* Resets all symbol sets.
*/
@@ -58,6 +63,37 @@ public class ProgramSymbolManager {
}
}
/**
* Clears a specific symbol set by LCID.
*/
public synchronized void clearSymbolSet(int lcid) {
if (lcid <= 0 || lcid >= 256) return;
lcidMap[lcid] = null;
for (ProgramSymbolSet s : sets) {
if (s != null && s.getLcid() == lcid) {
s.clear();
s.setLcid(0);
}
}
for (ProgramSymbolSet s : stagingSets) {
if (s != null && s.getLcid() == lcid) {
s.clear();
s.setLcid(0);
}
}
}
/**
* Clears an individual symbol glyph within a symbol set.
*/
public synchronized void clearSlot(int lcid, int codePoint) {
ProgramSymbolSet set = getSymbolSet(lcid);
if (set != null) {
int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint;
set.clearSlot(index);
}
}
/**
* Commits all staged multi-plane symbol sets to the active presentation sets atomically.
*/
@@ -94,6 +94,10 @@ public class ProgramSymbolSet {
return isTriplePlane;
}
public boolean isLoaded() {
return pixelData != null && pixelData.length > 0;
}
/**
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
* Enables unscaled 1:1 hardware blitting in Java2D.
@@ -17,5 +17,64 @@ public final class VectorSymbolData {
public static final int MARKER_WIDTH = 24;
public static final int MARKER_HEIGHT = 32;
static char[] marker_data = new char[]{'\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00ff', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\u0012', '\u0000', '\u0010', '\u00ff', '\u00c1', '\u0014', '\u0000', '\f', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\n', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\n', '\u00ff', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00ff', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\u0012', '\u0000', '\u0010', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\f', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\n', '`', '\u0000', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\n', '`', '\u0000', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\u000b', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\u0011', '\u0000', '\u000b', '\u0000', '\u0011', '\u0000', '\u000b', '\u0000', '\u000f', '`', '\u0000', '\u00ff', '\u00c5', '\u0018', '\u0000', '\t', '\u0000', '\u0010', '\u0000', '\t', '\u0000', '\u0013', '\u0000', '\u000f', '\u0000', '\u0013', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\t', '\u0000', '\r', '\u0000', '\t', '\u0000', '\u0010', '\u00ff'};
private static final int[] VSS_INDEX = buildVssIndex();
/**
* Builds and returns a 256-element offset lookup table for fast VSS character glyph addressing.
*/
public static int[] buildVssIndex() {
int[] offsets = new int[256];
java.util.Arrays.fill(offsets, -1);
int sym = VSS_SYMBOL_START;
if (sym < 256) {
offsets[sym] = 0;
}
for (int i = 0; i < vss_data.length; i++) {
if (vss_data[i] == END_DEFAULT) {
sym++;
if (sym < 256 && i + 1 < vss_data.length) {
offsets[sym] = i + 1;
}
}
}
return offsets;
}
/**
* Builds and returns an offset lookup table for standard GOCA markers.
*/
public static int[] buildMarkerIndex() {
int[] offsets = new int[32];
java.util.Arrays.fill(offsets, -1);
int m = 1;
if (m < 32) {
offsets[m] = 0;
}
for (int i = 0; i < marker_data.length; i++) {
if (marker_data[i] == END_DEFAULT) {
m++;
if (m < 32 && i + 1 < marker_data.length) {
offsets[m] = i + 1;
}
}
}
return offsets;
}
public static int getVssOffset(int charCode) {
if (charCode >= 0 && charCode < 256) {
return VSS_INDEX[charCode];
}
return -1;
}
public static char[] getVssData() {
return vss_data;
}
public static char[] getMarkerData() {
return marker_data;
}
}
@@ -863,6 +863,204 @@ public class InputProcessor {
return fieldLen;
}
/**
* Parse and execute IBM ECL standard mnemonic keystrokes (e.g. "USER[tab]PASS[enter]").
*/
public void sendKeys(String keys) {
if (keys == null || keys.isEmpty()) return;
int len = keys.length();
int i = 0;
while (i < len) {
char c = keys.charAt(i);
if (c == '[') {
int end = keys.indexOf(']', i);
if (end > i) {
String token = keys.substring(i + 1, end).trim().toLowerCase();
executeMnemonicToken(token);
i = end + 1;
continue;
}
}
typeCharacter(c);
i++;
}
}
private void executeMnemonicToken(String token) {
switch (token) {
case "enter":
case "return":
sendAid(AID_ENTER);
break;
case "clear":
sendAid(AID_CLEAR);
break;
case "tab":
tab();
break;
case "backtab":
case "btab":
backTab();
break;
case "newline":
case "nl":
newline();
break;
case "home":
cursorHome();
break;
case "up":
case "curup":
cursorUp();
break;
case "down":
case "curdown":
cursorDown();
break;
case "left":
case "curleft":
cursorLeft();
break;
case "right":
case "curright":
cursorRight();
break;
case "eraseeof":
case "erase_eof":
eraseEof();
break;
case "eraseinp":
case "erase_input":
eraseInput();
break;
case "dup":
dup();
break;
case "fm":
case "fieldmark":
fieldMark();
break;
case "attn":
attn();
break;
case "sysreq":
sysReq();
break;
case "reset":
reset();
break;
case "insert":
setInsertMode(!isInsertMode());
break;
case "delete":
deleteChar();
break;
case "backspace":
case "bs":
backspace();
break;
case "wordtab":
processWordTab(true);
break;
case "wordbacktab":
processWordTab(false);
break;
case "deleteword":
processDeleteWord();
break;
default:
if (token.startsWith("pf")) {
try {
int pfNum = Integer.parseInt(token.substring(2));
if (pfNum >= 1 && pfNum <= 24) {
sendAid(AID_PF1 + (pfNum - 1));
}
} catch (NumberFormatException ignored) {}
} else if (token.startsWith("pa")) {
try {
int paNum = Integer.parseInt(token.substring(2));
if (paNum >= 1 && paNum <= 3) {
sendAid(AID_PA1 + (paNum - 1));
}
} catch (NumberFormatException ignored) {}
}
break;
}
}
/**
* Jump cursor to next or previous word boundary.
*/
public void processWordTab(boolean forward) {
if (keyboardLocked || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int cur = screen.getCursorAddress();
if (forward) {
// Find next whitespace then next non-whitespace
int pos = screen.incrementAddress(cur);
int count = 0;
while (count < size && !isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
count++;
}
while (count < size && isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
count++;
}
screen.setCursorAddress(pos);
} else {
// Find previous non-whitespace after whitespace
int pos = screen.decrementAddress(cur);
int count = 0;
while (count < size && isWhitespaceOrNull(pos)) {
pos = screen.decrementAddress(pos);
count++;
}
while (count < size && !isWhitespaceOrNull(screen.decrementAddress(pos))) {
pos = screen.decrementAddress(pos);
count++;
}
screen.setCursorAddress(pos);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/**
* Delete the word starting at the cursor position.
*/
public void processDeleteWord() {
if (keyboardLocked || screen == null) return;
int cur = screen.getCursorAddress();
if (screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(cur);
if (faIsProtected(fa & 0xFF)) return;
}
int count = 0;
int size = screen.getRows() * screen.getCols();
while (count < size && !isWhitespaceOrNull(screen.getCursorAddress())) {
deleteChar();
count++;
}
// Also delete trailing spaces
while (count < size && isWhitespaceOrNull(screen.getCursorAddress()) && screen.getCell(screen.getCursorAddress()).ec != 0) {
deleteChar();
count++;
}
}
private boolean isWhitespaceOrNull(int pos) {
if (screen == null) return true;
ExtendedAttribute ea = screen.getCell(pos);
if (ea.isFieldAttribute()) return true;
int ec = ea.ec & 0xFF;
return ec == 0 || ec == 0x40; // Null or EBCDIC Space
}
/**
* Send a structured field response directly.
* Used by DFT mode file transfer.
@@ -873,6 +1071,167 @@ public class InputProcessor {
}
}
/**
* Enter a double-byte (DBCS) character at the given screen address (or cursor position if pos < 0).
*/
public boolean DBCSinputChar(char c, int pos) {
if (keyboardLocked || screen == null) return false;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return false;
int baddr = (pos >= 0) ? pos : screen.getCursorAddress();
baddr = ((baddr % size) + size) % size;
if (screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(baddr);
if (faIsProtected(fa & 0xFF)) return false;
}
int dbcs = translator.unicodeToDbcs(c);
if (dbcs < 0) {
// Fall back to SBCS typing
typeCharacter(c);
return true;
}
int b1 = (dbcs >> 8) & 0xFF;
int b2 = dbcs & 0xFF;
int nextAddr = screen.incrementAddress(baddr);
if (screen.getCell(baddr).isFieldAttribute() || screen.getCell(nextAddr).isFieldAttribute()) {
return false;
}
screen.setCell(baddr, b1);
screen.getCell(baddr).ucs4 = c;
screen.setCell(nextAddr, b2);
screen.getCell(nextAddr).ucs4 = c;
// Set MDT
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa |= FA_MODIFY;
}
}
screen.setCursorAddress(screen.incrementAddress(nextAddr));
screen.markAllChanged();
return true;
}
/**
* Shift characters right within a field by shiftAmount positions.
*/
public void shiftCharactersRightInField(int baddr, int shiftAmount) {
if (screen == null || shiftAmount <= 0) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
baddr = ((baddr % size) + size) % size;
// Find end of field
int endAddr = baddr;
int count = 0;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
endAddr = screen.incrementAddress(endAddr);
count++;
if (endAddr == baddr) break;
}
for (int s = 0; s < shiftAmount; s++) {
int dst = endAddr;
int shiftCount = 0;
while (dst != baddr && shiftCount < size) {
int src = screen.decrementAddress(dst);
screen.getCell(dst).ec = screen.getCell(src).ec;
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
dst = src;
shiftCount++;
}
screen.getCell(baddr).ec = 0;
screen.getCell(baddr).ucs4 = 0;
}
screen.markAllChanged();
}
/**
* Shift characters right within the current line by count positions.
*/
public void shiftCharactersRightInLine(int row, int col, int shiftAmount) {
if (screen == null || shiftAmount <= 0) return;
int cols = screen.getCols();
int baddr = screen.rowColToAddress(row, col);
int lineEnd = screen.rowColToAddress(row, cols - 1);
for (int s = 0; s < shiftAmount; s++) {
for (int dst = lineEnd; dst > baddr; dst--) {
screen.getCell(dst).ec = screen.getCell(dst - 1).ec;
screen.getCell(dst).ucs4 = screen.getCell(dst - 1).ucs4;
}
screen.getCell(baddr).ec = 0;
screen.getCell(baddr).ucs4 = 0;
}
screen.markAllChanged();
}
/**
* Shift characters right across the screen by count positions.
*/
public void shiftCharactersRightInScreen(int pos, int shiftAmount) {
if (screen == null || shiftAmount <= 0) return;
int size = screen.getRows() * screen.getCols();
pos = ((pos % size) + size) % size;
for (int s = 0; s < shiftAmount; s++) {
for (int dst = size - 1; dst > pos; dst--) {
screen.getCell(dst).ec = screen.getCell(dst - 1).ec;
screen.getCell(dst).ucs4 = screen.getCell(dst - 1).ucs4;
}
screen.getCell(pos).ec = 0;
screen.getCell(pos).ucs4 = 0;
}
screen.markAllChanged();
}
/**
* Insert a double-byte (DBCS) character with insert-mode shifting (2 positions).
*/
public boolean DBCSinputCharInsert(char c, int pos) {
if (keyboardLocked || screen == null) return false;
int baddr = (pos >= 0) ? pos : screen.getCursorAddress();
// Shift 2 characters right
shiftCharactersRightInField(baddr, 2);
return DBCSinputChar(c, baddr);
}
/**
* Clean up orphaned or empty Shift-Out / Shift-In (SO 0x0E followed by SI 0x0F) control codes.
*/
public void clearUnusedSISO(int pos) {
if (screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int start = (pos >= 0) ? pos : 0;
int end = (pos >= 0) ? Math.min(size, pos + screen.getCols()) : size;
for (int i = start; i < end - 1; i++) {
ExtendedAttribute ea1 = screen.getCell(i);
ExtendedAttribute ea2 = screen.getCell(i + 1);
if (!ea1.isFieldAttribute() && !ea2.isFieldAttribute()) {
if ((ea1.ec & 0xFF) == 0x0E && (ea2.ec & 0xFF) == 0x0F) {
ea1.ec = 0;
ea1.ucs4 = 0;
ea2.ec = 0;
ea2.ucs4 = 0;
screen.markAllChanged();
}
}
}
}
/** Get a reference to the TelnetFSM for direct SF operations. */
public TelnetFSM getTelnetFSM() {
return fsm;
@@ -0,0 +1,400 @@
package haus.nightmare.lib3270j.nvt;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/**
* Network Virtual Terminal (NVT) processor.
* Handles ASCII / ANSI VT100 character stream processing, cursor positioning,
* escape sequence decoding, and NVT character/string transmission.
*/
public class NvtProcessor {
private static final Logger log = Logger.getLogger(NvtProcessor.class.getName());
private final ScreenBuffer screenBuffer;
private final EbcdicTranslator translator;
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
// Escape sequence parser states
private static final int STATE_NORMAL = 0;
private static final int STATE_ESC = 1;
private static final int STATE_CSI = 2;
private int parseState = STATE_NORMAL;
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
// Output sender callback
private OutputSender outputSender;
// Graphic Rendition state
private byte currentFg = 0;
private byte currentBg = 0;
private byte currentGr = 0;
// Saved cursor position
private int savedCursorRow = 0;
private int savedCursorCol = 0;
@FunctionalInterface
public interface OutputSender {
void sendRaw(byte[] data) throws IOException;
}
public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) {
this.screenBuffer = screenBuffer;
this.translator = translator;
}
public void setOutputSender(OutputSender outputSender) {
this.outputSender = outputSender;
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
}
/**
* Process incoming ASCII NVT data bytes.
*/
public synchronized void processNVTData(byte[] data, int offset, int length) {
if (length <= 0) return;
int rows = screenBuffer.getRows();
int cols = screenBuffer.getCols();
int size = rows * cols;
int curAddr = screenBuffer.getCursorAddress();
for (int i = offset; i < offset + length; i++) {
int b = data[i] & 0xFF;
if (parseState == STATE_NORMAL) {
if (b == 0x1B) { // ESC
parseState = STATE_ESC;
escBuffer.reset();
escBuffer.write(b);
} else if (b == 0x0D) { // CR
int r = curAddr / cols;
curAddr = r * cols;
} else if (b == 0x0A) { // LF
int r = curAddr / cols;
int c = curAddr % cols;
r++;
if (r >= rows) {
scrollUp();
r = rows - 1;
}
curAddr = r * cols + c;
} else if (b == 0x08 || b == 0x7F) { // BS or DEL
int c = curAddr % cols;
if (c > 0) {
curAddr--;
}
} else if (b == 0x09) { // TAB
int c = curAddr % cols;
int nextTab = ((c / 8) + 1) * 8;
if (nextTab >= cols) nextTab = cols - 1;
curAddr = (curAddr / cols) * cols + nextTab;
} else if (b == 0x0C) { // FF
screenBuffer.clear();
curAddr = 0;
} else if (b >= 0x20 && b < 0xFF) { // Printable ASCII
char ch = (char) b;
int ebc = translator.unicodeToEbcdic(ch);
ExtendedAttribute cell = screenBuffer.getCell(curAddr);
cell.clear();
cell.ec = (byte) (ebc >= 0 ? ebc : 0x40);
cell.ucs4 = ch;
cell.fg = currentFg;
cell.bg = currentBg;
cell.gr = currentGr;
curAddr++;
if (curAddr >= size) {
scrollUp();
curAddr = (rows - 1) * cols;
}
}
} else if (parseState == STATE_ESC) {
escBuffer.write(b);
if (b == '[') {
parseState = STATE_CSI;
} else if (b == '7') { // Save cursor
savedCursorRow = curAddr / cols;
savedCursorCol = curAddr % cols;
parseState = STATE_NORMAL;
} else if (b == '8') { // Restore cursor
curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol);
parseState = STATE_NORMAL;
} else if (b == 'c') { // RIS - Reset to Initial State
screenBuffer.clear();
curAddr = 0;
currentFg = 0;
currentBg = 0;
currentGr = 0;
parseState = STATE_NORMAL;
} else {
// Unknown 2-byte escape, finish
parseState = STATE_NORMAL;
}
} else if (parseState == STATE_CSI) {
escBuffer.write(b);
// CSI parameter/intermediate bytes: 0x20..0x3F, final bytes: 0x40..0x7E
if (b >= 0x40 && b <= 0x7E) {
byte[] seq = escBuffer.toByteArray();
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
parseState = STATE_NORMAL;
}
}
}
screenBuffer.setCursorAddress(curAddr);
screenBuffer.markAllChanged();
screenBuffer.updateDisplaySnapshot();
notifyScreenUpdated();
}
/**
* Parse and execute an ANSI CSI escape sequence.
* Returns updated cursor address.
*/
public int processAnsiEscapeSequence(byte[] seq, int curAddr, int rows, int cols) {
if (seq.length < 3) return curAddr;
int finalByte = seq[seq.length - 1] & 0xFF;
String paramStr = new String(seq, 2, seq.length - 3, StandardCharsets.US_ASCII);
String[] params = paramStr.split(";");
int r = curAddr / cols;
int c = curAddr % cols;
switch (finalByte) {
case 'H': // CUP - Cursor Position
case 'f': // HVP - Horizontal and Vertical Position
{
int p1 = parseParam(params, 0, 1) - 1;
int p2 = parseParam(params, 1, 1) - 1;
r = Math.max(0, Math.min(rows - 1, p1));
c = Math.max(0, Math.min(cols - 1, p2));
return r * cols + c;
}
case 'A': // CUU - Cursor Up
{
int count = parseParam(params, 0, 1);
r = Math.max(0, r - count);
return r * cols + c;
}
case 'B': // CUD - Cursor Down
{
int count = parseParam(params, 0, 1);
r = Math.min(rows - 1, r + count);
return r * cols + c;
}
case 'C': // CUF - Cursor Forward
{
int count = parseParam(params, 0, 1);
c = Math.min(cols - 1, c + count);
return r * cols + c;
}
case 'D': // CUB - Cursor Back
{
int count = parseParam(params, 0, 1);
c = Math.max(0, c - count);
return r * cols + c;
}
case 'J': // ED - Erase in Display
{
int mode = parseParam(params, 0, 0);
if (mode == 0) { // Cursor to end
for (int i = curAddr; i < rows * cols; i++) clearCell(i);
} else if (mode == 1) { // Beginning to cursor
for (int i = 0; i <= curAddr; i++) clearCell(i);
} else if (mode == 2 || mode == 3) { // Entire screen
screenBuffer.clear();
return 0;
}
return curAddr;
}
case 'K': // EL - Erase in Line
{
int mode = parseParam(params, 0, 0);
int lineStart = r * cols;
int lineEnd = lineStart + cols;
if (mode == 0) { // Cursor to end of line
for (int i = curAddr; i < lineEnd; i++) clearCell(i);
} else if (mode == 1) { // Start of line to cursor
for (int i = lineStart; i <= curAddr; i++) clearCell(i);
} else if (mode == 2) { // Entire line
for (int i = lineStart; i < lineEnd; i++) clearCell(i);
}
return curAddr;
}
case 'm': // SGR - Select Graphic Rendition
{
if (params.length == 0 || (params.length == 1 && params[0].isEmpty())) {
currentFg = 0;
currentBg = 0;
currentGr = 0;
} else {
for (String p : params) {
if (p.isEmpty()) continue;
try {
int code = Integer.parseInt(p);
applySgr(code);
} catch (NumberFormatException ignored) {}
}
}
return curAddr;
}
case 's': // Save cursor
savedCursorRow = r;
savedCursorCol = c;
return curAddr;
case 'u': // Restore cursor
r = Math.min(rows - 1, savedCursorRow);
c = Math.min(cols - 1, savedCursorCol);
return r * cols + c;
default:
return curAddr;
}
}
private int parseParam(String[] params, int idx, int defaultVal) {
if (params != null && idx < params.length && !params[idx].trim().isEmpty()) {
try {
return Integer.parseInt(params[idx].trim());
} catch (NumberFormatException ignored) {}
}
return defaultVal;
}
private void clearCell(int addr) {
ExtendedAttribute cell = screenBuffer.getCell(addr);
cell.clear();
cell.ec = 0;
cell.ucs4 = ' ';
}
private void scrollUp() {
int rows = screenBuffer.getRows();
int cols = screenBuffer.getCols();
for (int r = 0; r < rows - 1; r++) {
for (int c = 0; c < cols; c++) {
int dst = r * cols + c;
int src = (r + 1) * cols + c;
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
}
}
// Clear last line
int lastRowStart = (rows - 1) * cols;
for (int c = 0; c < cols; c++) {
clearCell(lastRowStart + c);
}
}
private void applySgr(int code) {
switch (code) {
case 0: // Reset
currentFg = 0;
currentBg = 0;
currentGr = 0;
break;
case 1: // Bold / Bright
currentGr |= GR_INTENSIFY;
break;
case 4: // Underline
currentGr |= GR_UNDERLINE;
break;
case 5: // Blink
currentGr |= GR_BLINK;
break;
case 7: // Reverse
currentGr |= GR_REVERSE;
break;
case 22: // Normal intensity
currentGr &= ~GR_INTENSIFY;
break;
case 24: // Not underlined
currentGr &= ~GR_UNDERLINE;
break;
case 25: // Not blinking
currentGr &= ~GR_BLINK;
break;
case 27: // Positive image (not reverse)
currentGr &= ~GR_REVERSE;
break;
case 30: currentFg = (byte) HOST_COLOR_NEUTRAL_BLACK; break;
case 31: currentFg = (byte) HOST_COLOR_RED; break;
case 32: currentFg = (byte) HOST_COLOR_GREEN; break;
case 33: currentFg = (byte) HOST_COLOR_YELLOW; break;
case 34: currentFg = (byte) HOST_COLOR_BLUE; break;
case 35: currentFg = (byte) HOST_COLOR_PINK; break;
case 36: currentFg = (byte) HOST_COLOR_TURQUOISE; break;
case 37: currentFg = (byte) HOST_COLOR_NEUTRAL_WHITE; break;
case 39: currentFg = 0; break;
case 40: currentBg = (byte) HOST_COLOR_NEUTRAL_BLACK; break;
case 41: currentBg = (byte) HOST_COLOR_RED; break;
case 42: currentBg = (byte) HOST_COLOR_GREEN; break;
case 43: currentBg = (byte) HOST_COLOR_YELLOW; break;
case 44: currentBg = (byte) HOST_COLOR_BLUE; break;
case 45: currentBg = (byte) HOST_COLOR_PINK; break;
case 46: currentBg = (byte) HOST_COLOR_TURQUOISE; break;
case 47: currentBg = (byte) HOST_COLOR_NEUTRAL_WHITE; break;
case 49: currentBg = 0; break;
}
}
/**
* Send a single character in NVT mode.
*/
public void sendNVTChar(char c) throws IOException {
if (outputSender != null) {
if (c == '\n') {
outputSender.sendRaw(new byte[] { (byte) '\r', (byte) '\n' });
} else {
outputSender.sendRaw(new byte[] { (byte) c });
}
}
}
/**
* Send a string in NVT mode with standard CRLF normalization.
*/
public void sendNVTString(String s) throws IOException {
if (s == null || outputSender == null) return;
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\n') {
out.write('\r');
out.write('\n');
} else if (c == '\r') {
if (i + 1 < s.length() && s.charAt(i + 1) == '\n') {
// Handled on next iteration
} else {
out.write('\r');
out.write('\n');
}
} else {
out.write((byte) c);
}
}
outputSender.sendRaw(out.toByteArray());
}
private void notifyScreenUpdated() {
for (ScreenUpdateListener l : screenListeners) {
l.onScreenUpdated();
}
}
}
@@ -0,0 +1,273 @@
package haus.nightmare.lib3270j.printer;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Print Output Destination & Spool Interface (PD3270).
* Implements 1:1 functional compatibility with IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PD3270.
*
* Supports printing to:
* 1. In-memory buffer / capture (for programmatic access or UI display)
* 2. Local disk file (with append/overwrite modes)
* 3. External system print command / pipe process (e.g. lpr, lp)
*/
public class PD3270 {
private static final Logger log = Logger.getLogger(PD3270.class.getName());
private final PrinterConfig config;
private String destination;
private boolean open = false;
private OutputStream targetOutputStream;
private Writer targetWriter;
private Process pipeProcess;
private final ByteArrayOutputStream memoryStream = new ByteArrayOutputStream();
private long byteCount = 0;
private int pageCount = 0;
private Charset outputCharset = StandardCharsets.UTF_8;
public PD3270() {
this(new PrinterConfig());
}
public PD3270(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.destination = this.config.getDestinationTarget();
}
/**
* Open print destination matching IBM HoD openPrinter specification.
* @param destination Target path, command string, or null/empty for in-memory capture.
* @return true if opened successfully, false on error.
*/
public synchronized boolean openPrinter(String destination) {
if (open) {
closePrinter();
}
this.destination = destination != null ? destination : (config != null ? config.getDestinationTarget() : null);
this.byteCount = 0;
this.pageCount = 0;
this.memoryStream.reset();
PrinterConfig.DestinationType destType = config != null ? config.getDestinationType() : PrinterConfig.DestinationType.MEMORY;
try {
if (this.destination != null && !this.destination.trim().isEmpty()) {
String trimmedDest = this.destination.trim();
if (destType == PrinterConfig.DestinationType.COMMAND || trimmedDest.startsWith("|")) {
String cmd = trimmedDest.startsWith("|") ? trimmedDest.substring(1).trim() : trimmedDest;
log.info("Opening printer pipe to process: " + cmd);
pipeProcess = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
targetOutputStream = new BufferedOutputStream(pipeProcess.getOutputStream());
targetWriter = new OutputStreamWriter(targetOutputStream, outputCharset);
} else if (destType == PrinterConfig.DestinationType.FILE || destType != PrinterConfig.DestinationType.MEMORY) {
log.info("Opening printer file: " + trimmedDest);
File file = new File(trimmedDest);
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
targetOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
targetWriter = new OutputStreamWriter(targetOutputStream, outputCharset);
}
}
open = true;
log.fine("PD3270 printer opened: destination=" + this.destination);
return true;
} catch (Exception e) {
log.log(Level.SEVERE, "Failed to open printer destination: " + destination, e);
abortPrinter();
return false;
}
}
/**
* Write raw bytes directly to print output destination.
*/
public synchronized void writePrintBytes(byte[] data, int offset, int length) {
if (!open) {
openPrinter(destination);
}
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
return;
}
try {
memoryStream.write(data, offset, length);
byteCount += length;
if (targetOutputStream != null) {
targetOutputStream.write(data, offset, length);
}
} catch (IOException e) {
log.log(Level.WARNING, "Error writing bytes to printer destination", e);
}
}
/**
* Write a single character to the active print destination.
*/
public synchronized void writePrintChar(char c) {
if (!open) {
openPrinter(destination);
}
try {
byte[] b = String.valueOf(c).getBytes(outputCharset);
memoryStream.write(b);
byteCount += b.length;
if (targetWriter != null) {
targetWriter.write(c);
} else if (targetOutputStream != null) {
targetOutputStream.write(b);
}
} catch (IOException e) {
log.log(Level.WARNING, "Error writing character to printer", e);
}
}
/**
* Write a string to the active print destination.
*/
public synchronized void writePrintString(String s) {
if (s == null || s.isEmpty()) return;
if (!open) {
openPrinter(destination);
}
try {
byte[] b = s.getBytes(outputCharset);
memoryStream.write(b);
byteCount += b.length;
if (targetWriter != null) {
targetWriter.write(s);
} else if (targetOutputStream != null) {
targetOutputStream.write(b);
}
} catch (IOException e) {
log.log(Level.WARNING, "Error writing string to printer", e);
}
}
/**
* Write a line with system line-separator.
*/
public synchronized void writePrintLine(String line) {
writePrintString((line != null ? line : "") + "\n");
}
/**
* Advance / Form Feed to next page.
*/
public synchronized void formFeed() {
pageCount++;
writePrintChar('\f');
flush();
}
/**
* Flush all buffered print data to target stream.
*/
public synchronized void flush() {
try {
if (targetWriter != null) {
targetWriter.flush();
}
if (targetOutputStream != null) {
targetOutputStream.flush();
}
} catch (IOException e) {
log.log(Level.FINE, "Exception flushing print stream", e);
}
}
/**
* Close the printer destination cleanly.
*/
public synchronized void closePrinter() {
if (!open) return;
try {
flush();
if (targetWriter != null) {
targetWriter.close();
targetWriter = null;
}
if (targetOutputStream != null) {
targetOutputStream.close();
targetOutputStream = null;
}
if (pipeProcess != null) {
pipeProcess.getOutputStream().close();
try {
pipeProcess.waitFor();
} catch (InterruptedException ignored) {}
pipeProcess = null;
}
} catch (IOException e) {
log.log(Level.FINE, "Exception closing printer", e);
} finally {
open = false;
}
}
/**
* Abort the printer and clean up resources immediately.
*/
public synchronized void abortPrinter() {
try {
if (targetWriter != null) {
try { targetWriter.close(); } catch (Exception ignored) {}
targetWriter = null;
}
if (targetOutputStream != null) {
try { targetOutputStream.close(); } catch (Exception ignored) {}
targetOutputStream = null;
}
if (pipeProcess != null) {
pipeProcess.destroy();
pipeProcess = null;
}
} finally {
open = false;
}
}
// ========== Status & Accessors ==========
public boolean isOpen() { return open; }
public String getDestination() { return destination; }
public long getByteCount() { return byteCount; }
public int getPageCount() { return pageCount; }
public synchronized String getCapturedText() {
return new String(memoryStream.toByteArray(), outputCharset);
}
public synchronized byte[] getCapturedBytes() {
return memoryStream.toByteArray();
}
public synchronized void resetCapture() {
memoryStream.reset();
byteCount = 0;
pageCount = 0;
}
public Charset getOutputCharset() { return outputCharset; }
public void setOutputCharset(Charset charset) {
if (charset != null) {
this.outputCharset = charset;
}
}
}
@@ -0,0 +1,393 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* LU Type 3 3270 Printer Data Stream Processor (PrintPS3270 / DS3270P).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintPS3270 / DS3270P.
*
* Simulates a 3270 printer buffer, processes Write Control Characters (WCC),
* interprets 3270 buffer orders (SBA, SF, SFE, SA, RA, EUA, GE, NL, EM, FF, CR),
* and renders formatted lines to PD3270.
*/
public class PrintPS3270 {
private static final Logger log = Logger.getLogger(PrintPS3270.class.getName());
private final PrinterConfig config;
private final PD3270 pd;
private final EbcdicTranslator translator;
private int rows = 24;
private int cols = 80;
private int bufferSize = 24 * 80;
private int bufferAddress = 0;
// Buffer planes
private byte[] textPlane;
private byte[] attrPlane;
private byte[] colorPlane;
private byte[] hilitePlane;
private int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL;
public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
int mpp = this.config.getMpp();
this.cols = (mpp > 0) ? mpp : 80;
this.rows = (this.config.getMpl() > 0) ? this.config.getMpl() : 24;
this.bufferSize = this.rows * this.cols;
initBuffer();
}
private void initBuffer() {
this.textPlane = new byte[bufferSize];
this.attrPlane = new byte[bufferSize];
this.colorPlane = new byte[bufferSize];
this.hilitePlane = new byte[bufferSize];
erasePrintBuffer();
}
/**
* Erase all contents of the printer buffer to nulls (0x00).
*/
public synchronized void erasePrintBuffer() {
Arrays.fill(textPlane, (byte) 0x00);
Arrays.fill(attrPlane, (byte) 0x00);
Arrays.fill(colorPlane, (byte) 0x00);
Arrays.fill(hilitePlane, (byte) 0x00);
this.bufferAddress = 0;
}
/**
* Main entry point for processing LU Type 3 3270 Printer Data Stream records.
*/
public synchronized void process3270PrintDS(byte[] data, int offset, int length) {
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
return;
}
int cmd = data[offset] & 0xFF;
switch (cmd) {
case DS3270Constants.CMD_WRITE:
case DS3270Constants.SNA_CMD_W:
processWrite(data, offset, length, false);
break;
case DS3270Constants.CMD_ERASE_WRITE:
case DS3270Constants.SNA_CMD_EW:
processEraseWrite(data, offset, length);
break;
case DS3270Constants.CMD_ERASE_WRITE_ALT:
case DS3270Constants.SNA_CMD_EWA:
processEraseWriteAlternate(data, offset, length);
break;
case DS3270Constants.CMD_ERASE_ALL_UNPROTECTED:
case DS3270Constants.SNA_CMD_EAU:
erasePrintBuffer();
break;
default:
// If command byte is unrecognized, treat entire stream as write without erase
processWrite(data, offset - 1, length + 1, false);
break;
}
}
public synchronized void processWrite(byte[] data, int offset, int length, boolean eraseFirst) {
if (eraseFirst) {
erasePrintBuffer();
}
int idx = offset + 1; // Skip command byte
int end = offset + length;
if (idx >= end) return;
// WCC (Write Control Character)
int wcc = data[idx++] & 0xFF;
boolean startPrint = isStartPrint(wcc);
this.currentPrintFormat = getPrintFormat(wcc);
byte currentColor = 0;
byte currentHilite = 0;
while (idx < end) {
int b = data[idx++] & 0xFF;
switch (b) {
case PrinterConstants.ORDER_SBA:
if (idx + 1 < end) {
int b1 = data[idx++] & 0xFF;
int b2 = data[idx++] & 0xFF;
bufferAddress = decodeBufferAddress(b1, b2) % bufferSize;
}
break;
case PrinterConstants.ORDER_SF:
if (idx < end) {
byte fa = data[idx++];
setCellFA(bufferAddress, fa);
bufferAddress = (bufferAddress + 1) % bufferSize;
}
break;
case PrinterConstants.ORDER_SFE:
if (idx < end) {
int pairCount = data[idx++] & 0xFF;
byte fa = 0;
for (int p = 0; p < pairCount && idx + 1 < end; p++) {
int type = data[idx++] & 0xFF;
int val = data[idx++] & 0xFF;
if (type == 0x00 || type == 0xC0) {
fa = (byte) val;
} else if (type == PrinterConstants.SA_COLOR) {
currentColor = (byte) val;
} else if (type == PrinterConstants.SA_HILITE) {
currentHilite = (byte) val;
}
}
setCellFA(bufferAddress, fa);
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
break;
case PrinterConstants.ORDER_SA:
if (idx + 1 < end) {
int type = data[idx++] & 0xFF;
int val = data[idx++] & 0xFF;
if (type == PrinterConstants.SA_COLOR) {
currentColor = (byte) val;
} else if (type == PrinterConstants.SA_HILITE) {
currentHilite = (byte) val;
} else if (type == PrinterConstants.SA_RESET) {
currentColor = 0;
currentHilite = 0;
}
}
break;
case PrinterConstants.ORDER_RA:
if (idx + 2 < end) {
int b1 = data[idx++] & 0xFF;
int b2 = data[idx++] & 0xFF;
int stopAddr = decodeBufferAddress(b1, b2) % bufferSize;
byte fillChar = data[idx++];
if (stopAddr == bufferAddress) {
Arrays.fill(textPlane, fillChar);
} else {
while (bufferAddress != stopAddr) {
textPlane[bufferAddress] = fillChar;
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
}
}
break;
case PrinterConstants.ORDER_EUA:
if (idx + 1 < end) {
int b1 = data[idx++] & 0xFF;
int b2 = data[idx++] & 0xFF;
int stopAddr = decodeBufferAddress(b1, b2) % bufferSize;
while (bufferAddress != stopAddr) {
textPlane[bufferAddress] = 0x00;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
}
break;
case PrinterConstants.ORDER_IC:
case PrinterConstants.ORDER_PT:
// Position cursor / tab
break;
case PrinterConstants.ORDER_GE:
if (idx < end) {
byte geByte = data[idx++];
textPlane[bufferAddress] = geByte;
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
break;
case PrinterConstants.ORDER_NL:
// Advance bufferAddress to start of next line
int curRow = bufferAddress / cols;
bufferAddress = ((curRow + 1) * cols) % bufferSize;
break;
case PrinterConstants.ORDER_EM:
// End of message: trigger print buffer flush
flushPrintBuffer();
break;
case PrinterConstants.ORDER_FF:
pd.formFeed();
break;
case PrinterConstants.ORDER_CR:
bufferAddress = (bufferAddress / cols) * cols;
break;
default:
// Standard printable EBCDIC character
textPlane[bufferAddress] = (byte) b;
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
break;
}
}
if (startPrint) {
flushPrintBuffer();
}
}
public synchronized void processEraseWrite(byte[] data, int offset, int length) {
processWrite(data, offset, length, true);
}
public synchronized void processEraseWriteAlternate(byte[] data, int offset, int length) {
processWrite(data, offset, length, true);
}
private void setCellFA(int addr, byte fa) {
attrPlane[addr] = fa;
textPlane[addr] = 0x00; // Field attributes display as blanks/nulls
}
private int decodeBufferAddress(int b1, int b2) {
// Fast 3270 12-bit / 14-bit address calculation
if ((b1 & 0xC0) == 0) {
return ((b1 & 0x3F) << 8) | b2;
}
return ((b1 & 0x3F) << 6) | (b2 & 0x3F);
}
/**
* Format and print a specific row from the presentation buffer.
*/
public synchronized void printLine(int row, int length) {
if (row < 0 || row >= rows) return;
int lineLen = Math.min(length > 0 ? length : cols, cols);
int startAddr = row * cols;
char[] chars = new char[lineLen];
for (int c = 0; c < lineLen; c++) {
int ebc = textPlane[startAddr + c] & 0xFF;
if (ebc == 0x00) {
chars[c] = ' ';
} else {
chars[c] = translator.ebcdicToUnicode(ebc);
}
}
// Trim trailing blanks
int end = lineLen;
while (end > 0 && chars[end - 1] == ' ') {
end--;
}
if (end > 0) {
pd.writePrintString(new String(chars, 0, end));
}
pd.writePrintString("\n");
}
/**
* Print direct raw EBCDIC text through translator.
*/
public synchronized void printText(byte[] text, int len) {
if (text == null || len <= 0) return;
int actualLen = Math.min(len, text.length);
char[] chars = new char[actualLen];
for (int i = 0; i < actualLen; i++) {
int ebc = text[i] & 0xFF;
chars[i] = (ebc == 0x00) ? ' ' : translator.ebcdicToUnicode(ebc);
}
pd.writePrintString(new String(chars));
}
/**
* Render the entire 3270 printer buffer according to current formatting parameters.
*/
public synchronized void formatBufferToPrint(int lineLength) {
int effLineLen = lineLength > 0 ? lineLength : cols;
for (int r = 0; r < rows; r++) {
printLine(r, effLineLen);
}
}
/**
* Flush current printer buffer to PD3270 destination.
*/
public synchronized void flushPrintBuffer() {
int lineLen = cols;
switch (currentPrintFormat) {
case PrinterConstants.PRINT_FMT_40_COL: lineLen = 40; break;
case PrinterConstants.PRINT_FMT_64_COL: lineLen = 64; break;
case PrinterConstants.PRINT_FMT_80_COL: lineLen = 80; break;
default: lineLen = cols; break;
}
formatBufferToPrint(lineLen);
pd.flush();
}
// ========== Attribute & WCC Calculation Helpers ==========
public boolean isStartPrint(int wcc) {
return (wcc & PrinterConstants.WCC_START_PRINT_BIT) != 0;
}
public int getPrintFormat(int wcc) {
return wcc & PrinterConstants.WCC_PRINT_FORMAT_MASK;
}
public int calculateColor(int colorAttr) {
// Map IBM 3270 color attribute code (0xF1=Blue, 0xF2=Red, 0xF3=Pink, 0xF4=Green, 0xF5=Turquoise, 0xF6=Yellow, 0xF7=White)
switch (colorAttr) {
case 0xF1: return 1; // Blue
case 0xF2: return 2; // Red
case 0xF3: return 3; // Pink
case 0xF4: return 4; // Green
case 0xF5: return 5; // Turquoise
case 0xF6: return 6; // Yellow
case 0xF7: return 7; // Neutral White
default: return 0; // Default Neutral
}
}
public int calculateHighlight(int hiliteAttr) {
switch (hiliteAttr) {
case PrinterConstants.SEAC_BLINK: return 1;
case PrinterConstants.SEAC_REVERSE: return 2;
case PrinterConstants.SEAC_UNDERLINE: return 4;
default: return 0;
}
}
public int calculateCharset(int csAttr) {
return csAttr & 0xFF;
}
// ========== Accessors ==========
public int getRows() { return rows; }
public int getCols() { return cols; }
public int getBufferSize() { return bufferSize; }
public int getBufferAddress() { return bufferAddress; }
public PD3270 getPD() { return pd; }
public EbcdicTranslator getTranslator() { return translator; }
}
@@ -0,0 +1,586 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* SCS (SNA Character String) Interpreter for IBM LU Type 1 Printer Sessions.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintSCS3270.
*/
public class PrintSCS3270 {
private static final Logger log = Logger.getLogger(PrintSCS3270.class.getName());
private final PrinterConfig config;
private final PD3270 pd;
private final EbcdicTranslator translator;
// Formatting Parameters
private int mpp = PrinterConstants.DEFAULT_MPP; // Maximum Presentation Position (Line Length)
private int mpl = PrinterConstants.DEFAULT_MPL; // Maximum Page Length
private int leftMargin = 1;
private int rightMargin = PrinterConstants.DEFAULT_MPP;
private int topMargin = 1;
private int bottomMargin = PrinterConstants.DEFAULT_MPL;
private int cpi = PrinterConstants.DEFAULT_CPI;
private int lpi = PrinterConstants.DEFAULT_LPI;
// Tab Stops (1-based arrays)
private int[] horizontalTabs = new int[0];
private int[] verticalTabs = new int[0];
// Current State
private int currentRow = 1;
private int currentCol = 1;
private boolean doubleWidth = false;
private int activeColor = 0;
private int activeHighlight = PrinterConstants.SEAC_DEFAULT;
private int textOrientation = 0;
private boolean presentationEnabled = true;
// Line buffering for print composition
private char[] lineBuffer;
private boolean lineModified = false;
public PrintSCS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
resetSCSFormatDefaults();
}
/**
* Reset all SCS formatting parameters and presentation state to initial defaults.
*/
public synchronized void resetSCSFormatDefaults() {
this.mpp = config.getMpp();
this.mpl = config.getMpl();
this.leftMargin = config.getLeftMargin();
this.rightMargin = config.getRightMargin();
this.topMargin = config.getTopMargin();
this.bottomMargin = config.getBottomMargin();
this.cpi = config.getCpi();
this.lpi = config.getLpi();
this.horizontalTabs = new int[0];
this.verticalTabs = new int[0];
this.currentRow = this.topMargin;
this.currentCol = this.leftMargin;
this.doubleWidth = false;
this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
this.textOrientation = 0;
this.presentationEnabled = true;
this.lineBuffer = new char[Math.max(256, mpp + 1)];
Arrays.fill(lineBuffer, ' ');
this.lineModified = false;
}
/**
* Process host data stream containing SCS orders and characters.
* @param data Byte array from host.
* @param offset Starting offset.
* @param length Number of bytes to process.
*/
public synchronized void processHostData(byte[] data, int offset, int length) {
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
return;
}
int idx = offset;
int end = offset + length;
while (idx < end) {
int b = data[idx] & 0xFF;
// Check 2-byte SCS prefix (0x2B)
if (b == PrinterConstants.SCS_PREFIX_2B) {
if (idx + 1 >= end) {
idx++;
break;
}
int subOrder = data[idx + 1] & 0xFF;
int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0;
int orderTotalLen = 2 + (paramLen > 0 ? (paramLen + 1) : 1); // 0x2B + SubOrder + paramLen + payload
// In standard SCS, paramLen byte specifies length of following parameters
// or paramLen is total length including length byte.
int bytesAvailable = end - idx;
int sliceLen = Math.min(orderTotalLen, bytesAvailable);
switch (subOrder) {
case PrinterConstants.SCS_SHF:
processSetHorizontalFormat(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SVF:
processSetVerticalFormat(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SLD:
processSetLineDensity(data, idx, sliceLen);
break;
case PrinterConstants.SCS_STO:
processSetTextOrientation(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SEAC:
processSetEnhancedAttribute(data, idx, sliceLen);
break;
case PrinterConstants.SCS_PPA:
processPresentationPositionAdvancing(data, idx, sliceLen);
break;
case PrinterConstants.SCS_PPV:
processPresentationPositionVertical(data, idx, sliceLen);
break;
default:
log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder));
break;
}
idx += sliceLen;
continue;
}
// Check SA (Set Attribute 0x28)
if (b == PrinterConstants.SCS_SA) {
if (idx + 2 < end) {
processSetAttribute(data, idx, 3);
idx += 3;
} else {
idx = end;
}
continue;
}
// Check TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRS) {
if (idx + 1 < end) {
int trsLen = data[idx + 1] & 0xFF;
int actualTrs = Math.min(trsLen, end - (idx + 2));
processTransparentStream(data, idx + 2, actualTrs);
idx += 2 + actualTrs;
} else {
idx = end;
}
continue;
}
// Check Single-Byte SCS Controls
switch (b) {
case PrinterConstants.SCS_NUL:
// Null - ignored
idx++;
break;
case PrinterConstants.SCS_CR:
carriageReturn();
idx++;
break;
case PrinterConstants.SCS_LF:
lineFeed();
idx++;
break;
case PrinterConstants.SCS_NL:
case PrinterConstants.SCS_RNLS:
newLine();
idx++;
break;
case PrinterConstants.SCS_FF:
formFeed();
idx++;
break;
case PrinterConstants.SCS_BS:
case PrinterConstants.SCS_NBS:
backspace();
idx++;
break;
case PrinterConstants.SCS_HT:
processHorizontalTab();
idx++;
break;
case PrinterConstants.SCS_VT:
processVerticalTab();
idx++;
break;
case PrinterConstants.SCS_ENP:
presentationEnabled = true;
idx++;
break;
case PrinterConstants.SCS_INP:
presentationEnabled = false;
idx++;
break;
case PrinterConstants.SCS_BEL:
// Sound alarm
idx++;
break;
case PrinterConstants.SCS_GE:
if (idx + 1 < end) {
int geChar = data[idx + 1] & 0xFF;
processGraphicEscapeChar(geChar);
idx += 2;
} else {
idx++;
}
break;
case PrinterConstants.SCS_SP:
case PrinterConstants.SCS_RSP:
printCharacter(' ');
idx++;
break;
default:
// Standard printable character
if (presentationEnabled) {
char ch = translator.ebcdicToUnicode(b);
printCharacter(ch);
}
idx++;
break;
}
}
}
private void printCharacter(char c) {
if (currentCol > rightMargin || currentCol > mpp) {
advanceToNextLine();
}
ensureLineBufferSize(currentCol + (doubleWidth ? 2 : 1));
lineBuffer[currentCol - 1] = c;
lineModified = true;
currentCol += (doubleWidth ? 2 : 1);
}
private void processGraphicEscapeChar(int ec) {
char unicode = translator.getAplGraphic(ec);
if (unicode == 0 || unicode == ' ') {
unicode = translator.ebcdicToUnicode(ec);
}
printCharacter(unicode);
}
private void ensureLineBufferSize(int requiredSize) {
if (requiredSize > lineBuffer.length) {
int newSize = Math.max(requiredSize + 32, lineBuffer.length * 2);
char[] newBuf = new char[newSize];
Arrays.fill(newBuf, ' ');
System.arraycopy(lineBuffer, 0, newBuf, 0, lineBuffer.length);
lineBuffer = newBuf;
}
}
/**
* Flush the current line buffer to PD3270.
*/
public synchronized void flushLineBuffer() {
if (lineModified) {
// Trim trailing spaces
int len = lineBuffer.length;
while (len > 0 && lineBuffer[len - 1] == ' ') {
len--;
}
if (len > 0) {
pd.writePrintString(new String(lineBuffer, 0, len));
}
}
pd.writePrintString("\n");
Arrays.fill(lineBuffer, ' ');
lineModified = false;
}
// ========== SCS Order Implementations ==========
public synchronized void carriageReturn() {
currentCol = leftMargin;
}
public synchronized void lineFeed() {
flushLineBuffer();
currentRow++;
if (currentRow > bottomMargin || currentRow > mpl) {
advanceToNextPage();
}
}
public synchronized void newLine() {
carriageReturn();
lineFeed();
}
public synchronized void formFeed() {
flushLineBuffer();
pd.formFeed();
currentRow = topMargin;
currentCol = leftMargin;
}
public synchronized void backspace() {
if (currentCol > leftMargin) {
currentCol -= (doubleWidth ? 2 : 1);
if (currentCol < leftMargin) currentCol = leftMargin;
}
}
public synchronized void advanceToNextLine() {
newLine();
}
public synchronized void advanceToNextPage() {
formFeed();
}
// ========== Tab Stops and Calculations ==========
public synchronized int calculateHorizontalTab(int currentPos) {
if (horizontalTabs != null && horizontalTabs.length > 0) {
for (int tab : horizontalTabs) {
if (tab > currentPos) {
return tab;
}
}
}
// Default tab stop: advance to next multiple of 8 + 1
int nextTab = ((currentPos / 8) + 1) * 8 + 1;
return Math.min(nextTab, rightMargin);
}
public synchronized int calculateVerticalTab(int currentLine) {
if (verticalTabs != null && verticalTabs.length > 0) {
for (int tab : verticalTabs) {
if (tab > currentLine) {
return tab;
}
}
}
return -1; // No more vertical tabs on this page
}
public synchronized void processHorizontalTab() {
int nextTab = calculateHorizontalTab(currentCol);
if (nextTab <= rightMargin) {
currentCol = nextTab;
} else {
advanceToNextLine();
}
}
public synchronized void processVerticalTab() {
int nextTab = calculateVerticalTab(currentRow);
if (nextTab > 0 && nextTab <= bottomMargin) {
while (currentRow < nextTab) {
lineFeed();
}
} else {
advanceToNextPage();
}
}
// ========== Multi-Byte Order Parsers ==========
public synchronized void setHorizontalFormat(int lineLength, int[] tabs) {
this.mpp = lineLength > 0 ? lineLength : PrinterConstants.DEFAULT_MPP;
if (this.rightMargin > this.mpp) {
this.rightMargin = this.mpp;
}
if (tabs != null) {
this.horizontalTabs = Arrays.copyOf(tabs, tabs.length);
Arrays.sort(this.horizontalTabs);
}
}
public synchronized void setVerticalFormat(int pageLength, int[] tabs) {
this.mpl = pageLength > 0 ? pageLength : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) {
this.bottomMargin = this.mpl;
}
if (tabs != null) {
this.verticalTabs = Arrays.copyOf(tabs, tabs.length);
Arrays.sort(this.verticalTabs);
}
}
public synchronized void setHorizontalMargins(int leftMargin, int rightMargin) {
this.leftMargin = Math.max(1, leftMargin);
this.rightMargin = Math.min(this.mpp, Math.max(this.leftMargin, rightMargin));
if (currentCol < this.leftMargin) currentCol = this.leftMargin;
}
public synchronized void setVerticalMargins(int topMargin, int bottomMargin) {
this.topMargin = Math.max(1, topMargin);
this.bottomMargin = Math.min(this.mpl, Math.max(this.topMargin, bottomMargin));
if (currentRow < this.topMargin) currentRow = this.topMargin;
}
public synchronized void setPrintDensity(int cpi, int lpi) {
if (cpi > 0) this.cpi = cpi;
if (lpi > 0) this.lpi = lpi;
}
public synchronized void setEnhancedHighlight(int highlightType) {
this.activeHighlight = highlightType;
}
public synchronized void startDoubleWidthCharacters() {
this.doubleWidth = true;
}
public synchronized void endDoubleWidthCharacters() {
this.doubleWidth = false;
}
public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) {
if (len < 4) return;
int mppVal = data[offset + 3] & 0xFF;
if (mppVal > 0) {
this.mpp = mppVal;
this.rightMargin = Math.min(this.rightMargin, this.mpp);
}
if (len >= 6) {
int lm = data[offset + 4] & 0xFF;
int rm = data[offset + 5] & 0xFF;
if (lm > 0) this.leftMargin = lm;
if (rm > 0 && rm >= lm) this.rightMargin = rm;
}
if (len > 6) {
int tabCount = len - 6;
int[] tabs = new int[tabCount];
for (int i = 0; i < tabCount; i++) {
tabs[i] = data[offset + 6 + i] & 0xFF;
}
Arrays.sort(tabs);
this.horizontalTabs = tabs;
}
}
public synchronized void processSetVerticalFormat(byte[] data, int offset, int len) {
if (len < 4) return;
int mplVal = data[offset + 3] & 0xFF;
if (mplVal > 0) {
this.mpl = mplVal;
this.bottomMargin = Math.min(this.bottomMargin, this.mpl);
}
if (len >= 6) {
int tm = data[offset + 4] & 0xFF;
int bm = data[offset + 5] & 0xFF;
if (tm > 0) this.topMargin = tm;
if (bm > 0 && bm >= tm) this.bottomMargin = bm;
}
if (len > 6) {
int tabCount = len - 6;
int[] tabs = new int[tabCount];
for (int i = 0; i < tabCount; i++) {
tabs[i] = data[offset + 6 + i] & 0xFF;
}
Arrays.sort(tabs);
this.verticalTabs = tabs;
}
}
public synchronized void processSetLineDensity(byte[] data, int offset, int len) {
if (len < 4) return;
int points = data[offset + 3] & 0xFF;
if (points > 0) {
// Line density in points / inch (72 points = 1 inch)
// 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI
this.lpi = Math.max(1, 72 / points);
}
}
public synchronized void processSetTextOrientation(byte[] data, int offset, int len) {
if (len >= 4) {
this.textOrientation = data[offset + 3] & 0xFF;
}
}
public synchronized void processSetEnhancedAttribute(byte[] data, int offset, int len) {
if (len >= 5) {
int attrVal = data[offset + 4] & 0xFF;
setEnhancedHighlight(attrVal);
}
}
public synchronized void processPresentationPositionAdvancing(byte[] data, int offset, int len) {
if (len < 5) return;
int subfn = data[offset + 3] & 0xFF;
int val = data[offset + 4] & 0xFF;
if (subfn == PrinterConstants.POS_ABSOLUTE) {
currentCol = Math.min(mpp, Math.max(leftMargin, val));
} else if (subfn == PrinterConstants.POS_RELATIVE) {
currentCol = Math.min(mpp, currentCol + val);
}
}
public synchronized void processPresentationPositionVertical(byte[] data, int offset, int len) {
if (len < 5) return;
int subfn = data[offset + 3] & 0xFF;
int val = data[offset + 4] & 0xFF;
if (subfn == PrinterConstants.POS_ABSOLUTE) {
while (currentRow < val && currentRow < bottomMargin) {
lineFeed();
}
} else if (subfn == PrinterConstants.POS_RELATIVE) {
for (int i = 0; i < val && currentRow < bottomMargin; i++) {
lineFeed();
}
}
}
public synchronized void processSetAttribute(byte[] data, int offset, int len) {
if (len < 3) return;
int attrType = data[offset + 1] & 0xFF;
int attrVal = data[offset + 2] & 0xFF;
if (attrType == PrinterConstants.SA_COLOR) {
this.activeColor = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE) {
this.activeHighlight = attrVal;
} else if (attrType == PrinterConstants.SA_RESET) {
this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
this.doubleWidth = false;
}
}
public synchronized void processTransparentStream(byte[] data, int offset, int len) {
if (len > 0) {
pd.writePrintBytes(data, offset, len);
}
}
// ========== Accessors ==========
public int getLineLength() { return mpp; }
public void setLineLength(int len) { this.mpp = len; }
public int getPageLength() { return mpl; }
public void setPageLength(int len) { this.mpl = len; }
public int getLeftMargin() { return leftMargin; }
public int getRightMargin() { return rightMargin; }
public int getTopMargin() { return topMargin; }
public int getBottomMargin() { return bottomMargin; }
public int getCurrentRow() { return currentRow; }
public int getCurrentColumn() { return currentCol; }
public int getLinesPerInch() { return lpi; }
public int getCharsPerInch() { return cpi; }
public boolean isDoubleWidth() { return doubleWidth; }
public void setDoubleWidth(boolean dw) { this.doubleWidth = dw; }
public int getActiveColor() { return activeColor; }
public void setActiveColor(int color) { this.activeColor = color; }
public int getActiveHighlight() { return activeHighlight; }
public void setActiveHighlight(int hilite) { this.activeHighlight = hilite; }
public int getTextOrientation() { return textOrientation; }
public PD3270 getPD() { return pd; }
public EbcdicTranslator getTranslator() { return translator; }
}
@@ -0,0 +1,53 @@
package haus.nightmare.lib3270j.printer;
import java.util.EventObject;
/**
* Event object representing state and progress changes in a 3270/3287 printer session.
*/
public class PrintSessionEvent extends EventObject {
public static final int EVENT_JOB_STARTED = 1;
public static final int EVENT_JOB_DATA = 2;
public static final int EVENT_PAGE_COMPLETE = 3;
public static final int EVENT_JOB_COMPLETE = 4;
public static final int EVENT_STATUS_CHANGED = 5;
public static final int EVENT_ERROR = 6;
private final int eventType;
private final String jobName;
private final int pageNumber;
private final long byteCount;
private final int totalPages;
private final int statusCode;
private final String message;
private final byte[] data;
public PrintSessionEvent(Object source, int eventType, String jobName, int pageNumber,
long byteCount, int totalPages, int statusCode, String message, byte[] data) {
super(source);
this.eventType = eventType;
this.jobName = jobName;
this.pageNumber = pageNumber;
this.byteCount = byteCount;
this.totalPages = totalPages;
this.statusCode = statusCode;
this.message = message;
this.data = data;
}
public int getEventType() { return eventType; }
public String getJobName() { return jobName; }
public int getPageNumber() { return pageNumber; }
public long getByteCount() { return byteCount; }
public int getTotalPages() { return totalPages; }
public int getStatusCode() { return statusCode; }
public String getMessage() { return message; }
public byte[] getData() { return data; }
@Override
public String toString() {
return "PrintSessionEvent[type=" + eventType + ", page=" + pageNumber + ", bytes=" + byteCount +
", status=" + statusCode + ", msg=" + message + "]";
}
}
@@ -0,0 +1,27 @@
package haus.nightmare.lib3270j.printer;
import java.util.EventListener;
/**
* Listener interface for 3270/3287 printer session events.
*/
public interface PrintSessionListener extends EventListener {
/** Fired when a new print job begins. */
void onPrintJobStarted(PrintSessionEvent event);
/** Fired when raw or formatted print data is processed. */
void onPrintJobData(PrintSessionEvent event);
/** Fired when a page is completed (Form Feed). */
void onPrintJobPageComplete(PrintSessionEvent event);
/** Fired when the print job is completed (End Of Job). */
void onPrintJobComplete(PrintSessionEvent event);
/** Fired when the printer session status changes. */
void onPrinterStatusChanged(PrintSessionEvent event);
/** Fired when a printer error occurs. */
void onPrinterError(PrintSessionEvent event);
}
@@ -0,0 +1,162 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.tls.TlsCertificateVerifier;
/**
* Configuration parameters for an IBM 3287 / 3286 printer session.
*/
public class PrinterConfig {
public enum DestinationType {
MEMORY,
FILE,
COMMAND,
LISTENER
}
private String host;
private int port = 23;
private boolean useTls = false;
private boolean tlsVerifyCert = true;
private TlsCertificateVerifier certificateVerifier = null;
private String sslProtocol = "TLS";
private int connectTimeoutMs = 15000;
private int soTimeoutMs = 0;
private boolean tcpNoDelay = true;
private boolean soKeepAlive = true;
// LU and Association
private String printerLuName = null;
private String associatedDisplayLuName = null;
private String deviceType = PrinterConstants.DEV_IBM_3287_1;
private String codePage = "037";
// Geometry and formatting
private int mpp = PrinterConstants.DEFAULT_MPP;
private int mpl = PrinterConstants.DEFAULT_MPL;
private int leftMargin = 1;
private int rightMargin = PrinterConstants.DEFAULT_MPP;
private int topMargin = 1;
private int bottomMargin = PrinterConstants.DEFAULT_MPL;
private int cpi = PrinterConstants.DEFAULT_CPI;
private int lpi = PrinterConstants.DEFAULT_LPI;
// Spool destination
private DestinationType destinationType = DestinationType.MEMORY;
private String destinationTarget = null;
private boolean formFeedAtEoj = true;
private boolean autoFlushOnEoj = true;
private boolean autoReconnect = false;
public PrinterConfig() {}
public PrinterConfig(String host, int port) {
this.host = host;
this.port = port;
}
public PrinterConfig(String host, int port, String printerLuName) {
this.host = host;
this.port = port;
this.printerLuName = printerLuName;
}
public PrinterConfig(String host, int port, String printerLuName, boolean useTls) {
this.host = host;
this.port = port;
this.printerLuName = printerLuName;
this.useTls = useTls;
}
// Getters and Setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
public TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
public void setCertificateVerifier(TlsCertificateVerifier certificateVerifier) { this.certificateVerifier = certificateVerifier; }
public String getSslProtocol() { return sslProtocol; }
public void setSslProtocol(String sslProtocol) { this.sslProtocol = sslProtocol; }
public int getConnectTimeoutMs() { return connectTimeoutMs; }
public void setConnectTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; }
public int getSoTimeoutMs() { return soTimeoutMs; }
public void setSoTimeoutMs(int soTimeoutMs) { this.soTimeoutMs = soTimeoutMs; }
public boolean isTcpNoDelay() { return tcpNoDelay; }
public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; }
public boolean isSoKeepAlive() { return soKeepAlive; }
public void setSoKeepAlive(boolean soKeepAlive) { this.soKeepAlive = soKeepAlive; }
public String getPrinterLuName() { return printerLuName; }
public void setPrinterLuName(String printerLuName) { this.printerLuName = printerLuName; }
public String getAssociatedDisplayLuName() { return associatedDisplayLuName; }
public void setAssociatedDisplayLuName(String associatedDisplayLuName) { this.associatedDisplayLuName = associatedDisplayLuName; }
public String getDeviceType() { return deviceType; }
public void setDeviceType(String deviceType) {
this.deviceType = (deviceType != null && !deviceType.trim().isEmpty()) ? deviceType.trim() : PrinterConstants.DEV_IBM_3287_1;
}
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) {
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
}
public int getMpp() { return mpp; }
public void setMpp(int mpp) {
this.mpp = mpp > 0 ? mpp : PrinterConstants.DEFAULT_MPP;
if (this.rightMargin > this.mpp) this.rightMargin = this.mpp;
}
public int getMpl() { return mpl; }
public void setMpl(int mpl) {
this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl;
}
public int getLeftMargin() { return leftMargin; }
public void setLeftMargin(int leftMargin) { this.leftMargin = Math.max(1, leftMargin); }
public int getRightMargin() { return rightMargin; }
public void setRightMargin(int rightMargin) { this.rightMargin = Math.min(mpp, Math.max(leftMargin, rightMargin)); }
public int getTopMargin() { return topMargin; }
public void setTopMargin(int topMargin) { this.topMargin = Math.max(1, topMargin); }
public int getBottomMargin() { return bottomMargin; }
public void setBottomMargin(int bottomMargin) { this.bottomMargin = Math.min(mpl, Math.max(topMargin, bottomMargin)); }
public int getCpi() { return cpi; }
public void setCpi(int cpi) { this.cpi = cpi > 0 ? cpi : PrinterConstants.DEFAULT_CPI; }
public int getLpi() { return lpi; }
public void setLpi(int lpi) { this.lpi = lpi > 0 ? lpi : PrinterConstants.DEFAULT_LPI; }
public DestinationType getDestinationType() { return destinationType; }
public void setDestinationType(DestinationType destinationType) { this.destinationType = destinationType != null ? destinationType : DestinationType.MEMORY; }
public String getDestinationTarget() { return destinationTarget; }
public void setDestinationTarget(String destinationTarget) { this.destinationTarget = destinationTarget; }
public boolean isFormFeedAtEoj() { return formFeedAtEoj; }
public void setFormFeedAtEoj(boolean formFeedAtEoj) { this.formFeedAtEoj = formFeedAtEoj; }
public boolean isAutoFlushOnEoj() { return autoFlushOnEoj; }
public void setAutoFlushOnEoj(boolean autoFlushOnEoj) { this.autoFlushOnEoj = autoFlushOnEoj; }
public boolean isAutoReconnect() { return autoReconnect; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
}
@@ -0,0 +1,118 @@
package haus.nightmare.lib3270j.printer;
/**
* Constants for IBM 3287 / 3286 Printer Sessions and SCS (SNA Character String) Protocol.
* Conforms to IBM Host On-Demand v14 (com.ibm.eNetwork.ECL.tn3270p.*) and RFC 2355.
*/
public final class PrinterConstants {
private PrinterConstants() {}
// ========== LU BIND Types ==========
public static final short LU_TYPE_UNKNOWN = 0;
public static final short LU_TYPE_1_SCS = 1;
public static final short LU_TYPE_3_DS = 3;
// ========== Device Type Names ==========
public static final String DEV_IBM_3287_1 = "IBM-3287-1";
public static final String DEV_IBM_3287_2 = "IBM-3287-2";
public static final String DEV_IBM_3286_2 = "IBM-3286-2";
public static final String DEV_IBM_387_1 = "IBM-387-1";
public static final String DEV_IBM_387_2 = "IBM-387-2";
// ========== SCS Single-Byte Control Codes (EBCDIC) ==========
public static final int SCS_NUL = 0x00; // Null
public static final int SCS_HT = 0x05; // Horizontal Tab
public static final int SCS_RNLS = 0x06; // Required New Line
public static final int SCS_RCR = 0x07; // Required Carriage Return
public static final int SCS_GE = 0x08; // Graphic Escape
public static final int SCS_VT = 0x0B; // Vertical Tab
public static final int SCS_FF = 0x0C; // Form Feed
public static final int SCS_CR = 0x0D; // Carriage Return
public static final int SCS_ENP = 0x14; // Enable Presentation
public static final int SCS_NL = 0x15; // New Line
public static final int SCS_BS = 0x16; // Backspace
public static final int SCS_POC = 0x17; // Program Operator Communication
public static final int SCS_INP = 0x24; // Inhibit Presentation
public static final int SCS_LF = 0x25; // Line Feed
public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm
public static final int SCS_TRS = 0x35; // Transparent Stream (0x35 <len> <bytes>)
public static final int SCS_NBS = 0x36; // Numeric Backspace
public static final int SCS_SP = 0x40; // Space
public static final int SCS_RSP = 0x41; // Required Space
// ========== SCS Multi-Byte Order Prefixes ==========
public static final int SCS_PREFIX_2B = 0x2B; // 2-byte SCS prefix
public static final int SCS_SA = 0x28; // Set Attribute (0x28 <type> <val>)
// 0x2B Sub-orders
public static final int SCS_SHF = 0xD1; // Set Horizontal Format
public static final int SCS_SVF = 0xD2; // Set Vertical Format
public static final int SCS_STO = 0xD3; // Set Text Orientation
public static final int SCS_SCS = 0xD4; // Select Character Set
public static final int SCS_SEAC = 0xD5; // Set Enhanced Attribute / Highlight
public static final int SCS_SLD = 0xD6; // Set Line Density
public static final int SCS_PPV = 0xC4; // Presentation Position Vertical
public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal)
// SA Attribute Types
public static final int SA_RESET = 0x00;
public static final int SA_HILITE = 0x41;
public static final int SA_COLOR = 0x42;
public static final int SA_CHARSET = 0x43;
// SEAC Highlight Values
public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_BLINK = 0xF1;
public static final int SEAC_REVERSE = 0xF2;
public static final int SEAC_UNDERLINE = 0xF4;
// PPA/PPV Positioning Types
public static final int POS_ABSOLUTE = 0x01; // Absolute position (1-based)
public static final int POS_RELATIVE = 0x02; // Relative offset
// ========== 3270 Print Data Stream Constants (LU3) ==========
public static final int WCC_START_PRINT_BIT = 0x08; // Start Print in 3270 WCC
public static final int WCC_PRINT_FORMAT_MASK = 0x03; // Formatting: 00=unformatted, 01=40 col, 10=64 col, 11=80 col
public static final int PRINT_FMT_UNFORMATTED = 0;
public static final int PRINT_FMT_40_COL = 1;
public static final int PRINT_FMT_64_COL = 2;
public static final int PRINT_FMT_80_COL = 3;
// 3270 Orders in Print Stream
public static final int ORDER_SBA = 0x11;
public static final int ORDER_SF = 0x1D;
public static final int ORDER_SFE = 0x29;
public static final int ORDER_SA = 0x28;
public static final int ORDER_MF = 0x2C;
public static final int ORDER_IC = 0x13;
public static final int ORDER_PT = 0x05;
public static final int ORDER_RA = 0x3C;
public static final int ORDER_EUA = 0x12;
public static final int ORDER_GE = 0x08;
public static final int ORDER_NL = 0x15;
public static final int ORDER_EM = 0x19; // End of Message
public static final int ORDER_FF = 0x0C;
public static final int ORDER_CR = 0x0D;
// ========== Printer Session Status Codes ==========
public static final int STATUS_CONNECTING = 650;
public static final int STATUS_NEGOTIATING = 651;
public static final int STATUS_CONNECTED = 652;
public static final int STATUS_SECURITY = 654;
public static final int STATUS_BIND_ERROR = 655;
public static final int STATUS_DISCONNECTED = 656;
public static final int STATUS_PRINTER_READY = 700;
public static final int STATUS_PRINTING = 701;
public static final int STATUS_PAGE_COMPLETE = 702;
public static final int STATUS_JOB_COMPLETE = 703;
public static final int STATUS_PRINTER_BUSY = 704;
public static final int STATUS_PRINTER_ERROR = 705;
public static final int STATUS_PRINTER_CLOSED = 706;
// Default Geometry
public static final int DEFAULT_MPP = 80; // Default line length
public static final int DEFAULT_MPL = 66; // Default page length (11 inches @ 6 LPI)
public static final int DEFAULT_CPI = 10; // 10 chars per inch
public static final int DEFAULT_LPI = 6; // 6 lines per inch
}
@@ -0,0 +1,765 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import haus.nightmare.lib3270j.tls.TlsTrustManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* TN3270E Printer Connection & Protocol Engine (Telnet3270EP).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.Telnet3270EP.
*/
public class Telnet3270EP implements Runnable {
private static final Logger log = Logger.getLogger(Telnet3270EP.class.getName());
private final PrinterConfig config;
private final PD3270 pd;
private final EbcdicTranslator translator;
private final PrintSCS3270 scs;
private final PrintPS3270 printPs;
private final List<PrintSessionListener> listeners = new CopyOnWriteArrayList<>();
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
private Thread readerThread;
private volatile boolean running = false;
private volatile boolean connected = false;
// Protocol State
private int statusCode = PrinterConstants.STATUS_DISCONNECTED;
private short activeLuType = PrinterConstants.LU_TYPE_UNKNOWN;
private String assignedLuName = null;
private String negotiatedDeviceType = null;
private boolean tn3270eMode = false;
private final boolean[] negotiatedFunctions = new boolean[10];
// Sequence tracking
private int sendSeqNumber = 0;
private int lastRecvSeqNumber = 0;
private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE;
// Buffer for Telnet streaming
private final ByteArrayOutputStream recordBuffer = new ByteArrayOutputStream();
private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream();
private boolean inSubnegotiation = false;
public Telnet3270EP(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.pd = new PD3270(this.config);
this.translator = new EbcdicTranslator(this.config.getCodePage());
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
this.printPs = new PrintPS3270(this.config, this.pd, this.translator);
}
public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
this.printPs = new PrintPS3270(this.config, this.pd, this.translator);
}
// ========== Connection Lifecycle (Fn #1) ==========
/**
* Open connection to TN3270E printer host matching open() specification.
*/
public synchronized boolean open() {
if (connected || running) {
close();
}
updateStatus(PrinterConstants.STATUS_CONNECTING, "Connecting to host " + config.getHost() + ":" + config.getPort());
try {
if (config.isUseTls()) {
SSLContext sslContext = SSLContext.getInstance(config.getSslProtocol() != null ? config.getSslProtocol() : "TLS");
TrustManager[] tm = new TrustManager[]{new TlsTrustManager(config.isTlsVerifyCert(), config.getCertificateVerifier())};
sslContext.init(null, tm, new SecureRandom());
SSLSocketFactory factory = sslContext.getSocketFactory();
SSLSocket sslSocket = (SSLSocket) factory.createSocket();
if (config.getConnectTimeoutMs() > 0) {
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()), config.getConnectTimeoutMs());
} else {
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()));
}
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
sslSocket.setKeepAlive(config.isSoKeepAlive());
if (config.getSoTimeoutMs() > 0) {
sslSocket.setSoTimeout(config.getSoTimeoutMs());
}
sslSocket.startHandshake();
this.socket = sslSocket;
} else {
Socket sock = new Socket();
if (config.getConnectTimeoutMs() > 0) {
sock.connect(new InetSocketAddress(config.getHost(), config.getPort()), config.getConnectTimeoutMs());
} else {
sock.connect(new InetSocketAddress(config.getHost(), config.getPort()));
}
sock.setTcpNoDelay(config.isTcpNoDelay());
sock.setKeepAlive(config.isSoKeepAlive());
if (config.getSoTimeoutMs() > 0) {
sock.setSoTimeout(config.getSoTimeoutMs());
}
this.socket = sock;
}
this.inputStream = new BufferedInputStream(socket.getInputStream());
this.outputStream = new BufferedOutputStream(socket.getOutputStream());
this.connected = true;
this.running = true;
reset();
updateStatus(PrinterConstants.STATUS_NEGOTIATING, "Connected, negotiating TN3270E");
readerThread = new Thread(this, "Telnet3270EP-Reader-" + config.getHost());
readerThread.setDaemon(true);
readerThread.start();
// Initiate Telnet negotiation: DO TN3270E
sendTelnetCommand(TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
return true;
} catch (Exception e) {
log.log(Level.SEVERE, "Failed to connect to printer host", e);
updateStatus(PrinterConstants.STATUS_PRINTER_ERROR, "Connection failed: " + e.getMessage());
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, e.getMessage());
close();
return false;
}
}
/**
* Close printer connection cleanly.
*/
public synchronized void close() {
running = false;
connected = false;
if (socket != null) {
try {
socket.close();
} catch (IOException ignored) {}
socket = null;
}
if (readerThread != null) {
readerThread.interrupt();
readerThread = null;
}
pd.closePrinter();
updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected");
}
/**
* Reset printer session state.
*/
public synchronized void reset() {
this.activeLuType = PrinterConstants.LU_TYPE_UNKNOWN;
this.assignedLuName = null;
this.negotiatedDeviceType = null;
this.tn3270eMode = false;
Arrays.fill(negotiatedFunctions, false);
this.sendSeqNumber = 0;
this.lastRecvSeqNumber = 0;
this.recordBuffer.reset();
this.subnegBuffer.reset();
this.inSubnegotiation = false;
scs.resetSCSFormatDefaults();
printPs.erasePrintBuffer();
}
/**
* Send raw data stream to host with IAC (0xFF) escaping.
*/
public synchronized void send(byte[] data, int length) {
if (!connected || outputStream == null || data == null || length <= 0) {
return;
}
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int i = 0; i < length && i < data.length; i++) {
int b = data[i] & 0xFF;
out.write(b);
if (b == TelnetConstants.IAC) {
out.write(TelnetConstants.IAC); // 0xFF escaping
}
}
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.log(Level.WARNING, "Error sending data to printer host", e);
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, "Send error: " + e.getMessage());
}
}
// ========== BIND, Ready & EOJ Protocol Handlers (Fn #2, #3, #4) ==========
/**
* Process SNA BIND image to determine printer LU type (LU1 SCS vs LU3 3270 DS).
* @param bindType Inferred or explicit bind type (LU_TYPE_1_SCS or LU_TYPE_3_DS).
*/
public synchronized void process_bind(short bindType) {
this.activeLuType = bindType;
log.info("Printer session BIND accepted, LU type = " +
(activeLuType == PrinterConstants.LU_TYPE_1_SCS ? "LU-1 (SCS)" : "LU-3 (3270 DS)"));
updateStatus(PrinterConstants.STATUS_PRINTER_READY, "Printer session bound as " +
(activeLuType == PrinterConstants.LU_TYPE_1_SCS ? "LU-1" : "LU-3"));
sendPrinterReady();
}
/**
* Parse SNA BIND image payload and extract LU type and parameters.
*/
public synchronized void processBindImage(byte[] bindData, int offset, int length) {
if (bindData == null || length < 14) {
process_bind(PrinterConstants.LU_TYPE_1_SCS);
return;
}
// In SNA BIND request:
// Byte 14 (profile byte / secondary LU type):
// 0x01 = LU Type 1 (SCS Printer)
// 0x03 = LU Type 3 (3270 Printer Data Stream)
int profile = bindData[offset + 14] & 0xFF;
short inferredLuType = (profile == 0x03) ? PrinterConstants.LU_TYPE_3_DS : PrinterConstants.LU_TYPE_1_SCS;
process_bind(inferredLuType);
}
/**
* Send TN3270E Positive Response indicating printer readiness (DEVICE-END).
*/
public synchronized void sendPrinterReady() {
sendTN3270EResponse(lastRecvSeqNumber, TN3270EConstants.RSF_POSITIVE_RESPONSE, TN3270EConstants.POS_DEVICE_END);
}
/**
* Handle End-Of-Job indicator from host.
* @param isComplete true if print job is complete.
*/
public synchronized void sendEOJ(boolean isComplete) {
log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete);
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
firePrintJobComplete(pd.getPageCount(), pd.getByteCount());
updateStatus(PrinterConstants.STATUS_JOB_COMPLETE, "Print job completed (" + pd.getPageCount() + " pages)");
// Send EOJ acknowledgment to host if required
if (lastResponseRequired == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(lastRecvSeqNumber);
}
}
// ========== TN3270E Responses & Packet Construction ==========
public synchronized void sendTN3270EResponse(int seqNumber, int respType, int respCode) {
if (!tn3270eMode || outputStream == null) return;
byte[] resp = new byte[TN3270EConstants.EH_SIZE + 1];
resp[0] = (byte) TN3270EConstants.DT_RESPONSE;
resp[1] = 0; // Request flag
resp[2] = (byte) respType;
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
resp[4] = (byte) (seqNumber & 0xFF);
resp[5] = (byte) respCode;
sendTN3270ERecord(resp);
}
public synchronized void sendTN3270EPositiveResponse(int seqNumber) {
sendTN3270EResponse(seqNumber, TN3270EConstants.RSF_POSITIVE_RESPONSE, TN3270EConstants.POS_DEVICE_END);
}
public synchronized void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
sendTN3270EResponse(seqNumber, TN3270EConstants.RSF_NEGATIVE_RESPONSE, negCode);
}
private synchronized void sendTN3270ERecord(byte[] record) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (byte b : record) {
int ub = b & 0xFF;
out.write(ub);
if (ub == TelnetConstants.IAC) {
out.write(TelnetConstants.IAC);
}
}
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.EOR);
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.log(Level.WARNING, "Error sending TN3270E record", e);
}
}
private synchronized void sendTelnetCommand(int command, int option) throws IOException {
outputStream.write(new byte[]{(byte) TelnetConstants.IAC, (byte) command, (byte) option});
outputStream.flush();
}
// ========== Telnet & TN3270E Protocol I/O Loop ==========
@Override
public void run() {
byte[] buf = new byte[8192];
try {
while (running && connected) {
int read = inputStream.read(buf);
if (read < 0) {
log.info("Host closed socket connection");
break;
}
processIncomingBytes(buf, 0, read);
}
} catch (IOException e) {
if (running) {
log.log(Level.WARNING, "Connection exception in printer reader thread", e);
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, e.getMessage());
}
} finally {
close();
}
}
private void processIncomingBytes(byte[] data, int offset, int length) {
int idx = offset;
int end = offset + length;
while (idx < end) {
int b = data[idx++] & 0xFF;
if (inSubnegotiation) {
if (b == TelnetConstants.IAC) {
if (idx < end && (data[idx] & 0xFF) == TelnetConstants.SE) {
idx++;
inSubnegotiation = false;
processSubnegotiation(subnegBuffer.toByteArray());
subnegBuffer.reset();
} else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) {
subnegBuffer.write(TelnetConstants.IAC);
idx++;
}
} else {
subnegBuffer.write(b);
}
continue;
}
if (b == TelnetConstants.IAC) {
if (idx >= end) break;
int cmd = data[idx++] & 0xFF;
switch (cmd) {
case TelnetConstants.IAC:
recordBuffer.write(TelnetConstants.IAC);
break;
case TelnetConstants.SB:
inSubnegotiation = true;
subnegBuffer.reset();
break;
case TelnetConstants.EOR:
processCompleteRecord(recordBuffer.toByteArray());
recordBuffer.reset();
break;
case TelnetConstants.DO:
if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
break;
case TelnetConstants.DONT:
if (idx < end) handleTelnetDont(data[idx++] & 0xFF);
break;
case TelnetConstants.WILL:
if (idx < end) handleTelnetWill(data[idx++] & 0xFF);
break;
case TelnetConstants.WONT:
if (idx < end) handleTelnetWont(data[idx++] & 0xFF);
break;
default:
break;
}
} else {
recordBuffer.write(b);
}
}
}
private void handleTelnetDo(int option) {
try {
if (option == TelnetConstants.TELOPT_TN3270E) {
sendTelnetCommand(TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
tn3270eMode = true;
} else if (option == TelnetConstants.TELOPT_BINARY || option == TelnetConstants.TELOPT_EOR) {
sendTelnetCommand(TelnetConstants.WILL, option);
} else {
sendTelnetCommand(TelnetConstants.WONT, option);
}
} catch (IOException e) {
log.warning("Error responding to Telnet DO: " + e.getMessage());
}
}
private void handleTelnetDont(int option) {
try {
sendTelnetCommand(TelnetConstants.WONT, option);
} catch (IOException ignored) {}
}
private void handleTelnetWill(int option) {
try {
if (option == TelnetConstants.TELOPT_TN3270E) {
sendTelnetCommand(TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
tn3270eMode = true;
} else if (option == TelnetConstants.TELOPT_BINARY || option == TelnetConstants.TELOPT_EOR) {
sendTelnetCommand(TelnetConstants.DO, option);
} else {
sendTelnetCommand(TelnetConstants.DONT, option);
}
} catch (IOException e) {
log.warning("Error responding to Telnet WILL: " + e.getMessage());
}
}
private void handleTelnetWont(int option) {
try {
sendTelnetCommand(TelnetConstants.DONT, option);
} catch (IOException ignored) {}
}
private void processSubnegotiation(byte[] sb) {
if (sb.length == 0) return;
int opt = sb[0] & 0xFF;
if (opt == TelnetConstants.TELOPT_TN3270E && sb.length > 1) {
int op = sb[1] & 0xFF;
switch (op) {
case TN3270EConstants.OP_SEND:
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_DEVICE_TYPE) {
sendDeviceTypeRequest();
}
break;
case TN3270EConstants.OP_DEVICE_TYPE:
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_IS) {
parseDeviceTypeIs(sb);
sendFunctionsRequest();
}
break;
case TN3270EConstants.OP_FUNCTIONS:
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_IS) {
parseFunctionsIs(sb);
} else if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_REQUEST) {
parseFunctionsRequest(sb);
}
break;
default:
break;
}
}
}
private void sendDeviceTypeRequest() {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SB);
out.write(TelnetConstants.TELOPT_TN3270E);
out.write(TN3270EConstants.OP_DEVICE_TYPE);
out.write(TN3270EConstants.OP_REQUEST);
String devType = config.getDeviceType() != null ? config.getDeviceType() : PrinterConstants.DEV_IBM_3287_1;
out.write(devType.getBytes(StandardCharsets.US_ASCII));
if (config.getAssociatedDisplayLuName() != null && !config.getAssociatedDisplayLuName().trim().isEmpty()) {
out.write(TN3270EConstants.OP_ASSOCIATE);
out.write(config.getAssociatedDisplayLuName().trim().getBytes(StandardCharsets.US_ASCII));
} else if (config.getPrinterLuName() != null && !config.getPrinterLuName().trim().isEmpty()) {
out.write(TN3270EConstants.OP_CONNECT);
out.write(config.getPrinterLuName().trim().getBytes(StandardCharsets.US_ASCII));
}
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SE);
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.warning("Error sending TN3270E device type request: " + e.getMessage());
}
}
private void parseDeviceTypeIs(byte[] sb) {
// SB TN3270E DEVICE-TYPE IS <devtype> [CONNECT|ASSOCIATE <luname>]
int idx = 3;
ByteArrayOutputStream devOut = new ByteArrayOutputStream();
while (idx < sb.length && (sb[idx] & 0xFF) != TN3270EConstants.OP_CONNECT && (sb[idx] & 0xFF) != TN3270EConstants.OP_ASSOCIATE) {
devOut.write(sb[idx++]);
}
negotiatedDeviceType = new String(devOut.toByteArray(), StandardCharsets.US_ASCII).trim();
if (idx < sb.length) {
idx++; // skip CONNECT / ASSOCIATE
ByteArrayOutputStream luOut = new ByteArrayOutputStream();
while (idx < sb.length) {
luOut.write(sb[idx++]);
}
assignedLuName = new String(luOut.toByteArray(), StandardCharsets.US_ASCII).trim();
}
log.info("TN3270E device negotiated: " + negotiatedDeviceType + ", LU=" + assignedLuName);
}
private void sendFunctionsRequest() {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SB);
out.write(TelnetConstants.TELOPT_TN3270E);
out.write(TN3270EConstants.OP_FUNCTIONS);
out.write(TN3270EConstants.OP_REQUEST);
// Request BIND-IMAGE, RESPONSES, SCS-CTL-CODES
out.write(TN3270EConstants.FUNC_BIND_IMAGE);
out.write(TN3270EConstants.FUNC_RESPONSES);
out.write(TN3270EConstants.FUNC_SCS_CTL_CODES);
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SE);
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.warning("Error sending TN3270E functions request: " + e.getMessage());
}
}
private void parseFunctionsIs(byte[] sb) {
Arrays.fill(negotiatedFunctions, false);
for (int i = 3; i < sb.length; i++) {
int fn = sb[i] & 0xFF;
if (fn < negotiatedFunctions.length) {
negotiatedFunctions[fn] = true;
}
}
updateStatus(PrinterConstants.STATUS_CONNECTED, "Connected and negotiated with host");
}
private void parseFunctionsRequest(byte[] sb) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SB);
out.write(TelnetConstants.TELOPT_TN3270E);
out.write(TN3270EConstants.OP_FUNCTIONS);
out.write(TN3270EConstants.OP_IS);
Arrays.fill(negotiatedFunctions, false);
for (int i = 3; i < sb.length; i++) {
int fn = sb[i] & 0xFF;
if (fn == TN3270EConstants.FUNC_BIND_IMAGE || fn == TN3270EConstants.FUNC_RESPONSES || fn == TN3270EConstants.FUNC_SCS_CTL_CODES) {
negotiatedFunctions[fn] = true;
out.write(fn);
}
}
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SE);
outputStream.write(out.toByteArray());
outputStream.flush();
updateStatus(PrinterConstants.STATUS_CONNECTED, "Connected and negotiated with host");
} catch (IOException e) {
log.warning("Error replying to TN3270E functions request: " + e.getMessage());
}
}
// ========== TN3270E Record Dispatching ==========
private void processCompleteRecord(byte[] record) {
if (record == null || record.length == 0) return;
if (tn3270eMode) {
if (record.length < TN3270EConstants.EH_SIZE) {
return;
}
int dataType = record[0] & 0xFF;
int reqFlag = record[1] & 0xFF;
int respFlag = record[2] & 0xFF;
int seqNum = ((record[3] & 0xFF) << 8) | (record[4] & 0xFF);
this.lastRecvSeqNumber = seqNum;
this.lastResponseRequired = respFlag;
firePrintJobData(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
switch (dataType) {
case TN3270EConstants.DT_SCS_DATA:
updateStatus(PrinterConstants.STATUS_PRINTING, "Processing SCS print stream");
scs.processHostData(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNum);
}
break;
case TN3270EConstants.DT_3270_DATA:
updateStatus(PrinterConstants.STATUS_PRINTING, "Processing 3270 printer data stream");
printPs.process3270PrintDS(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNum);
}
break;
case TN3270EConstants.DT_BIND_IMAGE:
processBindImage(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
break;
case TN3270EConstants.DT_UNBIND:
log.info("Received TN3270E UNBIND");
reset();
updateStatus(PrinterConstants.STATUS_CONNECTED, "Session unbound");
break;
case TN3270EConstants.DT_PRINT_EOJ:
sendEOJ(true);
break;
case TN3270EConstants.DT_REQUEST:
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNum);
}
break;
case TN3270EConstants.DT_RESPONSE:
log.fine("Received response, seq=" + seqNum);
break;
default:
// Fallback to active LU type
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
printPs.process3270PrintDS(record, 0, record.length);
} else {
scs.processHostData(record, 0, record.length);
}
break;
}
} else {
// Non-TN3270E mode
firePrintJobData(record, 0, record.length);
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
printPs.process3270PrintDS(record, 0, record.length);
} else {
scs.processHostData(record, 0, record.length);
}
}
}
// ========== Event Dispatching & Listeners (Fn #51 - #58) ==========
public void addPrintListener(PrintSessionListener listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public void removePrintListener(PrintSessionListener listener) {
listeners.remove(listener);
}
public void firePrintJobStarted(String jobName) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_STARTED, jobName,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), statusCode, "Print job started", null);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobStarted(event); } catch (Exception ignored) {}
}
}
public void firePrintJobData(byte[] data, int offset, int length) {
byte[] copy = null;
if (data != null && length > 0) {
copy = new byte[length];
System.arraycopy(data, offset, copy, 0, length);
}
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_DATA, null,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), statusCode, "Print data", copy);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobData(event); } catch (Exception ignored) {}
}
}
public void firePrintJobPageComplete(int pageNumber) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_PAGE_COMPLETE, null,
pageNumber, pd.getByteCount(), pageNumber, statusCode, "Page complete", null);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobPageComplete(event); } catch (Exception ignored) {}
}
}
public void firePrintJobComplete(int totalPages, long totalBytes) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_COMPLETE, null,
totalPages, totalBytes, totalPages, statusCode, "Print job complete", null);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobComplete(event); } catch (Exception ignored) {}
}
}
public void firePrinterStatusChanged(int code, String message) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_STATUS_CHANGED, null,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), code, message, null);
for (PrintSessionListener l : listeners) {
try { l.onPrinterStatusChanged(event); } catch (Exception ignored) {}
}
}
public void firePrinterError(int errorCode, String errorMessage) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_ERROR, null,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), errorCode, errorMessage, null);
for (PrintSessionListener l : listeners) {
try { l.onPrinterError(event); } catch (Exception ignored) {}
}
}
private void updateStatus(int newCode, String msg) {
this.statusCode = newCode;
firePrinterStatusChanged(newCode, msg);
}
// ========== Accessors ==========
public boolean isConnected() { return connected; }
public int getStatusCode() { return statusCode; }
public short getActiveLuType() { return activeLuType; }
public String getAssignedLuName() { return assignedLuName; }
public String getNegotiatedDeviceType() { return negotiatedDeviceType; }
public PrinterConfig getConfig() { return config; }
public PD3270 getPD() { return pd; }
public EbcdicTranslator getTranslator() { return translator; }
public PrintSCS3270 getSCS() { return scs; }
public PrintPS3270 getPrintPS() { return printPs; }
}
@@ -0,0 +1,101 @@
package haus.nightmare.lib3270j.printer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* High-level API Client for managing 3270/3287 Printer Sessions.
* Coordinates Telnet3270EP protocol engine, SCS/LU3 interpreters, and PD3270 print spoolers.
*/
public class Telnet3270EPClient {
private final PrinterConfig config;
private final Telnet3270EP protocolEngine;
public Telnet3270EPClient(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.protocolEngine = new Telnet3270EP(this.config);
}
public Telnet3270EPClient(String host, int port, String printerLuName) {
this(new PrinterConfig(host, port, printerLuName));
}
/**
* Connect to printer host asynchronously.
*/
public boolean connect() {
return protocolEngine.open();
}
/**
* Disconnect from printer host.
*/
public void disconnect() {
protocolEngine.close();
}
public boolean isConnected() {
return protocolEngine.isConnected();
}
public int getStatusCode() {
return protocolEngine.getStatusCode();
}
public void addPrintListener(PrintSessionListener listener) {
protocolEngine.addPrintListener(listener);
}
public void removePrintListener(PrintSessionListener listener) {
protocolEngine.removePrintListener(listener);
}
public PrinterConfig getConfig() {
return config;
}
public Telnet3270EP getProtocolEngine() {
return protocolEngine;
}
public PD3270 getPD() {
return protocolEngine.getPD();
}
public PrintSCS3270 getSCS() {
return protocolEngine.getSCS();
}
public PrintPS3270 getPrintPS() {
return protocolEngine.getPrintPS();
}
/**
* Print raw data stream directly through active printer interpreter.
*/
public void printDirectBytes(byte[] data, int offset, int length) {
if (protocolEngine.getActiveLuType() == PrinterConstants.LU_TYPE_3_DS) {
protocolEngine.getPrintPS().process3270PrintDS(data, offset, length);
} else {
protocolEngine.getSCS().processHostData(data, offset, length);
}
}
/**
* Print local file through active printer destination.
*/
public void printFile(File file) throws IOException {
if (file == null || !file.exists()) {
throw new IllegalArgumentException("File not found: " + file);
}
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buf = new byte[8192];
int r;
while ((r = fis.read(buf)) > 0) {
protocolEngine.getPD().writePrintBytes(buf, 0, r);
}
}
}
}
@@ -19,6 +19,12 @@ public final class DS3270Constants {
public static final int CMD_EAU = 0x0f; // Erase All Unprotected
public static final int CMD_WSF = 0x11; // Write Structured Field
// Command aliases
public static final int CMD_WRITE = CMD_W;
public static final int CMD_ERASE_WRITE = CMD_EW;
public static final int CMD_ERASE_WRITE_ALT = CMD_EWA;
public static final int CMD_ERASE_ALL_UNPROTECTED = CMD_EAU;
// SNA 3270 Commands
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
@@ -195,6 +201,9 @@ public final class DS3270Constants {
public static final int SF_SRM_XFIELD = 0x01;
public static final int SF_SRM_CHAR = 0x02;
public static final int SF_CREATE_PART = 0x0c;
public static final int SF_DESTROY_PART = 0x0d;
public static final int SF_ACTIVATE_PART = 0x0e;
public static final int SF_MODIFY_PART = 0x0f;
public static final int SF_OUTBOUND_DS = 0x40;
public static final int SF_TRANSFER_DATA = 0xd0;
@@ -367,19 +376,57 @@ public final class DS3270Constants {
}
}
/** Get human-readable command name. */
private static final byte[] X2BIN_TABLE = new byte[256];
static {
java.util.Arrays.fill(X2BIN_TABLE, (byte) -1);
for (int i = 0; i < CODE_TABLE.length; i++) {
X2BIN_TABLE[CODE_TABLE[i] & 0xFF] = (byte) i;
}
}
/**
* Fast lookup for 6-bit 3270 buffer address decoding.
* Maps an EBCDIC buffer address character byte to its 6-bit binary value (0..63).
*/
public static int x2bin(int ebcByte) {
int idx = ebcByte & 0xFF;
byte val = X2BIN_TABLE[idx];
return val >= 0 ? (val & 0x3F) : (idx & 0x3F);
}
/**
* Translate 3270 command codes to EBCDIC byte values.
*/
public static int cmd2ebc(int commandCode) {
switch (commandCode) {
case CMD_W: case SNA_CMD_W: return SNA_CMD_W; // 0xF1
case CMD_RB: case SNA_CMD_RB: return SNA_CMD_RB; // 0xF2
case CMD_WSF: case SNA_CMD_WSF: return SNA_CMD_WSF; // 0xF3
case CMD_EW: case SNA_CMD_EW: return SNA_CMD_EW; // 0xF5
case CMD_RM: case SNA_CMD_RM: return SNA_CMD_RM; // 0xF6
case CMD_RMA: case SNA_CMD_RMA: return SNA_CMD_RMA; // 0x6E
case CMD_EAU: case SNA_CMD_EAU: return SNA_CMD_EAU; // 0x6F
case CMD_EWA: case SNA_CMD_EWA: return SNA_CMD_EWA; // 0x7E
case CMD_NOP: return CMD_NOP; // 0x03
default: return commandCode & 0xFF;
}
}
/**
* Get descriptive name of a 3270 command code.
*/
public static String commandName(int cmd) {
switch (cmd) {
case CMD_W: case SNA_CMD_W: return "Write";
case CMD_EW: case SNA_CMD_EW: return "EraseWrite";
case CMD_EWA: case SNA_CMD_EWA: return "EraseWriteAlternate";
case CMD_RB: case SNA_CMD_RB: return "ReadBuffer";
case CMD_RM: case SNA_CMD_RM: return "ReadModified";
case CMD_RMA: case SNA_CMD_RMA: return "ReadModifiedAll";
case CMD_EAU: case SNA_CMD_EAU: return "EraseAllUnprotected";
case CMD_WSF: case SNA_CMD_WSF: return "WriteStructuredField";
case CMD_NOP: return "NoOp";
default: return String.format("Unknown(0x%02x)", cmd);
case CMD_W: case SNA_CMD_W: return "Write";
case CMD_RB: case SNA_CMD_RB: return "Read Buffer";
case CMD_WSF: case SNA_CMD_WSF: return "Write Structured Field";
case CMD_EW: case SNA_CMD_EW: return "Erase/Write";
case CMD_RM: case SNA_CMD_RM: return "Read Modified";
case CMD_RMA: case SNA_CMD_RMA: return "Read Modified All";
case CMD_EAU: case SNA_CMD_EAU: return "Erase All Unprotected";
case CMD_EWA: case SNA_CMD_EWA: return "Erase/Write Alternate";
case CMD_NOP: return "NOP";
default: return "0x" + Integer.toHexString(cmd);
}
}
}
@@ -91,6 +91,25 @@ public final class TN3270EConstants {
public static final int UNBIND_CLEANUP = 0x0f;
public static final int UNBIND_BAD_SENSE = 0xfe;
// SNA Data Flow Control (DFC) Commands
public static final int DFC_CLEAR = 0x01;
public static final int DFC_CANCEL = 0x02;
public static final int DFC_RQR = 0x03;
public static final int DFC_STSN = 0x04;
public static final int DFC_SIGNAL = 0x05;
public static final int DFC_LUSTAT = 0x06;
public static final int DFC_BID = 0x07;
public static final int DFC_SHUTC = 0x08;
public static final int DFC_SHUTD = 0x09;
// IBM Host On-Demand Status Codes
public static final int STATUS_CONNECTING = 650;
public static final int STATUS_NEGOTIATING = 651;
public static final int STATUS_CONNECTED = 652;
public static final int STATUS_SECURITY = 654;
public static final int STATUS_BIND_ERROR = 655;
public static final int STATUS_DISCONNECTED = 656;
// Name lookups for tracing
private static final String[] REASON_NAMES = {
@@ -128,6 +147,38 @@ public final class TN3270EConstants {
return code >= 0 && code < HRSP_FLAG_NAMES.length ? HRSP_FLAG_NAMES[code] : "??";
}
public static String unbindReasonName(int code) {
switch (code) {
case UNBIND_NORMAL: return "NORMAL";
case UNBIND_BIND_FORTHCOMING: return "BIND-FORTHCOMING";
case UNBIND_VR_INOPERATIVE: return "VR-INOPERATIVE";
case UNBIND_RX_INOPERATIVE: return "RX-INOPERATIVE";
case UNBIND_HRESET: return "HRESET";
case UNBIND_SSCP_GONE: return "SSCP-GONE";
case UNBIND_VR_DEACTIVATED: return "VR-DEACTIVATED";
case UNBIND_LU_FAILURE_PERM: return "LU-FAILURE-PERM";
case UNBIND_LU_FAILURE_TEMP: return "LU-FAILURE-TEMP";
case UNBIND_CLEANUP: return "CLEANUP";
case UNBIND_BAD_SENSE: return "BAD-SENSE";
default: return "UNKNOWN-UNBIND(0x" + Integer.toHexString(code) + ")";
}
}
public static String dfcCommandName(int code) {
switch (code) {
case DFC_CLEAR: return "CLEAR";
case DFC_CANCEL: return "CANCEL";
case DFC_RQR: return "RQR";
case DFC_STSN: return "STSN";
case DFC_SIGNAL: return "SIGNAL";
case DFC_LUSTAT: return "LUSTAT";
case DFC_BID: return "BID";
case DFC_SHUTC: return "SHUTC";
case DFC_SHUTD: return "SHUTD";
default: return "UNKNOWN-DFC(0x" + Integer.toHexString(code) + ")";
}
}
/** Format a function set as a human-readable string. */
public static String functionNames(boolean[] funcs) {
StringBuilder sb = new StringBuilder();
@@ -33,6 +33,15 @@ public class ExtendedAttribute {
/** DBCS state. */
public byte db;
/** Outlining attribute (box borders/grid lines). */
public byte ol;
/** Validation attribute. */
public byte vl;
/** Transparency attribute. */
public byte tr;
/**
* Unicode character for display (set by translation from ec, or directly in NVT mode).
*/
@@ -48,6 +57,9 @@ public class ExtendedAttribute {
cs = 0;
ic = 0;
db = 0;
ol = 0;
vl = 0;
tr = 0;
ucs4 = 0;
}
@@ -61,6 +73,9 @@ public class ExtendedAttribute {
this.cs = other.cs;
this.ic = other.ic;
this.db = other.db;
this.ol = other.ol;
this.vl = other.vl;
this.tr = other.tr;
this.ucs4 = other.ucs4;
}
@@ -46,6 +46,10 @@ public class ScreenBuffer {
return renderLock;
}
public EbcdicTranslator getTranslator() {
return translator;
}
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
this.translator = translator;
this.defRows = MODEL_2_ROWS;
@@ -156,6 +160,69 @@ public class ScreenBuffer {
updateDisplaySnapshot();
}
/**
* Configure screen dimensions matching SNA BIND parameters.
*/
public synchronized void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
if (primaryRows > 0) this.defRows = primaryRows;
if (primaryCols > 0) this.defCols = primaryCols;
if (altRows > 0) this.altRows = altRows;
if (altCols > 0) this.altCols = altCols;
this.maxRows = Math.max(defRows, this.altRows);
this.maxCols = Math.max(defCols, this.altCols);
allocateBuffers();
updateDisplaySnapshot();
}
/**
* Switch between default (primary) and alternate screen sizes.
*/
public synchronized void setScrSizetoDefault(boolean isDefault) {
erase(!isDefault);
}
// ========== Partitions ==========
private final java.util.Map<Integer, PartitionInfo> partitions = new java.util.HashMap<>();
public static class PartitionInfo {
public final int pid;
public final int rows;
public final int cols;
public PartitionInfo(int pid, int rows, int cols) {
this.pid = pid;
this.rows = rows;
this.cols = cols;
}
}
public synchronized void createPartition(int pid, int pRows, int pCols) {
partitions.put(pid, new PartitionInfo(pid, pRows, pCols));
this.activePartition = pid;
this.explicitPartitionActive = true;
}
public synchronized void destroyPartition(int pid) {
partitions.remove(pid);
if (this.activePartition == pid) {
this.activePartition = 0;
this.explicitPartitionActive = !partitions.isEmpty();
}
}
public synchronized void activatePartition(int pid) {
this.activePartition = pid;
this.explicitPartitionActive = (pid != 0);
}
public synchronized void eraseReset(boolean alt) {
partitions.clear();
this.activePartition = 0;
this.explicitPartitionActive = false;
erase(alt);
}
// ========== Cursor ==========
public int getCursorAddress() { return cursorAddress; }
public synchronized void setCursorAddress(int addr) {
@@ -172,7 +239,7 @@ public class ScreenBuffer {
public void setReplyMode(byte mode) { this.replyMode = mode; }
public int getActivePartition() { return activePartition; }
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = true; }
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = (pid != 0); }
public boolean isExplicitPartitionActive() { return explicitPartitionActive; }
// ========== Screen erase ==========
@@ -50,8 +50,11 @@ public class TelnetConnection {
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
sslSocket.setKeepAlive(true);
sslSocket.setTcpNoDelay(true);
sslSocket.setKeepAlive(config.isSoKeepAlive());
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
if (config.getSoTimeoutMs() > 0) {
sslSocket.setSoTimeout(config.getSoTimeoutMs());
}
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
sslSocket.startHandshake();
@@ -67,9 +70,12 @@ public class TelnetConnection {
} else {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setKeepAlive(config.isSoKeepAlive());
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.setTcpNoDelay(config.isTcpNoDelay());
if (config.getSoTimeoutMs() > 0) {
socket.setSoTimeout(config.getSoTimeoutMs());
}
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
}
@@ -104,6 +110,24 @@ public class TelnetConnection {
}
}
/**
* Send payload with automatic doubling of 0xFF (IAC escaping).
*/
public synchronized void sendEscaped(byte[] data, int offset, int length) throws IOException {
if (outputStream == null) return;
ByteArrayOutputStream out = new ByteArrayOutputStream(length + 16);
for (int i = offset; i < offset + length; i++) {
int b = data[i] & 0xFF;
out.write(b);
if (b == IAC) {
out.write(IAC);
}
}
byte[] escaped = out.toByteArray();
outputStream.write(escaped, 0, escaped.length);
outputStream.flush();
}
/**
* Disconnect from the host.
*/
@@ -9,6 +9,7 @@ import haus.nightmare.lib3270j.listener.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
@@ -57,10 +58,15 @@ public class TelnetFSM {
private boolean tn3270eBound;
private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions
private int eXmitSeq;
private int lastRcvSeq;
private short lastRespType;
private short lastRespCode;
private int responseRequired = RSF_NO_RESPONSE;
private boolean deferredWillTtype;
private boolean tn3270eDeviceTypeSent;
private int ttypeIndex = 0;
private int luIndex = 0;
private final java.util.Map<String, List<String>> devicePools = new java.util.HashMap<>();
private List<String> getCandidateTerminalTypes() {
List<String> list = new ArrayList<>();
@@ -68,6 +74,9 @@ public class TelnetFSM {
list.add(config.getTerminalName().trim());
return list;
}
if (config.isDynamicModel()) {
list.add(config.isExtendedDataStream() ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC");
}
TerminalModel model = config.getModel();
list.add(model.getTerminalType());
list.add(model.getBaseTerminalType());
@@ -92,6 +101,7 @@ public class TelnetFSM {
private final ConnectionConfig config;
private final ScreenBuffer screenBuffer;
private final DataStreamProcessor dsProcessor;
private final haus.nightmare.lib3270j.nvt.NvtProcessor nvtProcessor;
private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
// Listeners
@@ -108,6 +118,12 @@ public class TelnetFSM {
this.config = config;
this.screenBuffer = screenBuffer;
this.dsProcessor = dsProcessor;
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
this.nvtProcessor.setOutputSender(this::sendBytes);
}
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
return nvtProcessor;
}
public void setConnection(TelnetConnection connection) {
@@ -115,7 +131,10 @@ public class TelnetFSM {
}
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
public void addScreenUpdateListener(ScreenUpdateListener l) { screenListeners.add(l); }
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
nvtProcessor.addScreenUpdateListener(l);
}
public ConnectionState getConnectionState() { return connectionState; }
public boolean[] getMyOpts() { return myOpts; }
@@ -135,8 +154,10 @@ public class TelnetFSM {
tn3270eSubmode = TN3270ESubmode.UNBOUND;
tn3270eBound = false;
eXmitSeq = 0;
lastRcvSeq = 0;
deferredWillTtype = false;
ttypeIndex = 0;
luIndex = 0;
ibuf.reset();
sbbuf.reset();
@@ -148,6 +169,7 @@ public class TelnetFSM {
eFuncs[FUNC_DATA_STREAM_CTL] = true;
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
statusDisplay(STATUS_CONNECTING, "Connecting to host");
changeState(ConnectionState.TELNET_PENDING);
}
@@ -167,7 +189,9 @@ public class TelnetFSM {
if (connectionState == ConnectionState.TELNET_PENDING) {
changeState(ConnectionState.CONNECTED_NVT);
}
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
if (connectionState.isNvt()) {
nvtProcessor.processNVTData(buf, start, i - start);
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
ibuf.write(buf, start, i - start);
}
}
@@ -236,11 +260,15 @@ public class TelnetFSM {
changeState(ConnectionState.CONNECTED_NVT);
}
if (connectionState.isNvt()) {
nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1);
return;
}
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
ibuf.write(c);
}
// NVT data would go to NVT processor (not implemented in initial version)
}
// ========== TNS_IAC ==========
@@ -431,7 +459,9 @@ public class TelnetFSM {
private void processSBIAC(int c) {
if (c == SE) {
// Sub-negotiation complete
processSubNegotiation(sbbuf.toByteArray());
byte[] sbData = sbbuf.toByteArray();
sbbuf.reset();
processSubNegotiation(sbData);
state = TNS_DATA;
} else if (c == IAC) {
// Escaped IAC within sub-negotiation
@@ -440,6 +470,7 @@ public class TelnetFSM {
} else {
// Shouldn't happen, but recover
log.warning("Unexpected byte " + c + " after IAC in SB");
sbbuf.reset();
state = TNS_DATA;
}
}
@@ -548,7 +579,16 @@ public class TelnetFSM {
}
private void sendTN3270EDeviceTypeRequest() {
String termType = config.getEffectiveTerminalType();
List<String> candidates = getCandidateTerminalTypes();
String termType = candidates.get(Math.min(ttypeIndex, candidates.size() - 1));
String currentLu = null;
List<String> lus = config.getLuNames();
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
currentLu = lus.get(luIndex);
} else if (config.getLuName() != null && !config.getLuName().isEmpty()) {
currentLu = config.getLuName();
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
@@ -559,9 +599,9 @@ public class TelnetFSM {
out.write((byte) ch);
}
// Add LU name if specified
if (config.getLuName() != null && !config.getLuName().isEmpty()) {
if (currentLu != null && !currentLu.isEmpty()) {
out.write(OP_CONNECT);
for (char ch : config.getLuName().toCharArray()) {
for (char ch : currentLu.toCharArray()) {
out.write((byte) ch);
}
}
@@ -569,7 +609,7 @@ public class TelnetFSM {
out.write(SE);
sendBytes(out.toByteArray());
log.warning(">>> SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
(config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE");
(currentLu != null ? " CONNECT " + currentLu : "") + " SE");
}
private void handleTN3270EDeviceType(byte[] data) {
@@ -584,23 +624,37 @@ public class TelnetFSM {
// Check if REJECT
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
pos++;
int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ;
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
// Try fallback model 2 if we were requesting something else
if (config.getModel() != TerminalModel.IBM_3278_2 &&
config.getModel() != TerminalModel.IBM_3279_4) {
log.warning("TN3270E device-type rejected (" +
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
config.setModel(TerminalModel.IBM_3278_2);
sendTN3270EDeviceTypeRequest();
return;
}
int reason = REASON_UNSUPPORTED_REQ;
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
pos++;
}
if (pos < data.length) {
reason = data[pos] & 0xFF;
}
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
// 1. Try next LU in pool if available
List<String> lus = config.getLuNames();
if (lus != null && luIndex + 1 < lus.size()) {
luIndex++;
log.info("Retrying TN3270E device-type with next LU: " + lus.get(luIndex));
sendTN3270EDeviceTypeRequest();
return;
}
// 2. Try fallback terminal type candidate if available
List<String> candidates = getCandidateTerminalTypes();
if (ttypeIndex + 1 < candidates.size() - 1) { // exclude UNKNOWN
ttypeIndex++;
log.info("Retrying TN3270E device-type with next candidate: " + candidates.get(ttypeIndex));
sendTN3270EDeviceTypeRequest();
return;
}
// Fall back to plain TN3270
myOpts[TELOPT_TN3270E] = false;
hisOpts[TELOPT_TN3270E] = false;
sendCommand(WONT, TELOPT_TN3270E);
// Send deferred WILL TTYPE if needed
if (deferredWillTtype) {
@@ -832,102 +886,21 @@ public class TelnetFSM {
break;
case DT_BIND_IMAGE:
tn3270eBound = true;
// Parse BIND image for screen dimensions (SNA BIND format)
{
int bindLen = data.length - EH_SIZE;
StringBuilder bindHex = new StringBuilder();
for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) {
bindHex.append(String.format("%02x ", data[bi] & 0xFF));
}
log.info("Received BIND image (" + bindLen + " bytes)" +
" responseFlag=" + responseFlag + " raw: " + bindHex.toString().trim());
// SNA BIND RU offsets (from 3270ds.h):
// Byte 20 = RD (default rows), Byte 21 = CD (default cols)
// Byte 22 = RA (alternate rows), Byte 23 = CA (alternate cols)
// Byte 24 = SSIZE (screen size indicator)
final int BIND_OFF_RD = 20, BIND_OFF_CD = 21;
final int BIND_OFF_RA = 22, BIND_OFF_CA = 23;
final int BIND_OFF_SSIZE = 24;
if (bindLen > BIND_OFF_SSIZE) {
int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF;
int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF;
int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF;
int bindRa, bindCa;
switch (ssize) {
case 0x00: case 0x02:
// Default model 2 dimensions for both default and alt
bindRd = 24; bindCd = 80;
bindRa = 24; bindCa = 80;
break;
case 0x03:
// Default = 24x80, alternate = configured model max
bindRd = 24; bindCd = 80;
bindRa = screenBuffer.getMaxRows();
bindCa = screenBuffer.getMaxCols();
break;
case 0x7E:
// Both default and alternate = specified values
bindRa = bindRd; bindCa = bindCd;
break;
case 0x7F:
// Default and alternate are both specified separately
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
break;
default:
// Unknown SSIZE - use model defaults
bindRa = screenBuffer.getMaxRows();
bindCa = screenBuffer.getMaxCols();
break;
}
log.info("BIND SSIZE=0x" + String.format("%02x", ssize) +
" default=" + bindRd + "x" + bindCd +
" alt=" + bindRa + "x" + bindCa);
// Apply dimensions constrain to model max
int maxR = screenBuffer.getMaxRows();
int maxC = screenBuffer.getMaxCols();
if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) {
screenBuffer.setAlternateDimensions(bindRa, bindCa);
}
}
}
// Clear and reset screen for new session
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
notifyScreenUpdate();
// Send positive response if required (critical for ISPF NEWAPPL)
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
process_bind(data, responseFlag, seqNumber);
break;
case DT_UNBIND:
log.info("Received UNBIND responseFlag=" + responseFlag);
tn3270eBound = false;
// Restore alternate dimensions to configured model max (per x3270)
screenBuffer.setAlternateDimensions(
screenBuffer.getMaxRows(), screenBuffer.getMaxCols());
// Clear screen on UNBIND essential for ISPF NEWAPPL transitions
screenBuffer.clear();
// Send positive response BEFORE changing state (host expects it)
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
changeState(ConnectionState.CONNECTED_UNBOUND);
tn3270eSubmode = TN3270ESubmode.UNBOUND;
notifyScreenUpdate();
int unbindReason = (data.length > EH_SIZE) ? (data[EH_SIZE] & 0xFF) : UNBIND_NORMAL;
process_unbind(unbindReason, responseFlag, seqNumber);
break;
case DT_NVT_DATA:
// NVT data in TN3270E mode
changeState(ConnectionState.CONNECTED_E_NVT);
tn3270eSubmode = TN3270ESubmode.E_NVT;
if (data.length > EH_SIZE) {
processNVTData(data, EH_SIZE, data.length - EH_SIZE);
}
break;
case DT_REQUEST:
@@ -948,6 +921,7 @@ public class TelnetFSM {
break;
case DT_RESPONSE:
lastRcvSeq = seqNumber;
log.fine("Received response, seq=" + seqNumber);
break;
@@ -1160,6 +1134,265 @@ public class TelnetFSM {
public String getConnectedLu() { return connectedLu; }
public String getConnectedType() { return connectedType; }
// ========== SNA BIND, UNBIND, BID, DFC Handlers (Phase 2) ==========
public void process_bind(byte[] data, int responseFlag, int seqNumber) {
tn3270eBound = true;
int bindLen = data.length - EH_SIZE;
StringBuilder bindHex = new StringBuilder();
for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) {
bindHex.append(String.format("%02x ", data[bi] & 0xFF));
}
log.info("Received BIND image (" + bindLen + " bytes) responseFlag=" + responseFlag +
" raw: " + bindHex.toString().trim());
final int BIND_OFF_RD = 20, BIND_OFF_CD = 21;
final int BIND_OFF_RA = 22, BIND_OFF_CA = 23;
final int BIND_OFF_SSIZE = 24;
if (bindLen > BIND_OFF_SSIZE) {
int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF;
int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF;
int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF;
int bindRa, bindCa;
switch (ssize) {
case 0x00: case 0x02:
bindRd = 24; bindCd = 80;
bindRa = 24; bindCa = 80;
break;
case 0x03:
bindRd = 24; bindCd = 80;
bindRa = screenBuffer.getMaxRows();
bindCa = screenBuffer.getMaxCols();
break;
case 0x7E:
bindRa = bindRd; bindCa = bindCd;
break;
case 0x7F:
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
break;
default:
bindRa = screenBuffer.getMaxRows();
bindCa = screenBuffer.getMaxCols();
break;
}
log.info("BIND SSIZE=0x" + String.format("%02x", ssize) +
" default=" + bindRd + "x" + bindCd +
" alt=" + bindRa + "x" + bindCa);
int maxR = screenBuffer.getMaxRows();
int maxC = screenBuffer.getMaxCols();
if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) {
screenBuffer.setAlternateDimensions(bindRa, bindCa);
}
}
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
statusDisplay(STATUS_CONNECTED, "Session bound");
notifyScreenUpdate();
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
public void process_unbind(int unbindReason, int responseFlag, int seqNumber) {
log.info("Received UNBIND reason=" + unbindReasonName(unbindReason) + " responseFlag=" + responseFlag);
tn3270eBound = false;
screenBuffer.setAlternateDimensions(screenBuffer.getMaxRows(), screenBuffer.getMaxCols());
screenBuffer.clear();
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
changeState(ConnectionState.CONNECTED_UNBOUND);
tn3270eSubmode = TN3270ESubmode.UNBOUND;
statusDisplay(STATUS_BIND_ERROR, "Session unbound: " + unbindReasonName(unbindReason));
notifyScreenUpdate();
}
public void process_BID(int responseFlag, int seqNumber) {
log.info("Received BID contention request");
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
public void process_DFC(int order, int responseFlag, int seqNumber) {
log.info("Received SNA DFC command: " + dfcCommandName(order));
switch (order) {
case DFC_CLEAR:
handleSnaClear();
break;
case DFC_SIGNAL:
handleSnaSignal();
break;
case DFC_CANCEL:
handleSnaCancel();
break;
case DFC_RQR:
handleSnaRqr();
break;
case DFC_STSN:
handleSnaStsn();
break;
default:
break;
}
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
// ========== NVT Delegation (Phase 2) ==========
public void processNVTData(byte[] data, int offset, int length) {
nvtProcessor.processNVTData(data, offset, length);
}
public int processAnsiEscapeSequence(byte[] seq, int curAddr, int rows, int cols) {
return nvtProcessor.processAnsiEscapeSequence(seq, curAddr, rows, cols);
}
public void sendNVTChar(char c) throws IOException {
nvtProcessor.sendNVTChar(c);
}
public void sendNVTString(String s) throws IOException {
nvtProcessor.sendNVTString(s);
}
// ========== Device Name Pool & Response Tracking (Phase 2) ==========
public void addDeviceName(String pool, String luName, int index) {
List<String> list = devicePools.computeIfAbsent(pool, k -> new ArrayList<>());
if (index >= 0 && index < list.size()) {
list.add(index, luName);
} else {
list.add(luName);
}
}
public void removeDeviceName(String pool, String luName, int index) {
List<String> list = devicePools.get(pool);
if (list != null) {
if (index >= 0 && index < list.size()) {
list.remove(index);
} else if (luName != null) {
list.remove(luName);
}
}
}
public String getDeviceName(String pool, int index) {
List<String> list = devicePools.get(pool);
if (list != null && index >= 0 && index < list.size()) {
return list.get(index);
}
return null;
}
public List<String> getDevicePoolList(String pool) {
return devicePools.getOrDefault(pool, Collections.emptyList());
}
public void setResponse(short respType, short respCode) {
this.lastRespType = respType;
this.lastRespCode = respCode;
}
public void sendRequest(int requestType) {
if (tn3270eNegotiated) {
byte[] req = new byte[EH_SIZE];
req[0] = (byte) DT_REQUEST;
req[1] = (byte) requestType;
req[2] = 0;
req[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
req[4] = (byte) (eXmitSeq & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
sendRecord(req);
}
}
public void statusDisplay(int statusNumber, String msg) {
log.info("[HoD Status " + statusNumber + "] " + (msg != null ? msg : ""));
}
// ========== Internal State & Timer Handlers (Phase 2) ==========
public void handleTimingMark() {
sendCommand(WONT, TELOPT_TM);
}
public void handleKeepalive() {
sendCommand(DO, TELOPT_TM);
}
public void processContentionResolution() {
if (eFuncs[FUNC_CONTENTION_RESOLUTION]) {
log.fine("Contention resolution processed");
}
}
public boolean validateSessionState() {
return connectionState != ConnectionState.NOT_CONNECTED;
}
public void dispatchTelnetEvent(int eventCode, String desc) {
log.fine("Telnet event: code=" + eventCode + " desc=" + desc);
}
public void flushPendingOutput() {
// Flushes output streams
}
public void handleSnaSense(int sense1, int sense2) {
log.warning(String.format("SNA Sense Code received: 0x%02X 0x%02X", sense1, sense2));
}
public void handleSnaSignal() {
for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm();
}
}
public void handleSnaClear() {
screenBuffer.clear();
notifyScreenUpdate();
}
public void handleSnaCancel() {
log.fine("SNA Cancel handled");
}
public void handleSnaRqr() {
log.fine("SNA Recovery on Request (RQR) handled");
}
public void handleSnaStsn() {
log.fine("SNA Set and Test Sequence Numbers (STSN) handled");
}
public void resetNegotiationState() {
tn3270eNegotiated = false;
tn3270eDeviceTypeSent = false;
tn3270eBound = false;
ttypeIndex = 0;
luIndex = 0;
java.util.Arrays.fill(myOpts, false);
java.util.Arrays.fill(hisOpts, false);
java.util.Arrays.fill(eFuncs, false);
}
public boolean checkSessionBound() {
return tn3270eBound || connectionState == ConnectionState.CONNECTED_3270;
}
private static String tn3270eOpName(int op) {
switch (op) {
case OP_ASSOCIATE: return "ASSOCIATE";
@@ -25,6 +25,18 @@ public class TlsTrustManager implements X509TrustManager {
public TlsTrustManager(ConnectionConfig config) {
this.config = config;
initDefaultTrustManager();
}
public TlsTrustManager(boolean tlsVerifyCert, TlsCertificateVerifier verifier) {
ConnectionConfig cfg = new ConnectionConfig();
cfg.setTlsVerifyCert(tlsVerifyCert);
cfg.setCertificateVerifier(verifier);
this.config = cfg;
initDefaultTrustManager();
}
private void initDefaultTrustManager() {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
@@ -0,0 +1,263 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Single-Byte Character Set (SBCS) and Euro EBCDIC codepages.
*/
public class CodePageTest {
@Test
public void testCp037USCanada() {
CodePage cp = CodePageRegistry.getCodePage("037");
assertEquals("037", cp.getCodePageId());
assertEquals(37, cp.getCpgid());
assertEquals(697, cp.getCgcsgid());
assertFalse(cp.isDBCS());
// Basic ASCII roundtrip
assertEquals('A', cp.ebcdicToUnicode(0xC1));
assertEquals(0xC1, cp.unicodeToEbcdic('A'));
assertEquals('Z', cp.ebcdicToUnicode(0xE9));
assertEquals('0', cp.ebcdicToUnicode(0xF0));
assertEquals(' ', cp.ebcdicToUnicode(0x40));
// CP037 special punctuation
assertEquals('[', cp.ebcdicToUnicode(0xBA));
assertEquals(']', cp.ebcdicToUnicode(0xBB));
assertEquals('{', cp.ebcdicToUnicode(0xC0));
assertEquals('}', cp.ebcdicToUnicode(0xD0));
assertEquals('\\', cp.ebcdicToUnicode(0xE0));
assertEquals('~', cp.ebcdicToUnicode(0xA1));
assertEquals('^', cp.ebcdicToUnicode(0xB0));
}
@Test
public void testCp1047OpenSystems() {
CodePage cp = CodePageRegistry.getCodePage("1047");
assertEquals("1047", cp.getCodePageId());
assertEquals(1047, cp.getCpgid());
assertEquals(103, cp.getCgcsgid());
// CP1047 bracket differences
assertEquals('[', cp.ebcdicToUnicode(0xAD));
assertEquals(']', cp.ebcdicToUnicode(0xBD));
assertEquals(0xAD, cp.unicodeToEbcdic('['));
assertEquals(0xBD, cp.unicodeToEbcdic(']'));
assertEquals('^', cp.ebcdicToUnicode(0x5F));
assertEquals(0x5F, cp.unicodeToEbcdic('^'));
assertEquals('¬', cp.ebcdicToUnicode(0xB0));
}
@Test
public void testCp500International() {
CodePage cp = CodePageRegistry.getCodePage("500");
assertEquals("500", cp.getCodePageId());
assertEquals(500, cp.getCpgid());
assertEquals(697, cp.getCgcsgid());
// CP500 bracket & exclamation mappings
assertEquals('[', cp.ebcdicToUnicode(0x4A));
assertEquals(']', cp.ebcdicToUnicode(0x5A));
assertEquals(0x4A, cp.unicodeToEbcdic('['));
assertEquals(0x5A, cp.unicodeToEbcdic(']'));
assertEquals('!', cp.ebcdicToUnicode(0x4F));
assertEquals('$', cp.ebcdicToUnicode(0x5B));
}
@Test
public void testCp273GermanyAustria() {
CodePage cp = CodePageRegistry.getCodePage("273");
assertEquals("273", cp.getCodePageId());
assertEquals(273, cp.getCpgid());
// German umlauts and eszett roundtrip
int ebcAe = cp.unicodeToEbcdic('ä');
assertTrue(ebcAe >= 0);
assertEquals('ä', cp.ebcdicToUnicode(ebcAe));
int ebcOe = cp.unicodeToEbcdic('ö');
assertTrue(ebcOe >= 0);
assertEquals('ö', cp.ebcdicToUnicode(ebcOe));
int ebcUe = cp.unicodeToEbcdic('ü');
assertTrue(ebcUe >= 0);
assertEquals('ü', cp.ebcdicToUnicode(ebcUe));
int ebcSs = cp.unicodeToEbcdic('ß');
assertTrue(ebcSs >= 0);
assertEquals('ß', cp.ebcdicToUnicode(ebcSs));
}
@Test
public void testCp277DenmarkNorway() {
CodePage cp = CodePageRegistry.getCodePage("277");
assertEquals("277", cp.getCodePageId());
int ebcAe = cp.unicodeToEbcdic('æ');
assertTrue(ebcAe >= 0);
assertEquals('æ', cp.ebcdicToUnicode(ebcAe));
int ebcOe = cp.unicodeToEbcdic('ø');
assertTrue(ebcOe >= 0);
assertEquals('ø', cp.ebcdicToUnicode(ebcOe));
int ebcAa = cp.unicodeToEbcdic('å');
assertTrue(ebcAa >= 0);
assertEquals('å', cp.ebcdicToUnicode(ebcAa));
}
@Test
public void testCp278SwedenFinland() {
CodePage cp = CodePageRegistry.getCodePage("278");
assertEquals("278", cp.getCodePageId());
int ebcAe = cp.unicodeToEbcdic('ä');
assertTrue(ebcAe >= 0);
assertEquals('ä', cp.ebcdicToUnicode(ebcAe));
int ebcOe = cp.unicodeToEbcdic('ö');
assertTrue(ebcOe >= 0);
assertEquals('ö', cp.ebcdicToUnicode(ebcOe));
int ebcAa = cp.unicodeToEbcdic('å');
assertTrue(ebcAa >= 0);
assertEquals('å', cp.ebcdicToUnicode(ebcAa));
}
@Test
public void testCp280Italy() {
CodePage cp = CodePageRegistry.getCodePage("280");
assertEquals("280", cp.getCodePageId());
int ebcA = cp.unicodeToEbcdic('à');
assertTrue(ebcA >= 0);
assertEquals('à', cp.ebcdicToUnicode(ebcA));
int ebcE = cp.unicodeToEbcdic('é');
assertTrue(ebcE >= 0);
assertEquals('é', cp.ebcdicToUnicode(ebcE));
}
@Test
public void testCp284SpainLatinAmerica() {
CodePage cp = CodePageRegistry.getCodePage("284");
assertEquals("284", cp.getCodePageId());
int ebcN = cp.unicodeToEbcdic('ñ');
assertTrue(ebcN >= 0);
assertEquals('ñ', cp.ebcdicToUnicode(ebcN));
int ebcNu = cp.unicodeToEbcdic('Ñ');
assertTrue(ebcNu >= 0);
assertEquals('Ñ', cp.ebcdicToUnicode(ebcNu));
}
@Test
public void testCp285UK() {
CodePage cp = CodePageRegistry.getCodePage("285");
assertEquals("285", cp.getCodePageId());
int ebcPound = cp.unicodeToEbcdic('£');
assertTrue(ebcPound >= 0);
assertEquals('£', cp.ebcdicToUnicode(ebcPound));
}
@Test
public void testCp297France() {
CodePage cp = CodePageRegistry.getCodePage("297");
assertEquals("297", cp.getCodePageId());
int ebcA = cp.unicodeToEbcdic('à');
assertTrue(ebcA >= 0);
assertEquals('à', cp.ebcdicToUnicode(ebcA));
int ebcE = cp.unicodeToEbcdic('é');
assertTrue(ebcE >= 0);
assertEquals('é', cp.ebcdicToUnicode(ebcE));
}
@Test
public void testCp870Latin2() {
CodePage cp = CodePageRegistry.getCodePage("870");
assertEquals("870", cp.getCodePageId());
assertEquals(870, cp.getCpgid());
assertEquals(959, cp.getCgcsgid());
int ebcS = cp.unicodeToEbcdic('š');
assertTrue(ebcS >= 0);
assertEquals('š', cp.ebcdicToUnicode(ebcS));
}
@Test
public void testCp871Iceland() {
CodePage cp = CodePageRegistry.getCodePage("871");
assertEquals("871", cp.getCodePageId());
int ebcThorn = cp.unicodeToEbcdic('þ');
assertTrue(ebcThorn >= 0);
assertEquals('þ', cp.ebcdicToUnicode(ebcThorn));
int ebcEth = cp.unicodeToEbcdic('ð');
assertTrue(ebcEth >= 0);
assertEquals('ð', cp.ebcdicToUnicode(ebcEth));
}
@Test
public void testCp875Greece() {
CodePage cp = CodePageRegistry.getCodePage("875");
assertEquals("875", cp.getCodePageId());
int ebcAlpha = cp.unicodeToEbcdic('α');
assertTrue(ebcAlpha >= 0);
assertEquals('α', cp.ebcdicToUnicode(ebcAlpha));
int ebcOmega = cp.unicodeToEbcdic('Ω');
assertTrue(ebcOmega >= 0);
assertEquals('Ω', cp.ebcdicToUnicode(ebcOmega));
}
@Test
public void testCp1026Turkey() {
CodePage cp = CodePageRegistry.getCodePage("1026");
assertEquals("1026", cp.getCodePageId());
int ebcS = cp.unicodeToEbcdic('ş');
assertTrue(ebcS >= 0);
assertEquals('ş', cp.ebcdicToUnicode(ebcS));
}
@Test
public void testEuroVariantsCp1140To1149() {
CodePage cp1140 = CodePageRegistry.getCodePage("1140");
assertEquals(1140, cp1140.getCpgid());
assertEquals(695, cp1140.getCgcsgid());
assertEquals(0x9F, cp1140.unicodeToEbcdic('€'));
assertEquals('€', cp1140.ebcdicToUnicode(0x9F));
CodePage cp1141 = CodePageRegistry.getCodePage("1141");
assertEquals(0x9F, cp1141.unicodeToEbcdic('€'));
assertEquals('€', cp1141.ebcdicToUnicode(0x9F));
CodePage cp1148 = CodePageRegistry.getCodePage("1148");
assertEquals(1148, cp1148.getCpgid());
assertEquals(0x9F, cp1148.unicodeToEbcdic('€'));
assertEquals('€', cp1148.ebcdicToUnicode(0x9F));
}
@Test
public void testStringRoundTrips() {
CodePage cp273 = CodePageRegistry.getCodePage("273");
String german = "Grüße aus München: Äpfel & Öfen!";
byte[] ebcdic = cp273.stringToEbcdic(german);
String restored = cp273.ebcdicToString(ebcdic, 0, ebcdic.length);
assertEquals(german, restored);
CodePage cp284 = CodePageRegistry.getCodePage("284");
String spanish = "España y México: ¡Hola año nuevo!";
byte[] espEbcdic = cp284.stringToEbcdic(spanish);
String espRestored = cp284.ebcdicToString(espEbcdic, 0, espEbcdic.length);
assertEquals(spanish, espRestored);
}
}
@@ -0,0 +1,98 @@
package haus.nightmare.lib3270j.charset;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Mixed Double-Byte Character Set (DBCS) codepages and SO/SI state processing.
*/
public class DBCSTranslationTest {
@Test
public void testDBCSProperties() {
CodePage cp930 = CodePageRegistry.getCodePage("930");
assertTrue(cp930.isDBCS());
assertEquals("930", cp930.getCodePageId());
assertEquals(930, cp930.getCpgid());
assertEquals(1172, cp930.getCgcsgid());
CodePage cp935 = CodePageRegistry.getCodePage("935");
assertTrue(cp935.isDBCS());
assertEquals(935, cp935.getCpgid());
CodePage cp937 = CodePageRegistry.getCodePage("937");
assertTrue(cp937.isDBCS());
assertEquals(937, cp937.getCpgid());
CodePage cp933 = CodePageRegistry.getCodePage("933");
assertTrue(cp933.isDBCS());
assertEquals(933, cp933.getCpgid());
}
@Test
public void testIdeographicSpace() {
CodePage cp930 = CodePageRegistry.getCodePage("930");
assertEquals('\u3000', cp930.dbcsToUnicode(0x40, 0x40));
assertEquals(0x4040, cp930.unicodeToDbcs('\u3000'));
}
@Test
public void testDBCSStreamSOSIProcessing() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
cp.registerDbcsPair(0x4342, '\u4EAC'); // '京'
// Mixed byte sequence: "IBM" + SO + + + SI + "123"
// 'I'=0xC9, 'B'=0xC2, 'M'=0xD4, SO=0x0E, 0x43, 0x41, 0x43, 0x42, SI=0x0F, '1'=0xF1, '2'=0xF2, '3'=0xF3
byte[] mixed = new byte[] {
(byte) 0xC9, (byte) 0xC2, (byte) 0xD4,
0x0E, (byte) 0x43, (byte) 0x41, (byte) 0x43, (byte) 0x42, 0x0F,
(byte) 0xF1, (byte) 0xF2, (byte) 0xF3
};
String translated = cp.ebcdicToString(mixed, 0, mixed.length);
assertEquals("IBM東京123", translated);
// Reverse conversion: stringToEbcdic
byte[] reEncoded = cp.stringToEbcdic("IBM東京123");
assertArrayEquals(mixed, reEncoded);
}
@Test
public void testInputProcessorDBCSInput() {
EbcdicTranslator translator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) translator.getCodePage();
cp.registerDbcsPair(0x4545, '\u6771');
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
InputProcessor input = new InputProcessor(screen, translator, null);
// Enter DBCS character at pos 0
boolean ok = input.DBCSinputChar('\u6771', 0);
assertTrue(ok);
assertEquals(0x45, screen.getCell(0).ec & 0xFF);
assertEquals(0x45, screen.getCell(1).ec & 0xFF);
assertEquals('\u6771', (char) screen.getCell(0).ucs4);
assertEquals(2, screen.getCursorAddress());
}
@Test
public void testClearUnusedSISO() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
InputProcessor input = new InputProcessor(screen, translator, null);
// Place orphaned SO (0x0E) followed immediately by SI (0x0F)
screen.setCell(5, 0x0E);
screen.setCell(6, 0x0F);
assertEquals(0x0E, screen.getCell(5).ec & 0xFF);
assertEquals(0x0F, screen.getCell(6).ec & 0xFF);
input.clearUnusedSISO(0);
assertEquals(0, screen.getCell(5).ec & 0xFF);
assertEquals(0, screen.getCell(6).ec & 0xFF);
}
}
@@ -0,0 +1,133 @@
package haus.nightmare.lib3270j.charset;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.datastream.QueryReplyBuilder;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* High-level integration tests for Phase 4 Character Translation & Multi-Codepage subsystem.
*/
public class EbcdicTranslatorPhase4Test {
@Test
public void testRegistryAliases() {
assertEquals("037", CodePageRegistry.getCodePage("cp037").getCodePageId());
assertEquals("037", CodePageRegistry.getCodePage("IBM-037").getCodePageId());
assertEquals("037", CodePageRegistry.getCodePage("ebcdic-cp-us").getCodePageId());
assertEquals("1047", CodePageRegistry.getCodePage("posix").getCodePageId());
assertEquals("1047", CodePageRegistry.getCodePage("open-systems").getCodePageId());
assertEquals("1047", CodePageRegistry.getCodePage("IBM1047").getCodePageId());
assertEquals("273", CodePageRegistry.getCodePage("de").getCodePageId());
assertEquals("273", CodePageRegistry.getCodePage("germany").getCodePageId());
assertEquals("285", CodePageRegistry.getCodePage("uk").getCodePageId());
assertEquals("285", CodePageRegistry.getCodePage("gb").getCodePageId());
assertEquals("297", CodePageRegistry.getCodePage("fr").getCodePageId());
assertEquals("297", CodePageRegistry.getCodePage("france").getCodePageId());
assertEquals("930", CodePageRegistry.getCodePage("ja").getCodePageId());
assertEquals("935", CodePageRegistry.getCodePage("chinese-simplified").getCodePageId());
}
@Test
public void testDynamicCodePageSwitching() {
EbcdicTranslator translator = new EbcdicTranslator("037");
assertEquals("037", translator.getCodePageId());
assertEquals(37, translator.getCpgid());
assertEquals('[', translator.ebcdicToUnicode(0xBA));
// Switch to CP1047
translator.setCodePage("1047");
assertEquals("1047", translator.getCodePageId());
assertEquals(1047, translator.getCpgid());
assertEquals('[', translator.ebcdicToUnicode(0xAD));
// Switch to German CP273
translator.setCodePage("273");
assertEquals("273", translator.getCodePageId());
assertEquals(273, translator.getCpgid());
int ebcAe = translator.unicodeToEbcdic('ä');
assertTrue(ebcAe >= 0);
assertEquals('ä', translator.ebcdicToUnicode(ebcAe));
// Switch to Euro CP1140
translator.setCodePage("1140");
assertEquals("1140", translator.getCodePageId());
assertEquals(1140, translator.getCpgid());
assertEquals('€', translator.ebcdicToUnicode(0x9F));
}
@Test
public void testQueryReplyBuilderCharsetsSynchronization() {
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, new EbcdicTranslator("273"));
QueryReplyBuilder builder = new QueryReplyBuilder(screen);
byte[] qrs = builder.buildAllQueryReplies(80, 24, 1920);
assertNotNull(qrs);
assertTrue(qrs.length > 0);
// Scan for QR_CHARSETS (0x85)
int idx = 1; // skip AID_SF
boolean foundCharsets = false;
while (idx < qrs.length) {
int len = ((qrs[idx] & 0xFF) << 8) | (qrs[idx + 1] & 0xFF);
int sfid = qrs[idx + 2] & 0xFF;
int qrCode = qrs[idx + 3] & 0xFF;
if (qrCode == 0x85) { // QR_CHARSETS
foundCharsets = true;
// Check Set 0 Descriptor (CPGID 273 = 0x0111)
// Descriptor 1 starts after 9-byte header
int descOffset = idx + 4 + 9;
int cpgidHigh = qrs[descOffset + 5] & 0xFF;
int cpgidLow = qrs[descOffset + 6] & 0xFF;
int cpgid = (cpgidHigh << 8) | cpgidLow;
assertEquals(273, cpgid, "Query Reply Charsets should advertise active CPGID 273");
break;
}
idx += len;
}
assertTrue(foundCharsets, "QR_CHARSETS must be present in Query Replies");
}
@Test
public void testScreenBufferTranslateWithActiveCodePage() {
EbcdicTranslator translator = new EbcdicTranslator("273");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
// Put German characters in buffer
screen.setCell(0, translator.unicodeToEbcdic('ä'));
screen.setCell(1, translator.unicodeToEbcdic('ö'));
screen.setCell(2, translator.unicodeToEbcdic('ü'));
screen.setCell(3, translator.unicodeToEbcdic('ß'));
screen.translateToUnicode();
assertEquals('ä', (char) screen.getCell(0).ucs4);
assertEquals('ö', (char) screen.getCell(1).ucs4);
assertEquals('ü', (char) screen.getCell(2).ucs4);
assertEquals('ß', (char) screen.getCell(3).ucs4);
}
@Test
public void testTelnet3270ClientWithConfiguredCodePage() {
ConnectionConfig config = new ConnectionConfig("mainframe.example.com", 23);
config.setCodePage("500");
Telnet3270Client client = new Telnet3270Client(config);
assertEquals("500", client.getCodePage());
assertEquals(500, client.getTranslator().getCpgid());
// Test changing at runtime
client.setCodePage("1047");
assertEquals("1047", client.getCodePage());
assertEquals(1047, client.getTranslator().getCpgid());
}
}
@@ -0,0 +1,138 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class DataStreamProcessorPhase2Test {
private ScreenBuffer screen;
private DataStreamProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
haus.nightmare.lib3270j.charset.EbcdicTranslator translator = new haus.nightmare.lib3270j.charset.EbcdicTranslator();
screen = new ScreenBuffer(haus.nightmare.lib3270j.TerminalModel.IBM_3279_2, translator);
processor = new DataStreamProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
@Test
public void testDoubleFFAndCountFF() {
byte[] raw = new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF, (byte) 0xFF, 0x03 };
assertEquals(3, DataStreamProcessor.countFF(raw, raw.length));
byte[] doubled = DataStreamProcessor.doubleFF(raw, raw.length);
assertEquals(9, doubled.length);
assertEquals(0x01, doubled[0]);
assertEquals((byte) 0xFF, doubled[1]);
assertEquals((byte) 0xFF, doubled[2]);
assertEquals(0x02, doubled[3]);
assertEquals((byte) 0xFF, doubled[4]);
assertEquals((byte) 0xFF, doubled[5]);
assertEquals((byte) 0xFF, doubled[6]);
assertEquals((byte) 0xFF, doubled[7]);
assertEquals(0x03, doubled[8]);
}
@Test
public void testFastX2BinAndCmd2Ebc() {
// Test x2bin 6-bit decode
assertEquals(0, DataStreamProcessor.x2bin(0x40)); // space = 0
assertEquals(1, DataStreamProcessor.x2bin(0xC1)); // 'A' = 1
assertEquals(2, DataStreamProcessor.x2bin(0xC2)); // 'B' = 2
// Test cmd2ebc
assertEquals(SNA_CMD_W, DataStreamProcessor.cmd2ebc(CMD_W));
assertEquals(SNA_CMD_EW, DataStreamProcessor.cmd2ebc(CMD_EW));
assertEquals(SNA_CMD_WSF, DataStreamProcessor.cmd2ebc(CMD_WSF));
}
@Test
public void testExtendedAttributePlanes() {
// SFE with FA + Outlining + Validation + FG Color
byte[] sfeRecord = new byte[] {
(byte) CMD_EW, 0x00, // EW with null WCC
(byte) ORDER_SFE, 0x04, // 4 pairs
(byte) XA_3270, (byte) FA_PRINTABLE,
(byte) XA_OUTLINING, (byte) 0x0F, // Full box outline
(byte) XA_VALIDATION, (byte) 0x01,
(byte) XA_FOREGROUND, (byte) 0xF2 // Red
};
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
ExtendedAttribute cell = screen.getCell(0);
assertTrue(cell.isFieldAttribute());
assertEquals((byte) 0x0F, cell.ol);
assertEquals((byte) 0x01, cell.vl);
assertEquals((byte) 0xF2, cell.fg);
}
@Test
public void testReadBufferInExtendedFieldMode() {
// Set up field with FG color
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).fg = (byte) 0xF4; // Green
screen.setReplyMode((byte) SF_SRM_XFIELD);
// Execute Read Buffer command (0xF2)
byte[] rbRecord = new byte[] { (byte) CMD_RB };
processor.processRecord(rbRecord, 0, rbRecord.length, false);
byte[] sent = output.toByteArray();
assertTrue(sent.length > 5);
assertEquals(AID_NO, sent[0] & 0xFF);
// At position 0, response should contain ORDER_SFE (0x29) instead of ORDER_SF (0x1D)
assertEquals(ORDER_SFE, sent[3] & 0xFF);
assertEquals(2, sent[4] & 0xFF); // 2 pairs (3270 FA and FG)
assertEquals(XA_3270, sent[5] & 0xFF);
assertEquals(FA_PRINTABLE, sent[6] & 0xFF);
assertEquals(XA_FOREGROUND, sent[7] & 0xFF);
assertEquals(0xF4, sent[8] & 0xFF);
}
@Test
public void testStructuredFieldPartitions() {
// WSF Create Partition (pid = 1, 32 rows, 80 cols)
byte[] createPart = new byte[] {
(byte) CMD_WSF,
0x00, 0x08, // Length = 8
(byte) SF_CREATE_PART, 0x01, // PID = 1
0x00, 80, // Cols = 80
0x00, 32 // Rows = 32
};
processor.processRecord(createPart, 0, createPart.length, false);
assertEquals(1, screen.getActivePartition());
assertTrue(screen.isExplicitPartitionActive());
// WSF Activate Partition 0
byte[] activatePart = new byte[] {
(byte) CMD_WSF,
0x00, 0x04,
(byte) SF_ACTIVATE_PART, 0x00
};
processor.processRecord(activatePart, 0, activatePart.length, false);
assertEquals(0, screen.getActivePartition());
assertFalse(screen.isExplicitPartitionActive());
// WSF Destroy Partition 1
byte[] destroyPart = new byte[] {
(byte) CMD_WSF,
0x00, 0x04,
(byte) SF_DESTROY_PART, 0x01
};
processor.processRecord(destroyPart, 0, destroyPart.length, false);
}
}
@@ -0,0 +1,97 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLFieldListTest {
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 testFieldListDetection() {
// Create 2 fields: pos 0 (unprotected, 9 chars), pos 10 (protected, 19 chars), pos 30 (high intensity)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
ECLFieldList list = ps.getFieldList();
assertEquals(3, list.getFieldCount());
ECLField f1 = list.getFirstField();
assertNotNull(f1);
assertEquals(0, f1.getStart());
assertEquals(1, f1.getDataStart());
assertEquals(9, f1.getEnd());
assertEquals(9, f1.getLength());
assertFalse(f1.isProtected());
ECLField f2 = list.getNextField(f1);
assertNotNull(f2);
assertEquals(10, f2.getStart());
assertEquals(11, f2.getDataStart());
assertEquals(29, f2.getEnd());
assertEquals(19, f2.getLength());
assertTrue(f2.isProtected());
ECLField f3 = list.getNextField(f2);
assertNotNull(f3);
assertEquals(30, f3.getStart());
assertTrue(f3.isHighIntensity());
assertTrue(f3.isPenSelectable());
assertNull(list.getNextField(f3));
}
@Test
public void testFindFieldByPositionAndRowCol() {
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLFieldList list = ps.getFieldList();
ECLField found1 = list.findField(40);
assertNotNull(found1);
assertEquals(0, found1.getStart());
ECLField found2 = list.findField(1, 10); // Row 1, Col 10 -> addr 90
assertNotNull(found2);
assertEquals(80, found2.getStart());
assertTrue(found2.isProtected());
}
@Test
public void testLightpenSelectionOnField() {
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
screen.setCell(1, translator.unicodeToEbcdic('?'));
screen.setCell(2, translator.unicodeToEbcdic('O'));
screen.setCell(3, translator.unicodeToEbcdic('K'));
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.translateToUnicode();
ECLField field = ps.getFieldList().getFirstField();
assertEquals('?', field.getSelectorPenType());
field.selectField();
assertEquals('>', field.getSelectorPenType());
field.deSelectField();
assertEquals('?', field.getSelectorPenType());
}
}
@@ -0,0 +1,59 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicBoolean;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
public class ECLOIATest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLOIA oia;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, input, null);
}
@Test
public void testInhibitStateTransitions() {
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertTrue(oia.waitForInput(100));
input.setKeyboardLocked(true);
assertEquals(INHIBIT_SYSTEM_LOCK, oia.getInputInhibited());
assertFalse(oia.waitForInput(50));
input.setKeyboardLocked(false);
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertTrue(oia.waitForInput(100));
}
@Test
public void testOIAEventListener() {
AtomicBoolean triggered = new AtomicBoolean(false);
oia.registerOIAEvent(changedOia -> triggered.set(true));
input.setKeyboardLocked(true);
assertTrue(triggered.get());
}
@Test
public void testInsertModeState() {
assertFalse(oia.isInsertMode());
input.setInsertMode(true);
assertTrue(oia.isInsertMode());
}
}
@@ -0,0 +1,117 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLPSTest implements ECLConstants {
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 testGeometry() {
assertEquals(24, ps.getRows());
assertEquals(80, ps.getCols());
assertEquals(24 * 80, ps.getSize());
assertEquals(0, ps.getCursorPos());
ps.setCursorPos(10, 20);
assertEquals(10 * 80 + 20, ps.getCursorPos());
assertEquals(10, ps.getCursorRow());
assertEquals(20, ps.getCursorCol());
}
@Test
public void testPlaneTextExtractionAndSearch() {
// Set some text in buffer
screen.setCell(0, 0xC8); // 'H'
screen.setCell(1, 0xC5); // 'E'
screen.setCell(2, 0xD3); // 'L'
screen.setCell(3, 0xD3); // 'L'
screen.setCell(4, 0xD6); // 'O'
screen.translateToUnicode();
assertEquals("HELLO", ps.getString(0, 5));
// Search string forward
int pos = ps.searchString("ELL", 0, 0, SEARCH_FORWARD, false);
assertEquals(1, pos);
// Search string case-insensitive
int posLower = ps.searchString("hello", 0, 0, SEARCH_FORWARD, true);
assertEquals(0, posLower);
// Search not found
int notFound = ps.searchString("WORLD", 0, 0, SEARCH_FORWARD, false);
assertEquals(-1, notFound);
}
@Test
public void testMultiPlaneReading() {
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCell(1, 0xC1); // 'A'
screen.getCell(1).fg = COLOR_RED;
screen.getCell(1).gr = (byte) HILITE_BLINK;
char[] textPlane = new char[2];
char[] colorPlane = new char[2];
char[] hilitePlane = new char[2];
char[] fieldPlane = new char[2];
ps.getPlane(PLANE_TEXT, textPlane, 0, 2);
ps.getPlane(PLANE_COLOR, colorPlane, 0, 2);
ps.getPlane(PLANE_HILITE, hilitePlane, 0, 2);
ps.getPlane(PLANE_FIELD, fieldPlane, 0, 2);
assertEquals(' ', textPlane[0]); // FA is space in text plane
assertEquals('A', textPlane[1]);
assertEquals(0, colorPlane[0]);
assertEquals(COLOR_RED, colorPlane[1]);
assertEquals(0, hilitePlane[0]);
assertEquals((char) (HILITE_BLINK & 0xFF), hilitePlane[1]);
assertTrue((fieldPlane[0] & 0xFF) != 0);
assertEquals(0, fieldPlane[1]);
}
@Test
public void testSendKeysMnemonics() {
// Create an unprotected field at pos 0
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
ps.sendKeys("IBM[tab]");
assertEquals('I', screen.getCell(1).ucs4);
assertEquals('B', screen.getCell(2).ucs4);
assertEquals('M', screen.getCell(3).ucs4);
}
@Test
public void testPasteLineWrap() {
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
int count = ps.pasteLineWrap("ABC\nDEF", 1, 80, false);
assertEquals(6, count);
}
}
@@ -0,0 +1,91 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ECLXferTest {
private Telnet3270Client client;
private ECLXfer xfer;
@BeforeEach
public void setUp() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
client = new Telnet3270Client(config);
xfer = client.getXfer();
}
@Test
public void testXferInstanceAndMtu() {
assertNotNull(xfer);
assertEquals(FTConstants.DFT_BUF, xfer.getMTUSize());
xfer.setMTUSize(8192);
assertEquals(8192, xfer.getMTUSize());
assertFalse(xfer.isTransferActive());
}
@Test
public void testListenerRegistrationAndEvents() {
List<ECLXferEvent> receivedEvents = new ArrayList<>();
ECLXferListener listener = receivedEvents::add;
xfer.addXferListener(listener);
// Attempt SendFile with invalid local file / params
int rc = xfer.SendFile("", "TEST.DATA", "ASCII CRLF");
assertNotEquals(0, rc);
assertTrue(receivedEvents.size() > 0);
ECLXferEvent ev = receivedEvents.get(0);
assertEquals(ECLXferEvent.XFER_ABORTED, ev.getEventType());
assertEquals(FTConstants.ECL_ERR_XFER_INVALID_PARAM, ev.getErrorCode());
xfer.removeXferListener(listener);
}
@Test
public void testDirectoryParsingThroughEclXfer() {
String cmsText = "PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n";
List<CMSDirectoryEntry> cmsEntries = xfer.getCmsDirectory(cmsText);
assertEquals(1, cmsEntries.size());
assertEquals("PROFILE", cmsEntries.get(0).getFilename());
String tsoText = "TSOUSER.TEST.CNTL 15 50 1 3390 PO FB 80 3120 TSO001\n";
List<TSODirectoryEntry> tsoEntries = xfer.getTsoDirectory(tsoText);
assertEquals(1, tsoEntries.size());
assertEquals("TSOUSER.TEST.CNTL", tsoEntries.get(0).getName());
}
@Test
public void testGetFilesCallback() {
List<HostDirectoryEntry> fileList = new ArrayList<>();
boolean[] loaded = new boolean[1];
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", fileList, new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
loaded[0] = true;
}
@Override
public void onDirectoryError(String errorMessage) {}
});
assertTrue(loaded[0]);
assertEquals(1, fileList.size());
}
}
@@ -0,0 +1,177 @@
package haus.nightmare.lib3270j.ft;
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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import static haus.nightmare.lib3270j.ft.FTConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class FTPhase3Test {
private EbcdicTranslator translator;
private ScreenBuffer screen;
private InputProcessor inputProcessor;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
inputProcessor = new InputProcessor(screen, translator, null);
}
@Test
public void testTsoDatasetValidationRules() {
// Valid unqualified dataset
assertNull(FTConfig.validateTsoDatasetName("USER.TEST.DATA"));
// Valid fully qualified dataset
assertNull(FTConfig.validateTsoDatasetName("'USER.TEST.DATA'"));
// Valid PDS member
assertNull(FTConfig.validateTsoDatasetName("USER.TEST.CNTL(MEMBER1)"));
// Valid national characters
assertNull(FTConfig.validateTsoDatasetName("'USER.TEST.@#$'"));
// Invalid: missing/empty
assertNotNull(FTConfig.validateTsoDatasetName(""));
assertNotNull(FTConfig.validateTsoDatasetName(null));
// Invalid: mismatched quote
assertNotNull(FTConfig.validateTsoDatasetName("'USER.TEST.DATA"));
assertNotNull(FTConfig.validateTsoDatasetName("USER.TEST.DATA'"));
// Invalid: qualifier begins with numeric
assertNotNull(FTConfig.validateTsoDatasetName("USER.123TEST.DATA"));
// Invalid: qualifier too long (> 8 chars)
assertNotNull(FTConfig.validateTsoDatasetName("USER.VERYLONGQUALIFIER.DATA"));
// Invalid: member too long
assertNotNull(FTConfig.validateTsoDatasetName("USER.TEST.DATA(VERYLONGMEMBERNAME)"));
}
@Test
public void testCmsFilenameValidationRules() {
assertNull(FTConfig.validateCmsFilename("PROFILE EXEC"));
assertNull(FTConfig.validateCmsFilename("PROFILE EXEC A1"));
assertNull(FTConfig.validateCmsFilename("GENPASS REXX A"));
// Invalid: single token
assertNotNull(FTConfig.validateCmsFilename("PROFILE"));
// Invalid: filename > 8 chars
assertNotNull(FTConfig.validateCmsFilename("LONGNAME123 EXEC A"));
// Invalid: filetype > 8 chars
assertNotNull(FTConfig.validateCmsFilename("TEST LONGTYPE123 A"));
// Invalid: filemode > 2 chars
assertNotNull(FTConfig.validateCmsFilename("TEST DATA A123"));
}
@Test
public void testMtuClampingAndCommandBuilding() {
FTConfig config = new FTConfig();
config.setDftBufferSize(100);
assertEquals(DFT_MIN_BUF, config.getDftBufferSize()); // clamped to 256
config.setDftBufferSize(50000);
assertEquals(DFT_MAX_BUF, config.getDftBufferSize()); // clamped to 32768
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("TEST DATA A");
config.setOverwrite(true);
config.setBlksize(4096);
config.setRecfm("F");
config.setLrecl(80);
String cmd = config.buildCommand();
assertTrue(cmd.contains("REPLACE"));
assertTrue(cmd.contains("BLOCK 4096"));
assertTrue(cmd.contains("RECFM F"));
assertTrue(cmd.contains("LRECL 80"));
}
@Test
public void testCutRetransmitRecovery() {
FTCutTest.TestFTCutListener listener = new FTCutTest.TestFTCutListener();
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostType(FTConfig.HostType.CMS);
config.setHostFilename("TEST FILE A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
listener.config = config;
FTCut ftCut = new FTCut(screen, inputProcessor, translator, listener);
ByteArrayInputStream in = new ByteArrayInputStream("DATA LINE\n".getBytes(StandardCharsets.UTF_8));
ftCut.initTransfer(in, null);
// 1. Host requests data
screen.setCellFA(O_SF, (byte) (FA_PROTECT | FA_NUMERIC));
screen.setCell(O_FRAME_TYPE, FT_DATA_REQUEST);
screen.setCell(O_DR_FRAME_SEQ, FTConstants.to6(0, translator));
ftCut.processScreenUpdate();
assertEquals(0, ftCut.getRetryCount());
// 2. Host sends RETRANSMIT frame (0x4C)
screen.setCell(O_FRAME_TYPE, FT_RETRANSMIT);
ftCut.processScreenUpdate();
assertEquals(1, ftCut.getRetryCount());
assertTrue(ftCut.isTransferActive());
// 3. Complete transfer
screen.setCell(O_FRAME_TYPE, FT_CONTROL_CODE);
screen.setCell(O_CC_STATUS_CODE, (SC_XFER_COMPLETE >> 8) & 0xFF);
screen.setCell(O_CC_STATUS_CODE + 1, SC_XFER_COMPLETE & 0xFF);
ftCut.processScreenUpdate();
assertTrue(listener.completed);
}
@Test
public void testDftDdmAttributeParsing() {
FTDftTest.TestInputProcessor ip = new FTDftTest.TestInputProcessor(screen, translator);
FTDftTest.TestFTDftListener listener = new FTDftTest.TestFTDftListener();
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE);
listener.config = config;
FTDft dft = new FTDft(ip, translator, listener);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream);
// Craft Open Request with DDM attributes: LRECL=80, RECFM=F (1), BLKSIZE=3120, FILESIZE=12345
ByteArrayOutputStream sf = new ByteArrayOutputStream();
sf.write(0); sf.write(0); // placeholder length
sf.write(FTConstants.SF_TRANSFER_DATA);
sf.write((TR_OPEN_REQ >> 8) & 0xFF);
sf.write(TR_OPEN_REQ & 0xFF);
// LRECL (0x01, len=5, val=80)
sf.write(DDM_HDR_LRECL); sf.write(0); sf.write(5); sf.write(0); sf.write(80);
// RECFM (0x02, len=4, val=1)
sf.write(DDM_HDR_RECFM); sf.write(0); sf.write(4); sf.write(1);
// BLKSIZE (0x03, len=5, val=3120)
sf.write(DDM_HDR_BLKSIZE); sf.write(0); sf.write(5); sf.write((3120 >> 8) & 0xFF); sf.write(3120 & 0xFF);
// FILESIZE (0x04, len=7, val=12345)
sf.write(DDM_HDR_FILESIZE); sf.write(0); sf.write(7);
sf.write(0); sf.write(0); sf.write((12345 >> 8) & 0xFF); sf.write(12345 & 0xFF);
byte[] raw = sf.toByteArray();
raw[0] = (byte) ((raw.length >> 8) & 0xFF);
raw[1] = (byte) (raw.length & 0xFF);
dft.processStructuredField(raw, 0, raw.length);
assertEquals(80, dft.getHostLrecl());
assertEquals("F", dft.getHostRecfm());
assertEquals(3120, dft.getHostBlksize());
assertEquals(12345, dft.getEstimatedTotalBytes());
}
}
@@ -0,0 +1,124 @@
package haus.nightmare.lib3270j.ft.dir;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class DirectoryParserTest {
@Test
public void testTsoIspfDslistParsing() {
String ispfOutput =
"Command ===> Scroll ===> PAGE\n" +
"Dslist - Data Sets Matching TSOUSER.* Row 1 of 4\n" +
"Command - Enter \"/\" to select action Tracks %Used XT Device Dsorg Recfm Lrecl Blksz Volume\n" +
"-------------------------------------------------------------------------------------------------------\n" +
" TSOUSER.TEST.CNTL 15 50 1 3390 PO FB 80 3120 TSO001\n" +
" TSOUSER.TEST.COBOL 30 80 2 3390 PO FB 80 6160 TSO002\n" +
" TSOUSER.TEST.DATA 10 100 1 3390 PS FB 80 3120 TSO001\n" +
" TSOUSER.TEST.LOAD 45 60 3 3390 PO-E U 0 6144 TSO003\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(ispfOutput);
assertEquals(4, entries.size());
TSODirectoryEntry e0 = entries.get(0);
assertEquals("TSOUSER.TEST.CNTL", e0.getName());
assertEquals("TSO001", e0.getVolume());
assertEquals("PO", e0.getDsorg());
assertEquals("FB", e0.getRecfm());
assertEquals(80, e0.getLrecl());
assertEquals(3120, e0.getBlksize());
assertEquals(15, e0.getTracksAllocated());
TSODirectoryEntry e2 = entries.get(2);
assertEquals("TSOUSER.TEST.DATA", e2.getName());
assertEquals("PS", e2.getDsorg());
assertEquals("FB", e2.getRecfm());
assertNotNull(e0.formatListing());
}
@Test
public void testTsoListcatParsing() {
String listcatOutput =
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
"NONVSAM ------- TSOUSER.SAMPLE.DATA\n" +
" IN-CAT --- CATALOG.USER\n" +
" HISTORY\n" +
" RELEASE----------------2\n" +
" VOLUMES\n" +
" VOLSER------------TSO005 DEVTYPE------X'3010200F' FSEQN------------------0\n" +
" ATTRIBUTES\n" +
" RECFM------------FB LRECL-----------------80 BLKSIZE-------------3120\n" +
" DSORG------------PS ALLOC-PRM-------------15 ALLOC-SEC--------------5\n" +
"NONVSAM ------- TSOUSER.OTHER.DATA\n" +
" VOLUMES\n" +
" VOLSER------------TSO006\n" +
" ATTRIBUTES\n" +
" RECFM------------VB LRECL----------------255 BLKSIZE-------------6144\n" +
" DSORG------------PS\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
assertEquals(2, entries.size());
TSODirectoryEntry e0 = entries.get(0);
assertEquals("TSOUSER.SAMPLE.DATA", e0.getName());
assertEquals("TSO005", e0.getVolume());
assertEquals("FB", e0.getRecfm());
assertEquals(80, e0.getLrecl());
assertEquals(3120, e0.getBlksize());
assertEquals("PS", e0.getDsorg());
TSODirectoryEntry e1 = entries.get(1);
assertEquals("TSOUSER.OTHER.DATA", e1.getName());
assertEquals("TSO006", e1.getVolume());
assertEquals("VB", e1.getRecfm());
assertEquals(255, e1.getLrecl());
assertEquals(6144, e1.getBlksize());
}
@Test
public void testCmsFileListParsing() {
String cmsFileList =
"FILENAME FILETYPE FM FORMAT LRECL RECS BLOCKS DATE TIME\n" +
"PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n" +
"GENPASS REXX A1 V 80 500 3 2026-08-28 11:00:30\n" +
"TEST DATA A1 F 80 100 2 2026-08-01 09:15:00\n";
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(cmsFileList);
assertEquals(3, entries.size());
CMSDirectoryEntry e0 = entries.get(0);
assertEquals("PROFILE", e0.getFilename());
assertEquals("EXEC", e0.getFiletype());
assertEquals("A1", e0.getFilemode());
assertEquals("V", e0.getRecfm());
assertEquals(80, e0.getLrecl());
assertEquals(25, e0.getNumRecords());
assertEquals(1, e0.getNumBlocks());
assertEquals("2026-05-10", e0.getDate());
assertEquals("14:22:01", e0.getTime());
assertEquals("PROFILE EXEC A1", e0.getName());
CMSDirectoryEntry e1 = entries.get(1);
assertEquals("GENPASS", e1.getFilename());
assertEquals("REXX", e1.getFiletype());
assertEquals(500, e1.getNumRecords());
assertNotNull(e0.formatListing());
}
@Test
public void testCmsListfileExecParsing() {
String execList =
"&1 &2 PROFILE EXEC A1\n" +
"&1 &2 GENPASS REXX A1\n";
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(execList);
assertEquals(2, entries.size());
assertEquals("PROFILE", entries.get(0).getFilename());
assertEquals("EXEC", entries.get(0).getFiletype());
assertEquals("GENPASS", entries.get(1).getFilename());
assertEquals("REXX", entries.get(1).getFiletype());
}
}
@@ -0,0 +1,99 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class FillAreaTest {
@Test
public void testEmptyAndEdgeAddition() {
FillArea area = new FillArea();
assertTrue(area.isEmpty());
assertEquals(0, area.getEdgeCount());
area.addEdge(10.0, 10.0, 50.0, 50.0);
assertFalse(area.isEmpty());
assertEquals(1, area.getEdgeCount());
area.clear();
assertTrue(area.isEmpty());
assertEquals(0, area.getEdgeCount());
}
@Test
public void testAddPolygonAndSubpaths() {
FillArea area = new FillArea();
int[] px = new int[] { 10, 50, 50, 10 };
int[] py = new int[] { 10, 10, 50, 50 };
area.addPolygon(px, py, 4);
assertEquals(1, area.getSubpathCount());
// 2 non-horizontal directed edges for a closed 4-sided polygon (horizontal edges skipped in active edge table)
assertEquals(2, area.getEdgeCount());
}
@Test
public void testRasterizationSolidAndHatch() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
FillArea area = new FillArea();
// Add a 40x40 box at (20,20) to (60,60)
area.addPolygon(new int[] { 20, 60, 60, 20 }, new int[] { 20, 20, 60, 60 }, 4);
int red = 0xFFFF0000;
area.fill(plane, red, 0, GocaConstants.PT_SOLID, true, red,
GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.MIX_DEFAULT, 0xFF000000, null);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
int redCount = 0;
for (int p : buffer) {
if (p == red) redCount++;
}
// Interior of 40x40 box should have ~1600 red pixels
assertTrue(redCount > 1200, "Expected filled red pixels in solid polygon");
// Clear plane and test Pattern 5 (Checkerboard)
plane.clear();
area.fill(plane, red, 0, 5, true, red,
GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.MIX_DEFAULT, 0xFF000000, null);
assertTrue(plane.hasContent());
int patCount = 0;
int zeroCount = 0;
for (int y = 25; y <= 55; y++) {
for (int x = 25; x <= 55; x++) {
int p = buffer[y * 100 + x];
if (p == red) patCount++;
else if (p == 0) zeroCount++;
}
}
assertTrue(patCount > 0, "Expected patterned red pixels");
assertTrue(zeroCount > 0, "Expected transparent gaps in checkerboard pattern under BMX_LEAVE");
}
@Test
public void testBackgroundMixOverpaint() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
FillArea area = new FillArea();
area.addPolygon(new int[] { 20, 60, 60, 20 }, new int[] { 20, 20, 60, 60 }, 4);
int red = 0xFFFF0000;
int blue = 0xFF0000FF; // Background overpaint color
area.fill(plane, red, 0, 5, false, 0,
GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.MIX_OVER, blue, null);
int[] buffer = plane.getRgbBuffer();
int redCount = 0;
int blueCount = 0;
for (int y = 25; y <= 55; y++) {
for (int x = 25; x <= 55; x++) {
int p = buffer[y * 100 + x];
if (p == red) redCount++;
else if (p == blue) blueCount++;
}
}
assertTrue(redCount > 0, "Expected pattern foreground red pixels");
assertTrue(blueCount > 0, "Expected pattern background blue pixels under MIX_OVER");
}
}
@@ -0,0 +1,71 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class FilletPtsTest {
@Test
public void testEmptyAndSinglePointInputs() {
double[][] empty = FilletPts.calculate((double[]) null, (double[]) null, 0, 10);
assertEquals(0, empty[0].length);
assertEquals(0, empty[1].length);
double[][] single = FilletPts.calculate(new double[] { 10.0 }, new double[] { 20.0 }, 1, 10);
assertEquals(1, single[0].length);
assertEquals(10.0, single[0][0], 1e-6);
assertEquals(20.0, single[1][0], 1e-6);
}
@Test
public void testTwoPointsStraightLine() {
double[] px = new double[] { 10.0, 50.0 };
double[] py = new double[] { 20.0, 80.0 };
double[][] res = FilletPts.calculate(px, py, 2, 10);
assertEquals(2, res[0].length);
assertEquals(10.0, res[0][0], 1e-6);
assertEquals(20.0, res[1][0], 1e-6);
assertEquals(50.0, res[0][1], 1e-6);
assertEquals(80.0, res[1][1], 1e-6);
}
@Test
public void testThreePointsQuadraticSpline() {
// Control points: (0, 0) -> (50, 100) -> (100, 0)
double[] px = new double[] { 0.0, 50.0, 100.0 };
double[] py = new double[] { 0.0, 100.0, 0.0 };
int steps = 10;
double[][] res = FilletPts.calculate(px, py, 3, steps);
// Start point must be (0, 0)
assertEquals(0.0, res[0][0], 1e-6);
assertEquals(0.0, res[1][0], 1e-6);
// End point must be (100, 0)
int last = res[0].length - 1;
assertEquals(100.0, res[0][last], 1e-6);
assertEquals(0.0, res[1][last], 1e-6);
// Curve must be symmetric and positive Y in between
assertTrue(res[0].length > 15);
for (int i = 1; i < last; i++) {
assertTrue(res[1][i] > 0.0, "Y coordinate along upward curve should be positive");
assertTrue(res[0][i] >= 0.0 && res[0][i] <= 100.0);
}
}
@Test
public void testIntegerOverload() {
int[] px = new int[] { 10, 50, 90 };
int[] py = new int[] { 10, 100, 10 };
int[][] res = FilletPts.calculate(px, py, 3, 8);
assertEquals(10, res[0][0]);
assertEquals(10, res[1][0]);
int last = res[0].length - 1;
assertEquals(90, res[0][last]);
assertEquals(10, res[1][last]);
assertTrue(res[0].length > 10);
}
}

Some files were not shown because too many files have changed in this diff Show More