Compare commits
17 Commits
3526e682a1
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
a43969da08
|
|||
|
e18d2f436f
|
|||
|
27976dd31f
|
|||
|
a3c4b95379
|
|||
|
3a576b79b4
|
|||
|
40ebd40fe2
|
|||
|
f310d39fc1
|
|||
|
a44ea00d3a
|
|||
|
037ad1f941
|
|||
|
79258c0c44
|
|||
|
ff5dfda343
|
|||
|
8654b72d8c
|
|||
|
2e9b6d325a
|
|||
|
540a8dfd1d
|
|||
|
ebc0b5ae6f
|
|||
|
bcfd4ba2e0
|
|||
|
2584f4289f
|
@@ -1,13 +0,0 @@
|
|||||||
# Fixed Bugs
|
|
||||||
- Complete failure when using `cp term conmode 3270` under VM:
|
|
||||||
Fixed in InputProcessor by sending raw line-mode EBCDIC character data in SSCP-LU mode instead of 3270 AID headers and 1920-byte buffer dumps, and correctly processing subsequent 3270 stream transitions upon CONMODE 3270.
|
|
||||||
|
|
||||||
- Unable to start a 2nd transfer after first completed ("A transfer is already in progress"):
|
|
||||||
Fixed in FTDft by signaling completion upon handling `TR_CLOSE_REQ` / host completion messages, and adding state reset in FileTransfer.
|
|
||||||
|
|
||||||
- IND$FILE CMS and TSO:
|
|
||||||
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
|
|
||||||
|
|
||||||
- Local keyboard input and cursor updates not rendering in terminal:
|
|
||||||
Fixed in ScreenBuffer, InputProcessor, and TerminalPanel by ensuring display snapshot and cursor address are updated synchronously on user input operations (typing, backspace, delete, cursor movement, erase) so the presentation layer immediately renders user keystrokes.
|
|
||||||
|
|
||||||
@@ -10,8 +10,10 @@ allprojects {
|
|||||||
subprojects {
|
subprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
|
|
||||||
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
|||||||
+1
-4
@@ -118,10 +118,7 @@ public class TestRunner {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
|
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
|
||||||
.selectors(
|
.selectors(
|
||||||
selectPackage("haus.nightmare.lib3270j.graphics"),
|
selectPackage("haus.nightmare.lib3270j")
|
||||||
selectPackage("haus.nightmare.lib3270j.datastream"),
|
|
||||||
selectPackage("haus.nightmare.lib3270j.screen"),
|
|
||||||
selectPackage("haus.nightmare.lib3270j.protocol")
|
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package haus.nightmare.j3270;
|
package haus.nightmare.j3270;
|
||||||
|
|
||||||
import haus.nightmare.lib3270j.*;
|
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.ConnectionListener;
|
||||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import haus.nightmare.j3270.ui.StatusBar;
|
import haus.nightmare.j3270.ui.*;
|
||||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
|
||||||
import haus.nightmare.j3270.ft.FileTransfer;
|
import haus.nightmare.j3270.ft.FileTransfer;
|
||||||
import haus.nightmare.j3270.ft.FileTransferDialog;
|
import haus.nightmare.j3270.ft.FileTransferDialog;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.datatransfer.DataFlavor;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
|
import java.awt.print.PrinterJob;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.logging.*;
|
import java.util.logging.*;
|
||||||
|
|
||||||
@@ -36,10 +39,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
private FileTransfer fileTransfer;
|
private FileTransfer fileTransfer;
|
||||||
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
|
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() {
|
public J3270App() {
|
||||||
super("j3270 — Java TN3270 Terminal Emulator");
|
super("j3270 — Java TN3270 Terminal Emulator");
|
||||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
setBackground(new Color(10, 10, 10));
|
setBackground(Color.BLACK);
|
||||||
|
|
||||||
buildUI();
|
buildUI();
|
||||||
buildMenuBar();
|
buildMenuBar();
|
||||||
@@ -52,7 +61,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
refreshTimer = new Timer(100, e -> {
|
refreshTimer = new Timer(100, e -> {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
statusBar.updateStatus();
|
statusBar.updateStatus();
|
||||||
// Keep focus on terminal panel when window is active
|
|
||||||
if (isActive() && !terminalPanel.hasFocus()) {
|
if (isActive() && !terminalPanel.hasFocus()) {
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
@@ -66,12 +74,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
disconnect();
|
disconnect();
|
||||||
if (refreshTimer != null)
|
if (refreshTimer != null)
|
||||||
refreshTimer.stop();
|
refreshTimer.stop();
|
||||||
|
if (printerSessionDialog != null) {
|
||||||
|
printerSessionDialog.dispose();
|
||||||
|
}
|
||||||
terminalPanel.dispose();
|
terminalPanel.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void windowActivated(WindowEvent e) {
|
public void windowActivated(WindowEvent e) {
|
||||||
// When window gains focus, push to terminal panel
|
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,13 +89,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
private void buildUI() {
|
private void buildUI() {
|
||||||
terminalPanel = new TerminalPanel();
|
terminalPanel = new TerminalPanel();
|
||||||
|
|
||||||
// Status bar
|
|
||||||
statusBar = new StatusBar();
|
statusBar = new StatusBar();
|
||||||
|
|
||||||
// Layout
|
|
||||||
getContentPane().setLayout(new BorderLayout());
|
getContentPane().setLayout(new BorderLayout());
|
||||||
getContentPane().setBackground(new Color(10, 10, 10));
|
getContentPane().setBackground(Color.BLACK);
|
||||||
getContentPane().add(terminalPanel, BorderLayout.CENTER);
|
getContentPane().add(terminalPanel, BorderLayout.CENTER);
|
||||||
getContentPane().add(statusBar, BorderLayout.SOUTH);
|
getContentPane().add(statusBar, BorderLayout.SOUTH);
|
||||||
}
|
}
|
||||||
@@ -95,11 +102,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
menuBar.setBackground(new Color(30, 30, 30));
|
menuBar.setBackground(new Color(30, 30, 30));
|
||||||
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
|
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");
|
JMenu fileMenu = createMenu("File");
|
||||||
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
||||||
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
||||||
fileMenu.addSeparator();
|
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.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
|
||||||
fileMenu.addSeparator();
|
fileMenu.addSeparator();
|
||||||
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
||||||
@@ -108,7 +121,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}));
|
}));
|
||||||
menuBar.add(fileMenu);
|
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");
|
JMenu viewMenu = createMenu("View");
|
||||||
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
|
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
|
||||||
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> 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.setFontSize(16);
|
||||||
terminalPanel.guardedPack();
|
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);
|
menuBar.add(viewMenu);
|
||||||
|
|
||||||
// Actions menu
|
// 4. Actions menu
|
||||||
JMenu actionsMenu = createMenu("Actions");
|
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, () -> {
|
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
|
||||||
if (client != null)
|
if (client != null)
|
||||||
client.sendClear();
|
client.sendClear();
|
||||||
@@ -130,17 +214,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
client.reset();
|
client.reset();
|
||||||
terminalPanel.repaint();
|
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.addSeparator();
|
||||||
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
||||||
terminalPanel.toggleLightPen();
|
terminalPanel.toggleLightPen();
|
||||||
}, true));
|
}, true));
|
||||||
actionsMenu.addSeparator();
|
actionsMenu.addSeparator();
|
||||||
|
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
||||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
||||||
menuBar.add(actionsMenu);
|
menuBar.add(actionsMenu);
|
||||||
|
|
||||||
// Help menu
|
// 5. Help menu
|
||||||
JMenu helpMenu = createMenu("Help");
|
JMenu helpMenu = createMenu("Help");
|
||||||
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
|
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));
|
helpMenu.add(createMenuItem("About", -1, this::showAbout));
|
||||||
menuBar.add(helpMenu);
|
menuBar.add(helpMenu);
|
||||||
|
|
||||||
@@ -169,6 +277,164 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
return createMenuItem(name, mnemonic, action, false);
|
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() {
|
private void showFileTransferDialog() {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
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.setMillisToDecideToPopup(0);
|
||||||
monitor.setMillisToPopup(0);
|
monitor.setMillisToPopup(0);
|
||||||
|
|
||||||
// Start a timer to check for manual cancellation
|
|
||||||
Timer cancelCheckTimer = new Timer(500, e -> {
|
Timer cancelCheckTimer = new Timer(500, e -> {
|
||||||
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
||||||
fileTransfer.cancel();
|
fileTransfer.cancel();
|
||||||
@@ -208,7 +473,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
public void onBytesTransferred(long bytes) {
|
public void onBytesTransferred(long bytes) {
|
||||||
if (monitor != null) {
|
if (monitor != null) {
|
||||||
monitor.setNote(bytes + " bytes transferred");
|
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.setInitialPort(initialPort);
|
||||||
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
||||||
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||||
|
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
||||||
dialog.setVisible(true);
|
dialog.setVisible(true);
|
||||||
|
|
||||||
if (dialog.isConfirmed()) {
|
if (dialog.isConfirmed()) {
|
||||||
@@ -265,16 +530,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
lastHost = config.getHost();
|
lastHost = config.getHost();
|
||||||
lastPort = config.getPort();
|
lastPort = config.getPort();
|
||||||
|
|
||||||
// Save for auto-connect
|
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||||
|
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
||||||
|
|
||||||
// Disconnect existing connection
|
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|
||||||
// Wire interactive certificate verifier if none configured
|
|
||||||
if (config.getCertificateVerifier() == null) {
|
if (config.getCertificateVerifier() == null) {
|
||||||
config.setCertificateVerifier((chain, authType, exception) ->
|
config.setCertificateVerifier((chain, authType, exception) ->
|
||||||
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, 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]") : "";
|
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||||
|
|
||||||
// Resize window to match model
|
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
@@ -307,7 +569,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}, "Connect-Thread").start();
|
}, "Connect-Thread").start();
|
||||||
|
|
||||||
// Focus the terminal panel
|
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,8 +591,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
int current = terminalPanel.getFontSize();
|
int current = terminalPanel.getFontSize();
|
||||||
int newSize = Math.max(8, Math.min(72, current + delta));
|
int newSize = Math.max(8, Math.min(72, current + delta));
|
||||||
terminalPanel.setFontSize(newSize);
|
terminalPanel.setFontSize(newSize);
|
||||||
// Pack after font change — setFontSize sets the resize guard
|
|
||||||
// to prevent the componentResized from re-triggering autoFitFont
|
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +603,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
statusBar.updateStatus();
|
statusBar.updateStatus();
|
||||||
terminalPanel.repaint();
|
terminalPanel.repaint();
|
||||||
|
|
||||||
// Auto-resize and focus on first full session
|
|
||||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
@@ -365,6 +623,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (deviceName != null) {
|
if (deviceName != null) {
|
||||||
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
||||||
}
|
}
|
||||||
|
statusBar.updateStatus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,10 +634,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (screenUpdatePending.compareAndSet(false, true)) {
|
if (screenUpdatePending.compareAndSet(false, true)) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
screenUpdatePending.set(false);
|
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) {
|
if (fileTransfer != null) {
|
||||||
fileTransfer.onScreenUpdated();
|
fileTransfer.onScreenUpdated();
|
||||||
}
|
}
|
||||||
@@ -400,6 +655,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
public void onScreenSizeChanged(int rows, int cols) {
|
public void onScreenSizeChanged(int rows, int cols) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
|
statusBar.updateStatus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,11 +677,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
"Insert — Toggle insert mode\n" +
|
"Insert — Toggle insert mode\n" +
|
||||||
"Escape — Reset\n" +
|
"Escape — Reset\n" +
|
||||||
"PageUp/Down — PF7/PF8\n" +
|
"PageUp/Down — PF7/PF8\n" +
|
||||||
"Alt+C — Clear\n" +
|
"Alt+C / Ctrl+K — Clear\n" +
|
||||||
"Alt+1/2/3 — PA1/PA2/PA3\n" +
|
"Alt+E — Erase Input\n" +
|
||||||
"Cmd+D — Disconnect\n" +
|
"Alt+A — Attention\n" +
|
||||||
"Cmd+Q — Quit\n" +
|
"Alt+S — System Request\n" +
|
||||||
"Cmd+=/-/0 — Font size +/-/reset";
|
"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);
|
JTextArea area = new JTextArea(text);
|
||||||
area.setEditable(false);
|
area.setEditable(false);
|
||||||
@@ -433,18 +696,56 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
area.setBackground(new Color(30, 30, 30));
|
area.setBackground(new Color(30, 30, 30));
|
||||||
area.setForeground(new Color(200, 200, 200));
|
area.setForeground(new Color(200, 200, 200));
|
||||||
JScrollPane sp = new JScrollPane(area);
|
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);
|
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() {
|
private void showAbout() {
|
||||||
JOptionPane.showMessageDialog(this,
|
JOptionPane.showMessageDialog(this,
|
||||||
"j3270 — Java TN3270 Terminal Emulator\n\n" +
|
"j3270 — Java TN3270 Terminal Emulator\n\n" +
|
||||||
"A Java reimplementation of x3270.\n" +
|
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" +
|
||||||
"lib3270j v0.1.0\n\n" +
|
"lib3270j v0.2.0\n\n" +
|
||||||
"Supports: TN3270, TN3270E (RFC 2355)\n" +
|
"Features:\n" +
|
||||||
"Models: IBM 3278/3279 Models 2-5\n" +
|
"• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" +
|
||||||
"Colors, Extended Attributes, Query Replies",
|
"• 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);
|
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +756,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
boolean cliTls = false;
|
boolean cliTls = false;
|
||||||
boolean cliNoVerifyCert = false;
|
boolean cliNoVerifyCert = false;
|
||||||
Boolean cliTn3270e = null;
|
Boolean cliTn3270e = null;
|
||||||
haus.nightmare.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
GraphicsMode cliGraphicsMode = null;
|
||||||
String configFile = null;
|
String configFile = null;
|
||||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
@@ -470,11 +771,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
} else if ("--tn3270e".equals(args[i])) {
|
} else if ("--tn3270e".equals(args[i])) {
|
||||||
cliTn3270e = true;
|
cliTn3270e = true;
|
||||||
} else if (args[i].startsWith("--graphics=")) {
|
} 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("-")) {
|
} 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])) {
|
} 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) {
|
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||||
configFile = args[++i];
|
configFile = args[++i];
|
||||||
} else {
|
} 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;
|
Level logLevel = debug ? Level.ALL : Level.INFO;
|
||||||
Logger globalRoot = Logger.getLogger("");
|
Logger globalRoot = Logger.getLogger("");
|
||||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||||
@@ -522,7 +822,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load INI config file if specified
|
|
||||||
if (configFile != null) {
|
if (configFile != null) {
|
||||||
try {
|
try {
|
||||||
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
||||||
@@ -533,43 +832,38 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dark look and feel
|
|
||||||
try {
|
try {
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.fine("Could not set system look and feel");
|
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.laf.useScreenMenuBar", "true");
|
||||||
System.setProperty("apple.awt.application.name", "j3270");
|
System.setProperty("apple.awt.application.name", "j3270");
|
||||||
|
|
||||||
final boolean finalTls = cliTls;
|
final boolean finalTls = cliTls;
|
||||||
final boolean finalNoVerify = cliNoVerifyCert;
|
final boolean finalNoVerify = cliNoVerifyCert;
|
||||||
final Boolean finalTn3270e = cliTn3270e;
|
final Boolean finalTn3270e = cliTn3270e;
|
||||||
final haus.nightmare.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
J3270App app = new J3270App();
|
J3270App app = new J3270App();
|
||||||
app.setVisible(true);
|
app.setVisible(true);
|
||||||
|
|
||||||
// If host:port given on command line, connect directly
|
|
||||||
if (!remainingArgs.isEmpty()) {
|
if (!remainingArgs.isEmpty()) {
|
||||||
String hostArg = remainingArgs.get(0);
|
String hostArg = remainingArgs.get(0);
|
||||||
int port = finalTls ? 992 : 23;
|
int port = finalTls ? 992 : 23;
|
||||||
if (remainingArgs.size() >= 2) {
|
if (remainingArgs.size() >= 2) {
|
||||||
try {
|
try {
|
||||||
port = Integer.parseInt(remainingArgs.get(1));
|
port = Integer.parseInt(remainingArgs.get(1));
|
||||||
} catch (NumberFormatException ignored) {
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TerminalModel model = TerminalModel.IBM_3279_4;
|
TerminalModel model = TerminalModel.IBM_3279_4;
|
||||||
if (remainingArgs.size() >= 3) {
|
if (remainingArgs.size() >= 3) {
|
||||||
try {
|
try {
|
||||||
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
||||||
model = TerminalModel.forModel(modelNum, true);
|
model = TerminalModel.forModel(modelNum, true);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
||||||
if (finalTls) {
|
if (finalTls) {
|
||||||
|
|||||||
@@ -95,6 +95,14 @@ public class Settings {
|
|||||||
prefs.put("graphicsMode", (mode != null ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH).name());
|
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) {
|
public static Color getColorOverride(int index, Color defaultColor) {
|
||||||
String hex = prefs.get("color_" + index, null);
|
String hex = prefs.get("color_" + index, null);
|
||||||
try {
|
try {
|
||||||
@@ -256,6 +264,11 @@ public class Settings {
|
|||||||
case "enableTn3270e":
|
case "enableTn3270e":
|
||||||
setAutoConnectTn3270e(Boolean.parseBoolean(value));
|
setAutoConnectTn3270e(Boolean.parseBoolean(value));
|
||||||
break;
|
break;
|
||||||
|
case "codePage":
|
||||||
|
case "codepage":
|
||||||
|
case "charset":
|
||||||
|
setCodePage(value);
|
||||||
|
break;
|
||||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||||
default:
|
default:
|
||||||
log.warning("Unknown behavior/connection key: " + key);
|
log.warning("Unknown behavior/connection key: " + key);
|
||||||
@@ -324,6 +337,7 @@ public class Settings {
|
|||||||
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
|
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
|
||||||
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
||||||
}
|
}
|
||||||
|
w.println("codePage = " + getCodePage());
|
||||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||||
w.println();
|
w.println();
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
|||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Telnet3270Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a new file transfer with the given configuration.
|
* Start a new file transfer with the given configuration.
|
||||||
* @return null if started successfully, error message otherwise.
|
* @return null if started successfully, error message otherwise.
|
||||||
@@ -121,6 +125,21 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
|||||||
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
|
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
|
||||||
log.info("User cancelled transfer");
|
log.info("User cancelled transfer");
|
||||||
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance
|
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance
|
||||||
|
// Start a safety timeout in case the host does not send further packets to trigger abort
|
||||||
|
cancelTimeout();
|
||||||
|
timeoutTimer = new Timer("FTCancelTimeout", true);
|
||||||
|
timeoutTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
if (state != FTState.NONE) {
|
||||||
|
log.warning("Transfer cancel timeout — force resetting state");
|
||||||
|
completeTransfer("Transfer cancelled by user.");
|
||||||
|
callback.onTransferAborted("Cancelled by user.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
} else if (state != FTState.NONE) {
|
} else if (state != FTState.NONE) {
|
||||||
log.info("Forcing cancel from state " + state);
|
log.info("Forcing cancel from state " + state);
|
||||||
completeTransfer("Transfer cancelled.");
|
completeTransfer("Transfer cancelled.");
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package haus.nightmare.j3270.ft;
|
package haus.nightmare.j3270.ft;
|
||||||
|
|
||||||
|
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||||
|
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.border.EmptyBorder;
|
import javax.swing.border.EmptyBorder;
|
||||||
@@ -18,6 +20,8 @@ public class FileTransferDialog extends JDialog {
|
|||||||
private JTextField localFileField;
|
private JTextField localFileField;
|
||||||
private JButton browseLocalButton;
|
private JButton browseLocalButton;
|
||||||
private JTextField hostFileField;
|
private JTextField hostFileField;
|
||||||
|
private JButton browseHostButton;
|
||||||
|
private JComboBox<Integer> mtuCombo;
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
private JRadioButton asciiRadio;
|
private JRadioButton asciiRadio;
|
||||||
@@ -40,7 +44,7 @@ public class FileTransferDialog extends JDialog {
|
|||||||
private JButton cancelButton;
|
private JButton cancelButton;
|
||||||
|
|
||||||
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
|
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.owner = owner;
|
||||||
this.coordinator = coordinator;
|
this.coordinator = coordinator;
|
||||||
|
|
||||||
@@ -76,7 +80,6 @@ public class FileTransferDialog extends JDialog {
|
|||||||
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
|
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
|
||||||
dirPanel.add(sendRadio);
|
dirPanel.add(sendRadio);
|
||||||
|
|
||||||
// Disable append/overwrite when sending
|
|
||||||
sendRadio.addActionListener(e -> updateOptionStates());
|
sendRadio.addActionListener(e -> updateOptionStates());
|
||||||
receiveRadio.addActionListener(e -> updateOptionStates());
|
receiveRadio.addActionListener(e -> updateOptionStates());
|
||||||
|
|
||||||
@@ -94,7 +97,13 @@ public class FileTransferDialog extends JDialog {
|
|||||||
|
|
||||||
// Host File
|
// Host File
|
||||||
hostFileField = new JTextField(20);
|
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
|
// Mode
|
||||||
asciiRadio = new JRadioButton("ASCII");
|
asciiRadio = new JRadioButton("ASCII");
|
||||||
@@ -114,6 +123,11 @@ public class FileTransferDialog extends JDialog {
|
|||||||
|
|
||||||
formPanel.add(createRow("Transfer Mode:", modePanel));
|
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)
|
// Options (checkboxes)
|
||||||
crCheck = new JCheckBox("Add/Remove CR");
|
crCheck = new JCheckBox("Add/Remove CR");
|
||||||
crCheck.setSelected(true);
|
crCheck.setSelected(true);
|
||||||
@@ -198,10 +212,7 @@ public class FileTransferDialog extends JDialog {
|
|||||||
|
|
||||||
setContentPane(mainPanel);
|
setContentPane(mainPanel);
|
||||||
|
|
||||||
// Theme styling for components
|
|
||||||
applyTheme(mainPanel);
|
applyTheme(mainPanel);
|
||||||
|
|
||||||
// Initialize state
|
|
||||||
updateOptionStates();
|
updateOptionStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,14 +247,29 @@ public class FileTransferDialog extends JDialog {
|
|||||||
JFileChooser chooser = new JFileChooser();
|
JFileChooser chooser = new JFileChooser();
|
||||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
|
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
|
||||||
|
|
||||||
// Auto-fill host file if empty
|
|
||||||
if (hostFileField.getText().trim().isEmpty()) {
|
if (hostFileField.getText().trim().isEmpty()) {
|
||||||
hostFileField.setText(chooser.getSelectedFile().getName());
|
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() {
|
private void startTransfer() {
|
||||||
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
|
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
|
||||||
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
|
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
|
||||||
@@ -262,6 +288,11 @@ public class FileTransferDialog extends JDialog {
|
|||||||
config.setAppend(appendCheck.isSelected());
|
config.setAppend(appendCheck.isSelected());
|
||||||
config.setOverwrite(overwriteCheck.isSelected());
|
config.setOverwrite(overwriteCheck.isSelected());
|
||||||
|
|
||||||
|
Integer mtu = (Integer) mtuCombo.getSelectedItem();
|
||||||
|
if (mtu != null) {
|
||||||
|
config.setDftBufferSize(mtu);
|
||||||
|
}
|
||||||
|
|
||||||
config.setRecfm(recfmField.getText().trim());
|
config.setRecfm(recfmField.getText().trim());
|
||||||
config.setLrecl(lreclField.getText().trim());
|
config.setLrecl(lreclField.getText().trim());
|
||||||
config.setBlksize(blksizeField.getText().trim());
|
config.setBlksize(blksizeField.getText().trim());
|
||||||
@@ -272,7 +303,6 @@ public class FileTransferDialog extends JDialog {
|
|||||||
if (error != null) {
|
if (error != null) {
|
||||||
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||||
} else {
|
} else {
|
||||||
// Success, close dialog (progress will be shown separately)
|
|
||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
private JTextField portField;
|
private JTextField portField;
|
||||||
private JComboBox<TerminalModel> modelCombo;
|
private JComboBox<TerminalModel> modelCombo;
|
||||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
||||||
|
private JComboBox<String> codePageCombo;
|
||||||
private JTextField luField;
|
private JTextField luField;
|
||||||
private JCheckBox tlsCheckBox;
|
private JCheckBox tlsCheckBox;
|
||||||
private JCheckBox verifyCertCheckBox;
|
private JCheckBox verifyCertCheckBox;
|
||||||
@@ -114,9 +115,57 @@ public class ConnectDialog extends JDialog {
|
|||||||
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||||
mainPanel.add(graphicsCombo, gbc);
|
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
|
// TLS / SSL Checkbox
|
||||||
gbc.gridx = 1;
|
gbc.gridx = 1;
|
||||||
gbc.gridy = 5;
|
gbc.gridy = 6;
|
||||||
gbc.weightx = 1.0;
|
gbc.weightx = 1.0;
|
||||||
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
||||||
tlsCheckBox.setBackground(new Color(30, 30, 30));
|
tlsCheckBox.setBackground(new Color(30, 30, 30));
|
||||||
@@ -137,7 +186,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
|
|
||||||
// Verify Certificate Checkbox
|
// Verify Certificate Checkbox
|
||||||
gbc.gridx = 1;
|
gbc.gridx = 1;
|
||||||
gbc.gridy = 6;
|
gbc.gridy = 7;
|
||||||
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
||||||
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
|
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
|
||||||
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
|
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
|
||||||
@@ -149,7 +198,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
|
|
||||||
// TN3270E Checkbox
|
// TN3270E Checkbox
|
||||||
gbc.gridx = 1;
|
gbc.gridx = 1;
|
||||||
gbc.gridy = 7;
|
gbc.gridy = 8;
|
||||||
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
|
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
|
||||||
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
|
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
|
||||||
tn3270eCheckBox.setForeground(fg);
|
tn3270eCheckBox.setForeground(fg);
|
||||||
@@ -181,7 +230,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
buttonPanel.add(connectBtn);
|
buttonPanel.add(connectBtn);
|
||||||
|
|
||||||
gbc.gridx = 0;
|
gbc.gridx = 0;
|
||||||
gbc.gridy = 8;
|
gbc.gridy = 9;
|
||||||
gbc.gridwidth = 2;
|
gbc.gridwidth = 2;
|
||||||
mainPanel.add(buttonPanel, gbc);
|
mainPanel.add(buttonPanel, gbc);
|
||||||
|
|
||||||
@@ -224,6 +273,18 @@ public class ConnectDialog extends JDialog {
|
|||||||
result.setLuName(lu);
|
result.setLuName(lu);
|
||||||
}
|
}
|
||||||
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
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.setUseTls(tlsCheckBox.isSelected());
|
||||||
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||||
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
||||||
@@ -260,4 +321,17 @@ public class ConnectDialog extends JDialog {
|
|||||||
public void setInitialTn3270e(boolean tn3270e) {
|
public void setInitialTn3270e(boolean tn3270e) {
|
||||||
tn3270eCheckBox.setSelected(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 "<";
|
||||||
|
case '>': return ">";
|
||||||
|
case '&': return "&";
|
||||||
|
case '"': return """;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -453,7 +453,8 @@ public class SettingsDialog extends JDialog {
|
|||||||
JPanel main = new JPanel(new BorderLayout());
|
JPanel main = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||||
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
|
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR",
|
||||||
|
"ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL"};
|
||||||
|
|
||||||
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
|
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
|
||||||
@Override
|
@Override
|
||||||
@@ -465,7 +466,14 @@ public class SettingsDialog extends JDialog {
|
|||||||
// Populate table from Settings or Defaults
|
// Populate table from Settings or Defaults
|
||||||
for(String act : actions) {
|
for(String act : actions) {
|
||||||
String def = act;
|
String def = act;
|
||||||
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
|
if (def.equals("ERASE_INPUT")) def = "alt E";
|
||||||
|
else if (def.equals("NEWLINE")) def = "shift ENTER";
|
||||||
|
else if (def.equals("DUP")) def = "alt D";
|
||||||
|
else if (def.equals("FIELD_MARK")) def = "alt M";
|
||||||
|
else if (def.equals("ATTN")) def = "alt A";
|
||||||
|
else if (def.equals("SYSREQ")) def = "alt S";
|
||||||
|
else if (def.equals("CURSEL")) def = "alt Q";
|
||||||
|
else if (def.equals("CLEAR")) def = "alt C";
|
||||||
String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
|
String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
|
||||||
tempKeyBindings.put(act, current);
|
tempKeyBindings.put(act, current);
|
||||||
keymapModel.addRow(new Object[]{act, current});
|
keymapModel.addRow(new Object[]{act, current});
|
||||||
|
|||||||
@@ -2,25 +2,26 @@ package haus.nightmare.j3270.ui;
|
|||||||
|
|
||||||
import haus.nightmare.lib3270j.ConnectionState;
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status bar displaying connection state, cursor position, timing, and lock
|
* Status bar displaying connection state, cursor position, timing, and lock status.
|
||||||
* status.
|
|
||||||
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
|
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
|
||||||
*/
|
*/
|
||||||
public class StatusBar extends JPanel {
|
public class StatusBar extends JPanel {
|
||||||
|
|
||||||
private final JLabel connectionStatus;
|
private final JLabel connectionStatus;
|
||||||
private final JLabel tlsStatus;
|
private final JLabel tlsStatus;
|
||||||
private final JLabel cursorPosition;
|
|
||||||
private final JLabel luName;
|
private final JLabel luName;
|
||||||
private final JLabel lockStatus;
|
private final JLabel lockStatus;
|
||||||
|
private final JLabel fieldTypeStatus;
|
||||||
|
private final JLabel codePageInfo;
|
||||||
private final JLabel modelInfo;
|
private final JLabel modelInfo;
|
||||||
private final JButton lpButton;
|
private final JLabel cursorPosition;
|
||||||
|
|
||||||
private Telnet3270Client client;
|
private Telnet3270Client client;
|
||||||
private TerminalPanel terminalPanel;
|
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_FG = new Color(50, 205, 50);
|
||||||
private static final Color OIA_DIM = new Color(80, 80, 80);
|
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_ALERT = new Color(255, 80, 80);
|
||||||
|
private static final Color OIA_WARN = new Color(255, 200, 80);
|
||||||
|
|
||||||
public StatusBar() {
|
public StatusBar() {
|
||||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||||
@@ -43,33 +45,24 @@ public class StatusBar extends JPanel {
|
|||||||
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
||||||
luName = createLabel("", oiaFont, OIA_FG);
|
luName = createLabel("", oiaFont, OIA_FG);
|
||||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||||
|
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
|
||||||
|
codePageInfo = createLabel("", oiaFont, OIA_DIM);
|
||||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||||
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
|
cursorPosition = createLabel("001/001 [0000]", 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"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
add(Box.createHorizontalStrut(6));
|
add(Box.createHorizontalStrut(6));
|
||||||
add(connectionStatus);
|
add(connectionStatus);
|
||||||
add(Box.createHorizontalStrut(10));
|
add(Box.createHorizontalStrut(10));
|
||||||
add(tlsStatus);
|
add(tlsStatus);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(10));
|
||||||
add(luName);
|
add(luName);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
add(lockStatus);
|
add(lockStatus);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(10));
|
||||||
add(lpButton);
|
add(fieldTypeStatus);
|
||||||
add(Box.createHorizontalGlue());
|
add(Box.createHorizontalGlue());
|
||||||
|
add(codePageInfo);
|
||||||
|
add(Box.createHorizontalStrut(12));
|
||||||
add(modelInfo);
|
add(modelInfo);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
add(cursorPosition);
|
add(cursorPosition);
|
||||||
@@ -86,15 +79,22 @@ public class StatusBar extends JPanel {
|
|||||||
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.terminalPanel = terminalPanel;
|
this.terminalPanel = terminalPanel;
|
||||||
if (terminalPanel != null) {
|
|
||||||
terminalPanel.setOnLightPenToggle(this::updateStatus);
|
|
||||||
}
|
|
||||||
updateStatus();
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void 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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Connection state
|
// Connection state
|
||||||
ConnectionState state = client.getConnectionState();
|
ConnectionState state = client.getConnectionState();
|
||||||
@@ -128,7 +128,7 @@ public class StatusBar extends JPanel {
|
|||||||
break;
|
break;
|
||||||
case CONNECTED_UNBOUND:
|
case CONNECTED_UNBOUND:
|
||||||
connectionStatus.setText("Unbound");
|
connectionStatus.setText("Unbound");
|
||||||
connectionStatus.setForeground(new Color(255, 255, 80));
|
connectionStatus.setForeground(OIA_WARN);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
connectionStatus.setText(state.name());
|
connectionStatus.setText(state.name());
|
||||||
@@ -156,16 +156,41 @@ public class StatusBar extends JPanel {
|
|||||||
tlsStatus.setToolTipText(null);
|
tlsStatus.setToolTipText(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LU name
|
// LU Name
|
||||||
String lu = "";
|
String lu = "";
|
||||||
if (client.getConnectionState().isTn3270e()) {
|
if (client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null && !client.getTelnetFSM().getConnectedLu().isEmpty()) {
|
||||||
// lu would come from FSM
|
lu = "LU:" + client.getTelnetFSM().getConnectedLu();
|
||||||
|
} else if (client.getConfig().getLuName() != null && !client.getConfig().getLuName().isEmpty()) {
|
||||||
|
lu = "LU:" + client.getConfig().getLuName();
|
||||||
}
|
}
|
||||||
luName.setText(lu);
|
luName.setText(lu);
|
||||||
|
|
||||||
// Lock status
|
// Lock / Inhibit status
|
||||||
if (client.getInputProcessor().isKeyboardLocked()) {
|
int inhibit = client.getOIA().getInputInhibited();
|
||||||
|
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
|
||||||
|
switch (inhibit) {
|
||||||
|
case ECLConstants.INHIBIT_SYSTEM_LOCK:
|
||||||
lockStatus.setText("X SYSTEM");
|
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);
|
lockStatus.setForeground(OIA_ALERT);
|
||||||
} else if (client.getInputProcessor().isInsertMode()) {
|
} else if (client.getInputProcessor().isInsertMode()) {
|
||||||
lockStatus.setText("INSERT");
|
lockStatus.setText("INSERT");
|
||||||
@@ -174,19 +199,34 @@ public class StatusBar extends JPanel {
|
|||||||
lockStatus.setText("");
|
lockStatus.setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model info
|
// Field status: Numeric vs Alphanumeric
|
||||||
modelInfo.setText(client.getConfig().getModel().getTerminalType());
|
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();
|
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 row = sb.getCursorRow() + 1;
|
||||||
int col = sb.getCursorCol() + 1;
|
int col = sb.getCursorCol() + 1;
|
||||||
cursorPosition.setText(String.format("%03d/%03d", row, col));
|
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package haus.nightmare.j3270.ui;
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
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.
|
* Custom JPanel that renders the 3270 screen buffer.
|
||||||
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
|
* Supports colors, bold, underline, reverse, blink, GOCA vector graphics,
|
||||||
* attributes.
|
* 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;
|
private Telnet3270Client client;
|
||||||
|
|
||||||
@@ -62,7 +63,12 @@ public class TerminalPanel extends JPanel {
|
|||||||
private boolean isDragging = false;
|
private boolean isDragging = false;
|
||||||
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
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 boolean lightPenMode = false;
|
||||||
private long lastGraphicsUpdateCount = -1;
|
private long lastGraphicsUpdateCount = -1;
|
||||||
private java.awt.image.BufferedImage cachedGraphicsImage = null;
|
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.
|
* 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() {
|
private int getRenderOffsetX() {
|
||||||
int termCols = 80;
|
int termCols = 80;
|
||||||
@@ -82,7 +87,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the vertical render offset to center the grid within the panel.
|
* Compute the vertical render offset to center the grid within the panel.
|
||||||
* Must use getDisplayRows() to match paintComponent's iteration.
|
|
||||||
*/
|
*/
|
||||||
private int getRenderOffsetY() {
|
private int getRenderOffsetY() {
|
||||||
int termRows = 24;
|
int termRows = 24;
|
||||||
@@ -119,7 +123,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
|
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
|
||||||
|
|
||||||
// Default Background
|
// Default Background
|
||||||
public static final Color DEFAULT_BG_COLOR = new Color(10, 10, 10);
|
public static final Color DEFAULT_BG_COLOR = Color.BLACK;
|
||||||
|
|
||||||
public TerminalPanel() {
|
public TerminalPanel() {
|
||||||
setupColors();
|
setupColors();
|
||||||
@@ -128,8 +132,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
setDoubleBuffered(true);
|
setDoubleBuffered(true);
|
||||||
setFocusTraversalKeysEnabled(false);
|
setFocusTraversalKeysEnabled(false);
|
||||||
|
|
||||||
// Use key bindings instead of KeyListener for reliable key handling
|
|
||||||
// This avoids focus/event issues with JScrollPane
|
|
||||||
setOpaque(true);
|
setOpaque(true);
|
||||||
setupKeyBindings();
|
setupKeyBindings();
|
||||||
setupFont();
|
setupFont();
|
||||||
@@ -160,13 +162,17 @@ public class TerminalPanel extends JPanel {
|
|||||||
col = Math.max(0, Math.min(col, displayCols - 1));
|
col = Math.max(0, Math.min(col, displayCols - 1));
|
||||||
row = Math.max(0, Math.min(row, displayRows - 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 mode (Alt+L on 3270 formatted screens), clicks toggle selectable fields.
|
|
||||||
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
|
|
||||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||||
if (isGraphic || lightPenMode) {
|
boolean isSelectableField = false;
|
||||||
|
if (sb.isFormatted()) {
|
||||||
|
int faPos = sb.findFieldAttribute(row * displayCols + col);
|
||||||
|
if (faPos >= 0) {
|
||||||
|
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||||
|
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||||
|
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isGraphic || lightPenMode || isSelectableField) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
} else {
|
} else {
|
||||||
selectionStartRow = row;
|
selectionStartRow = row;
|
||||||
@@ -215,8 +221,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
int ox = getRenderOffsetX();
|
int ox = getRenderOffsetX();
|
||||||
int oy = getRenderOffsetY();
|
int oy = getRenderOffsetY();
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
int gridW = sb.getDisplayCols() * cellWidth;
|
int gridW = (sb != null ? sb.getDisplayCols() : 80) * cellWidth;
|
||||||
int gridH = sb.getDisplayRows() * cellHeight;
|
int gridH = (sb != null ? sb.getDisplayRows() : 24) * cellHeight;
|
||||||
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||||
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
||||||
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
|
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
|
||||||
@@ -247,6 +253,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
if (isGraphic) {
|
if (isGraphic) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
sb.setCursorAddress(clickAddr);
|
||||||
int gridW = displayCols * cellWidth;
|
int gridW = displayCols * cellWidth;
|
||||||
int gridH = displayRows * cellHeight;
|
int gridH = displayRows * cellHeight;
|
||||||
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||||
@@ -257,14 +264,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||||
|
|
||||||
int gx = client.getGocaDecoder().getGraphicCursorX();
|
|
||||||
int gy = client.getGocaDecoder().getGraphicCursorY();
|
|
||||||
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
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) btn=%d",
|
|
||||||
e.getX(), e.getY(), ox, oy, gridW, gridH, px, py, gx, gy, button
|
|
||||||
));
|
|
||||||
|
|
||||||
client.getInputProcessor().sendGraphicMouseAid(
|
client.getInputProcessor().sendGraphicMouseAid(
|
||||||
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
||||||
button,
|
button,
|
||||||
@@ -275,13 +275,31 @@ public class TerminalPanel extends JPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatic Light-Pen / Selectable Field detection on mouse click:
|
||||||
|
if (sb.isFormatted()) {
|
||||||
|
int faPos = sb.findFieldAttribute(clickAddr);
|
||||||
|
if (faPos >= 0) {
|
||||||
|
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||||
|
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||||
|
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||||
|
clearSelection();
|
||||||
|
if (client.lightPenSelect(clickAddr)) {
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (lightPenMode) {
|
if (lightPenMode) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
sb.setCursorAddress(clickAddr);
|
sb.setCursorAddress(clickAddr);
|
||||||
boolean result = client.getInputProcessor().lightPenSelect(clickAddr);
|
boolean result = client.lightPenSelect(clickAddr);
|
||||||
if (result) {
|
if (result) {
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,23 +329,42 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
// ========== Selection / Copy-Paste ==========
|
// ========== Selection / Copy-Paste ==========
|
||||||
|
|
||||||
private void clearSelection() {
|
public void clearSelection() {
|
||||||
selectionStartRow = -1;
|
selectionStartRow = -1;
|
||||||
selectionStartCol = -1;
|
selectionStartCol = -1;
|
||||||
selectionEndRow = -1;
|
selectionEndRow = -1;
|
||||||
selectionEndCol = -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 &&
|
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
|
||||||
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
|
!(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() {
|
public String getSelectedText() {
|
||||||
if (!hasSelection() || client == null) return "";
|
if (!hasSelection() || client == null) return "";
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
@@ -338,7 +375,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
int c1, c2;
|
int c1, c2;
|
||||||
|
|
||||||
if (blockSelectMode) {
|
if (blockSelectMode) {
|
||||||
// Block mode: rectangular selection
|
|
||||||
c1 = Math.min(selectionStartCol, selectionEndCol);
|
c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||||
c2 = Math.max(selectionStartCol, selectionEndCol);
|
c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||||
|
|
||||||
@@ -358,7 +394,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
return result.toString();
|
return result.toString();
|
||||||
} else {
|
} else {
|
||||||
// Line/stream mode: flow from start to end
|
|
||||||
int startAddr, endAddr;
|
int startAddr, endAddr;
|
||||||
if (selectionStartRow < selectionEndRow ||
|
if (selectionStartRow < selectionEndRow ||
|
||||||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||||
@@ -389,7 +424,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copySelection() {
|
public void copySelection() {
|
||||||
String text = getSelectedText();
|
String text = getSelectedText();
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
StringSelection ss = new StringSelection(text);
|
StringSelection ss = new StringSelection(text);
|
||||||
@@ -397,7 +432,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pasteClipboard() {
|
public void pasteClipboard() {
|
||||||
if (client == null || !client.getConnectionState().isFullSession()) return;
|
if (client == null || !client.getConnectionState().isFullSession()) return;
|
||||||
try {
|
try {
|
||||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
@@ -405,8 +440,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (text != null) {
|
if (text != null) {
|
||||||
for (char ch : text.toCharArray()) {
|
for (char ch : text.toCharArray()) {
|
||||||
if (ch == '\n' || ch == '\r') {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if (ch >= 0x20 && ch != 0x7F) {
|
if (ch >= 0x20 && ch != 0x7F) {
|
||||||
@@ -415,9 +448,35 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ignored) {}
|
||||||
// Clipboard not available or wrong type — silently ignore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private void showContextMenu(MouseEvent e) {
|
||||||
@@ -433,6 +492,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
pasteItem.addActionListener(ev -> pasteClipboard());
|
pasteItem.addActionListener(ev -> pasteClipboard());
|
||||||
popup.add(pasteItem);
|
popup.add(pasteItem);
|
||||||
|
|
||||||
|
JMenuItem selectAllItem = new JMenuItem("Select All");
|
||||||
|
selectAllItem.addActionListener(ev -> selectAll());
|
||||||
|
popup.add(selectAllItem);
|
||||||
|
|
||||||
popup.addSeparator();
|
popup.addSeparator();
|
||||||
|
|
||||||
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||||
@@ -456,7 +519,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||||
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
||||||
} else {
|
} else {
|
||||||
// Stream mode
|
|
||||||
int cols = 80;
|
int cols = 80;
|
||||||
if (client != null) cols = client.getScreenBuffer().getCols();
|
if (client != null) cols = client.getScreenBuffer().getCols();
|
||||||
int addr = row * cols + col;
|
int addr = row * cols + col;
|
||||||
@@ -475,12 +537,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
// ========== Font auto-resize ==========
|
// ========== 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() {
|
private void autoFitFont() {
|
||||||
int panelW = getWidth();
|
int panelW = getWidth();
|
||||||
int panelH = getHeight();
|
int panelH = getHeight();
|
||||||
@@ -494,12 +550,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
termRows = sb.getDisplayRows();
|
termRows = sb.getDisplayRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use minimal padding for the fit calculation
|
|
||||||
int availW = panelW - 2 * padding;
|
int availW = panelW - 2 * padding;
|
||||||
int availH = panelH - 2 * padding;
|
int availH = panelH - 2 * padding;
|
||||||
if (availW <= 0 || availH <= 0) return;
|
if (availW <= 0 || availH <= 0) return;
|
||||||
|
|
||||||
// Find the largest font size where the grid fits
|
|
||||||
int bestSize = 8;
|
int bestSize = 8;
|
||||||
for (int testSize = 8; testSize <= 72; testSize++) {
|
for (int testSize = 8; testSize <= 72; testSize++) {
|
||||||
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
|
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
|
||||||
@@ -520,15 +574,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
||||||
updateCellSize();
|
updateCellSize();
|
||||||
}
|
}
|
||||||
// No window snap — any leftover pixels are centered via getRenderOffsetX/Y
|
|
||||||
repaint();
|
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() {
|
public void guardedPack() {
|
||||||
resizeGuard = true;
|
resizeGuard = true;
|
||||||
revalidate();
|
revalidate();
|
||||||
@@ -539,16 +587,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
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) {
|
private void bindKeyToMap(InputMap im, String action, String bindingStr) {
|
||||||
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
|
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Support multiple bindings separated by commas
|
|
||||||
String[] bindings = bindingStr.split(",");
|
String[] bindings = bindingStr.split(",");
|
||||||
for (String binding : bindings) {
|
for (String binding : bindings) {
|
||||||
binding = binding.trim();
|
binding = binding.trim();
|
||||||
@@ -568,7 +610,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
im.clear();
|
im.clear();
|
||||||
am.clear();
|
am.clear();
|
||||||
|
|
||||||
// Block the scroll pane from handling Tab, arrows, Page keys
|
|
||||||
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||||
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
||||||
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
||||||
@@ -578,25 +619,16 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PF keys 1-24
|
// 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++) {
|
for (int i = 1; i <= 24; i++) {
|
||||||
String defaultBinding;
|
String defaultBinding = i <= 12 ? ("F" + i) : ("shift F" + (i - 12));
|
||||||
if (i <= 12) {
|
|
||||||
defaultBinding = "F" + i;
|
|
||||||
} else {
|
|
||||||
defaultBinding = "shift F" + (i - 12);
|
|
||||||
}
|
|
||||||
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
||||||
bindKeyToMap(im, "PF" + i, binding);
|
bindKeyToMap(im, "PF" + i, binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PA keys: Alt+1, Alt+2, Alt+3
|
// PA keys
|
||||||
String pa1Def = "alt 1";
|
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", "alt 1"));
|
||||||
String pa2Def = "alt 2";
|
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", "alt 2"));
|
||||||
String pa3Def = "alt 3";
|
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", "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));
|
|
||||||
|
|
||||||
// Clear
|
// Clear
|
||||||
bindKeyToMap(im, "CLEAR", haus.nightmare.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
bindKeyToMap(im, "CLEAR", haus.nightmare.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
||||||
@@ -608,14 +640,16 @@ public class TerminalPanel extends JPanel {
|
|||||||
bindKeyToMap(im, "FIELD_MARK", haus.nightmare.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
bindKeyToMap(im, "FIELD_MARK", haus.nightmare.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
||||||
bindKeyToMap(im, "ATTN", haus.nightmare.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
bindKeyToMap(im, "ATTN", haus.nightmare.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
||||||
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
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();
|
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
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_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");
|
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-ENTER", createAction(this::handleEnter));
|
||||||
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
||||||
am.put("j3270-TAB", createAction(() -> handleTab(false)));
|
am.put("j3270-TAB", createAction(() -> handleTab(false)));
|
||||||
@@ -638,10 +672,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
|
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
|
||||||
am.put("j3270-ATTN", createAction(this::handleAttn));
|
am.put("j3270-ATTN", createAction(this::handleAttn));
|
||||||
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
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-COPY", createAction(this::copySelection));
|
||||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||||
|
am.put("j3270-SELECTALL", createAction(this::selectAll));
|
||||||
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
||||||
|
|
||||||
for (int i = 1; i <= 24; i++) {
|
for (int i = 1; i <= 24; i++) {
|
||||||
@@ -653,18 +688,24 @@ public class TerminalPanel extends JPanel {
|
|||||||
am.put("j3270-PA2", createAction(() -> handlePA(2)));
|
am.put("j3270-PA2", createAction(() -> handlePA(2)));
|
||||||
am.put("j3270-PA3", createAction(() -> handlePA(3)));
|
am.put("j3270-PA3", createAction(() -> handlePA(3)));
|
||||||
|
|
||||||
// For printable character input, we override processKeyEvent
|
|
||||||
enableEvents(AWTEvent.KEY_EVENT_MASK);
|
enableEvents(AWTEvent.KEY_EVENT_MASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void processKeyEvent(KeyEvent e) {
|
protected void processKeyEvent(KeyEvent e) {
|
||||||
// Handle character typing via processKeyEvent to capture ALL typed chars
|
|
||||||
if (e.getID() == KeyEvent.KEY_TYPED) {
|
if (e.getID() == KeyEvent.KEY_TYPED) {
|
||||||
char ch = e.getKeyChar();
|
char ch = e.getKeyChar();
|
||||||
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
||||||
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
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);
|
client.typeCharacter(ch);
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
e.consume();
|
e.consume();
|
||||||
@@ -672,6 +713,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
super.processKeyEvent(e);
|
super.processKeyEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,14 +729,23 @@ public class TerminalPanel extends JPanel {
|
|||||||
// ========== Key action handlers ==========
|
// ========== Key action handlers ==========
|
||||||
|
|
||||||
private void handleEnter() {
|
private void handleEnter() {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
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();
|
client.sendEnter();
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleReset() {
|
private void handleReset() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
|
clearSearchHighlight();
|
||||||
|
clearSelection();
|
||||||
client.reset();
|
client.reset();
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
@@ -702,10 +753,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
private void handleTab(boolean shift) {
|
private void handleTab(boolean shift) {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
if (shift)
|
if (shift) client.backTab();
|
||||||
client.backTab();
|
else client.tab();
|
||||||
else
|
|
||||||
client.tab();
|
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -713,21 +762,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
private void handleCursor(String dir) {
|
private void handleCursor(String dir) {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
switch (dir) {
|
switch (dir) {
|
||||||
case "up":
|
case "up": client.cursorUp(); break;
|
||||||
client.cursorUp();
|
case "down": client.cursorDown(); break;
|
||||||
break;
|
case "left": client.cursorLeft(); break;
|
||||||
case "down":
|
case "right": client.cursorRight(); break;
|
||||||
client.cursorDown();
|
case "home": client.cursorHome(); break;
|
||||||
break;
|
|
||||||
case "left":
|
|
||||||
client.cursorLeft();
|
|
||||||
break;
|
|
||||||
case "right":
|
|
||||||
client.cursorRight();
|
|
||||||
break;
|
|
||||||
case "home":
|
|
||||||
client.cursorHome();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
@@ -762,11 +801,18 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleBackspace() {
|
private void handleBackspace() {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
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();
|
client.getInputProcessor().backspace();
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleInsert() {
|
private void handleInsert() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
@@ -824,12 +870,22 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshScreen() {
|
private void handleCursorSelect() {
|
||||||
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
|
boolean selected = client.cursorSelect();
|
||||||
|
if (selected) {
|
||||||
|
refreshScreen();
|
||||||
|
} else {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshScreen() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.getScreenBuffer().updateDisplaySnapshot();
|
client.getScreenBuffer().updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
repaint();
|
repaint();
|
||||||
// Notify parent to update status bar too
|
|
||||||
Container parent = getParent();
|
Container parent = getParent();
|
||||||
while (parent != null) {
|
while (parent != null) {
|
||||||
if (parent instanceof JFrame) {
|
if (parent instanceof JFrame) {
|
||||||
@@ -846,7 +902,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
||||||
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
||||||
// Fallback
|
|
||||||
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
|
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
|
||||||
}
|
}
|
||||||
updateCellSize();
|
updateCellSize();
|
||||||
@@ -930,8 +985,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||||
|
|
||||||
int fontSize = (int) Math.round(ch * 0.95);
|
int fontSize = Math.min((int) Math.round(ch * 0.82), (int) Math.round(cw * 1.45));
|
||||||
if (fontSize < 10) fontSize = 10;
|
if (fontSize < 9) fontSize = 9;
|
||||||
Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize);
|
Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize);
|
||||||
g2.setFont(f);
|
g2.setFont(f);
|
||||||
FontMetrics fm = g2.getFontMetrics();
|
FontMetrics fm = g2.getFontMetrics();
|
||||||
@@ -941,11 +996,17 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
double curX = x;
|
double curX = x;
|
||||||
double curY = y;
|
double curY = y;
|
||||||
|
if (dir == GocaConstants.CD_TB) {
|
||||||
|
curY += ch;
|
||||||
|
} else if (dir == GocaConstants.CD_RL) {
|
||||||
|
curX -= cw;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
for (int i = 0; i < text.length(); i++) {
|
||||||
String s = text.substring(i, i + 1);
|
String s = text.substring(i, i + 1);
|
||||||
int charW = fm.stringWidth(s);
|
int charW = fm.stringWidth(s);
|
||||||
int drawX = (int) Math.round(curX + Math.max(0, (cw - charW) / 2.0));
|
int drawX = (int) Math.round(curX + Math.max(0, (cw - charW) / 2.0));
|
||||||
int drawY = (int) Math.round(curY + ascent + Math.max(0, (ch - fm.getHeight()) / 2.0));
|
int drawY = (int) Math.round(curY);
|
||||||
g2.drawString(s, drawX, drawY);
|
g2.drawString(s, drawX, drawY);
|
||||||
|
|
||||||
switch (dir) {
|
switch (dir) {
|
||||||
@@ -969,9 +1030,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
public Dimension getPreferredSize() {
|
public Dimension getPreferredSize() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
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 displayCols = sb.getDisplayCols();
|
||||||
int displayRows = sb.getDisplayRows();
|
int displayRows = sb.getDisplayRows();
|
||||||
return new Dimension(displayCols * cellWidth + padding * 2,
|
return new Dimension(displayCols * cellWidth + padding * 2,
|
||||||
@@ -996,11 +1054,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||||
|
|
||||||
// Clear entire panel with background color
|
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||||
|
|
||||||
// Compute centered offsets for the grid
|
|
||||||
int ox = getRenderOffsetX();
|
int ox = getRenderOffsetX();
|
||||||
int oy = getRenderOffsetY();
|
int oy = getRenderOffsetY();
|
||||||
|
|
||||||
@@ -1031,7 +1087,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track current field attribute for monochrome color decisions
|
|
||||||
byte currentFA = 0;
|
byte currentFA = 0;
|
||||||
ExtendedAttribute currentFieldEa = null;
|
ExtendedAttribute currentFieldEa = null;
|
||||||
|
|
||||||
@@ -1043,7 +1098,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
int x = ox + col * cellWidth;
|
int x = ox + col * cellWidth;
|
||||||
int y = oy + row * cellHeight;
|
int y = oy + row * cellHeight;
|
||||||
|
|
||||||
// Determine colors and attributes
|
|
||||||
Color fgColor;
|
Color fgColor;
|
||||||
Color bgColor;
|
Color bgColor;
|
||||||
boolean bold = false;
|
boolean bold = false;
|
||||||
@@ -1053,7 +1107,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (ea.isFieldAttribute()) {
|
if (ea.isFieldAttribute()) {
|
||||||
currentFA = ea.fa;
|
currentFA = ea.fa;
|
||||||
currentFieldEa = ea;
|
currentFieldEa = ea;
|
||||||
// Selection highlight on field attribute cells
|
|
||||||
if (isCellSelected(row, col)) {
|
if (isCellSelected(row, col)) {
|
||||||
g2.setColor(SELECTION_COLOR);
|
g2.setColor(SELECTION_COLOR);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
@@ -1073,20 +1126,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Graphics rendition
|
// Graphics rendition
|
||||||
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
|
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
|
||||||
if (gr != 0) {
|
if (gr != 0) {
|
||||||
if ((gr & GR_INTENSIFY) != 0)
|
if ((gr & GR_INTENSIFY) != 0) bold = true;
|
||||||
bold = true;
|
if ((gr & GR_UNDERLINE) != 0) underline = true;
|
||||||
if ((gr & GR_UNDERLINE) != 0)
|
if ((gr & GR_REVERSE) != 0) reverse = true;
|
||||||
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)
|
// Password fields
|
||||||
// Modern UX: render '*' for typed characters so user sees length/digit count
|
|
||||||
if (faIsZero(currentFA & 0xFF)) {
|
if (faIsZero(currentFA & 0xFF)) {
|
||||||
char ch = ea.ucs4;
|
char ch = ea.ucs4;
|
||||||
if (ch > 0x20 && ch != 0xFF) {
|
if (ch > 0x20 && ch != 0xFF) {
|
||||||
@@ -1103,14 +1150,12 @@ public class TerminalPanel extends JPanel {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply reverse video
|
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
Color tmp = fgColor;
|
Color tmp = fgColor;
|
||||||
fgColor = bgColor;
|
fgColor = bgColor;
|
||||||
bgColor = tmp;
|
bgColor = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw background only if different from default panel bgColor or if inverted
|
|
||||||
if (!bgColor.equals(this.bgColor) || reverse) {
|
if (!bgColor.equals(this.bgColor) || reverse) {
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
@@ -1137,19 +1182,25 @@ public class TerminalPanel extends JPanel {
|
|||||||
Font f = bold ? boldTerminalFont : terminalFont;
|
Font f = bold ? boldTerminalFont : terminalFont;
|
||||||
g2.setFont(f);
|
g2.setFont(f);
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
|
int textY = y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2);
|
||||||
if (ch < 128) {
|
if (ch < 128) {
|
||||||
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
|
g2.drawString(CHAR_STRINGS[ch], x, textY);
|
||||||
} else {
|
} else {
|
||||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
g2.drawString(String.valueOf(ch), x, textY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw underline
|
|
||||||
if (underline) {
|
if (underline) {
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
g2.drawLine(x, y + cellHeight - fontDescent,
|
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
||||||
x + cellWidth - 1, y + cellHeight - fontDescent);
|
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
|
// Draw selection highlight
|
||||||
@@ -1160,7 +1211,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) {
|
if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
|
||||||
int gocaX = client.getGocaDecoder().getGraphicCursorX();
|
int gocaX = client.getGocaDecoder().getGraphicCursorX();
|
||||||
int gocaY = client.getGocaDecoder().getGraphicCursorY();
|
int gocaY = client.getGocaDecoder().getGraphicCursorY();
|
||||||
@@ -1175,7 +1226,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
g2.setColor(Color.WHITE);
|
g2.setColor(Color.WHITE);
|
||||||
g2.setXORMode(Color.BLACK);
|
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 - 6, py, px + 6, py);
|
||||||
g2.drawLine(px, py - 6, px, py + 6);
|
g2.drawLine(px, py - 6, px, py + 6);
|
||||||
g2.setPaintMode();
|
g2.setPaintMode();
|
||||||
@@ -1194,13 +1244,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||||
g2.setPaintMode();
|
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) {
|
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||||
@@ -1220,8 +1263,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
|
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
|
||||||
if (bg >= 0xf0 && bg <= 0xff) {
|
if (bg >= 0xf0 && bg <= 0xff) {
|
||||||
int idx = bg - 0xf0;
|
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) {
|
if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) {
|
||||||
return bgColor;
|
return bgColor;
|
||||||
}
|
}
|
||||||
@@ -1241,22 +1282,26 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (blinkTimer != null)
|
if (blinkTimer != null)
|
||||||
blinkTimer.stop();
|
blinkTimer.stop();
|
||||||
}
|
}
|
||||||
private Runnable onLightPenToggle;
|
|
||||||
|
|
||||||
public void setOnLightPenToggle(Runnable callback) {
|
|
||||||
this.onLightPenToggle = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void toggleLightPen() {
|
public void toggleLightPen() {
|
||||||
this.lightPenMode = !this.lightPenMode;
|
this.lightPenMode = !this.lightPenMode;
|
||||||
System.out.println("Light Pen mode: " + (this.lightPenMode ? "ON" : "OFF"));
|
|
||||||
if (onLightPenToggle != null) {
|
|
||||||
onLightPenToggle.run();
|
|
||||||
}
|
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLightPenMode() {
|
public boolean isLightPenMode() {
|
||||||
return this.lightPenMode;
|
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 int nopIntervalSeconds = 0;
|
||||||
private String terminalName = null; // override terminal type string
|
private String terminalName = null; // override terminal type string
|
||||||
private boolean tn3270eEnabled = true;
|
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 haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||||
|
private String codePage = "037";
|
||||||
|
private String associatedPrinterLu = null;
|
||||||
|
|
||||||
public ConnectionConfig() {}
|
public ConnectionConfig() {}
|
||||||
|
|
||||||
@@ -87,9 +96,44 @@ public class ConnectionConfig {
|
|||||||
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
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 String getTerminalName() { return terminalName; }
|
||||||
public void setTerminalName(String name) { this.terminalName = name; }
|
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"),
|
* 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.
|
* 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) {
|
if (terminalName != null) {
|
||||||
return terminalName;
|
return terminalName;
|
||||||
}
|
}
|
||||||
|
if (dynamicModel) {
|
||||||
|
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
|
||||||
|
}
|
||||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
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 DataStreamProcessor dsProcessor;
|
||||||
private final TelnetFSM fsm;
|
private final TelnetFSM fsm;
|
||||||
private final InputProcessor inputProcessor;
|
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;
|
private TelnetConnection connection;
|
||||||
|
|
||||||
public Telnet3270Client(ConnectionConfig config) {
|
public Telnet3270Client(ConnectionConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.translator = new EbcdicTranslator();
|
this.translator = new EbcdicTranslator(config.getCodePage());
|
||||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||||
|
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
|
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||||
dsProcessor.setInputProcessor(inputProcessor);
|
dsProcessor.setInputProcessor(inputProcessor);
|
||||||
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
||||||
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
|
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. */
|
/** Get the EBCDIC translator. */
|
||||||
public EbcdicTranslator getTranslator() { return 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. */
|
/** Get the current connection state. */
|
||||||
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
|
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
|
||||||
|
|
||||||
@@ -125,6 +144,21 @@ public class Telnet3270Client {
|
|||||||
/** Get the connection config. */
|
/** Get the connection config. */
|
||||||
public ConnectionConfig getConfig() { return 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. */
|
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||||
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||||
config.setCertificateVerifier(verifier);
|
config.setCertificateVerifier(verifier);
|
||||||
@@ -132,7 +166,27 @@ public class Telnet3270Client {
|
|||||||
|
|
||||||
/** Get the active SSLSession if connected over TLS, or null. */
|
/** Get the active SSLSession if connected over TLS, or null. */
|
||||||
public javax.net.ssl.SSLSession getSslSession() {
|
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 ==========
|
// ========== Convenience input methods ==========
|
||||||
@@ -223,6 +277,10 @@ public class Telnet3270Client {
|
|||||||
public void sysReq() { inputProcessor.sysReq(); }
|
public void sysReq() { inputProcessor.sysReq(); }
|
||||||
/** Reset (unlock keyboard). */
|
/** Reset (unlock keyboard). */
|
||||||
public void reset() { inputProcessor.reset(); }
|
public void reset() { inputProcessor.reset(); }
|
||||||
|
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
|
||||||
|
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
||||||
|
/** Trigger Light Pen selection at the specified screen address. */
|
||||||
|
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
|
||||||
|
|
||||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||||
return dsProcessor.getProgramSymbolManager();
|
return dsProcessor.getProgramSymbolManager();
|
||||||
|
|||||||
@@ -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,204 @@
|
|||||||
package haus.nightmare.lib3270j.charset;
|
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).
|
* Default: Code Page 037 (US/Canada EBCDIC).
|
||||||
*/
|
*/
|
||||||
public class EbcdicTranslator {
|
public class EbcdicTranslator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EBCDIC Code Page 037 to Unicode mapping.
|
* EBCDIC Code Page 037 to Unicode mapping table preserved for legacy static access.
|
||||||
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint.
|
|
||||||
*/
|
*/
|
||||||
private static final int[] CP037_TO_UNICODE = {
|
public static final int[] CP037_TO_UNICODE = Cp037.MAPPING;
|
||||||
// 00-0F
|
|
||||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
private CodePage activeCodePage;
|
||||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
|
||||||
// 10-1F
|
public EbcdicTranslator() {
|
||||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
this("037");
|
||||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
}
|
||||||
// 20-2F
|
|
||||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
public EbcdicTranslator(String codePageId) {
|
||||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
setCodePage(codePageId);
|
||||||
// 30-3F
|
}
|
||||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
|
||||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
public EbcdicTranslator(CodePage codePage) {
|
||||||
// 40-4F (space, accent chars, punctuation)
|
setCodePage(codePage);
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1).
|
* Set active code page by identifier or alias (e.g. "037", "1047", "500", "273", "1140", "930").
|
||||||
* Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable).
|
|
||||||
*/
|
*/
|
||||||
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 synchronized CodePage getCodePage() {
|
||||||
|
return activeCodePage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isDBCS() {
|
||||||
|
return isDBCSCodePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
public char ebcdicToUnicode(int ebc) {
|
||||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
return activeCodePage.ebcdicToUnicode(ebc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static helper to translate EBCDIC byte to Unicode character.
|
* Static helper to translate EBCDIC byte to Unicode character (using standard CP037).
|
||||||
*/
|
*/
|
||||||
public static char toUnicode(int ebc) {
|
public static char toUnicode(int ebc) {
|
||||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
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) {
|
public static char ebcdicToAscii(int ebc) {
|
||||||
return toUnicode(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.
|
* Returns -1 if the character cannot be mapped.
|
||||||
*/
|
*/
|
||||||
public int unicodeToEbcdic(char unicode) {
|
public int unicodeToEbcdic(char unicode) {
|
||||||
if (unicode < 256) {
|
return activeCodePage.unicodeToEbcdic(unicode);
|
||||||
return UNICODE_TO_CP037[unicode];
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
|
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
|
||||||
*/
|
*/
|
||||||
public byte unicodeToEbcdicSafe(char unicode) {
|
public byte unicodeToEbcdicSafe(char unicode) {
|
||||||
int ebc = unicodeToEbcdic(unicode);
|
return activeCodePage.unicodeToEbcdicSafe(unicode);
|
||||||
return (byte) (ebc >= 0 ? ebc : 0x40);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
|
||||||
StringBuilder sb = new StringBuilder(length);
|
return activeCodePage.ebcdicToString(ebcdic, offset, length);
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
public byte[] stringToEbcdic(String s) {
|
||||||
byte[] result = new byte[s.length()];
|
return activeCodePage.stringToEbcdic(s);
|
||||||
for (int i = 0; i < s.length(); i++) {
|
}
|
||||||
result[i] = unicodeToEbcdicSafe(s.charAt(i));
|
|
||||||
|
/**
|
||||||
|
* 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+303
-33
@@ -4,6 +4,7 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
|||||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@@ -41,7 +42,7 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
// Graphics & Programmed Symbols
|
// Graphics & Programmed Symbols
|
||||||
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
||||||
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384);
|
||||||
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
||||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||||
private int currentGocaSubtype = 0;
|
private int currentGocaSubtype = 0;
|
||||||
@@ -86,6 +87,16 @@ public class DataStreamProcessor {
|
|||||||
this.ftDft = ftDft;
|
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) {
|
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
|
||||||
this.inputProcessor = inputProcessor;
|
this.inputProcessor = inputProcessor;
|
||||||
}
|
}
|
||||||
@@ -638,10 +649,21 @@ public class DataStreamProcessor {
|
|||||||
case XA_CHARSET:
|
case XA_CHARSET:
|
||||||
ea.cs = (byte) value;
|
ea.cs = (byte) value;
|
||||||
break;
|
break;
|
||||||
case XA_VALIDATION:
|
|
||||||
case XA_OUTLINING:
|
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:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -677,7 +699,71 @@ public class DataStreamProcessor {
|
|||||||
outputWrite(caddr[0] & 0xFF);
|
outputWrite(caddr[0] & 0xFF);
|
||||||
outputWrite(caddr[1] & 0xFF);
|
outputWrite(caddr[1] & 0xFF);
|
||||||
|
|
||||||
// Buffer contents
|
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++) {
|
for (int i = 0; i < size; i++) {
|
||||||
ExtendedAttribute ea = screen.getCell(i);
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
if (ea.isFieldAttribute()) {
|
if (ea.isFieldAttribute()) {
|
||||||
@@ -687,6 +773,7 @@ public class DataStreamProcessor {
|
|||||||
outputWrite(ea.ec & 0xFF);
|
outputWrite(ea.ec & 0xFF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sendOutput();
|
sendOutput();
|
||||||
}
|
}
|
||||||
@@ -694,8 +781,8 @@ public class DataStreamProcessor {
|
|||||||
// ========== Read Modified ==========
|
// ========== Read Modified ==========
|
||||||
|
|
||||||
private void processReadModified(boolean all) {
|
private void processReadModified(boolean all) {
|
||||||
if (ftDft != null) {
|
if (ftDft != null && ftDft.readModified()) {
|
||||||
ftDft.readModified();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
outputPos = 0;
|
outputPos = 0;
|
||||||
@@ -726,7 +813,7 @@ public class DataStreamProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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++) {
|
for (int i = 0; i < size; i++) {
|
||||||
ExtendedAttribute ea = screen.getCell(i);
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
||||||
@@ -779,33 +866,23 @@ public class DataStreamProcessor {
|
|||||||
case SF_READ_PART:
|
case SF_READ_PART:
|
||||||
processSFReadPartition(data, pos, fieldLen);
|
processSFReadPartition(data, pos, fieldLen);
|
||||||
break;
|
break;
|
||||||
case SF_ERASE_RESET: {
|
case SF_ERASE_RESET:
|
||||||
boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT);
|
processEraseReset(data, pos, fieldLen);
|
||||||
screen.erase(alt);
|
|
||||||
graphicsPlane.clear();
|
|
||||||
gocaDecoder.resetDefaults();
|
|
||||||
notifyScreenSizeChanged();
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case SF_SET_REPLY_MODE:
|
case SF_SET_REPLY_MODE:
|
||||||
if (fieldLen >= 5) {
|
processSetReplyMode(data, pos, fieldLen);
|
||||||
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case SF_CREATE_PART:
|
case SF_CREATE_PART:
|
||||||
if (fieldLen >= 4) {
|
processCreatePartition(data, pos, fieldLen);
|
||||||
int pid = data[pos + 3] & 0xFF;
|
break;
|
||||||
screen.setActivePartition(pid);
|
case SF_DESTROY_PART:
|
||||||
log.fine("Created active partition ID=" + pid);
|
processDestroyPartition(data, pos, fieldLen);
|
||||||
}
|
break;
|
||||||
graphicsPlane.clear();
|
case SF_ACTIVATE_PART:
|
||||||
gocaDecoder.resetDefaults();
|
processActivatePartition(data, pos, fieldLen);
|
||||||
break;
|
break;
|
||||||
case SF_OUTBOUND_DS:
|
case SF_OUTBOUND_DS:
|
||||||
if (fieldLen > 5) {
|
processOutbound3270DS(data, pos, fieldLen);
|
||||||
// Outbound DS contains another 3270 command
|
|
||||||
processRecord(data, pos + 4, fieldLen - 4, false);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case SF_TRANSFER_DATA:
|
case SF_TRANSFER_DATA:
|
||||||
if (ftDft != null) {
|
if (ftDft != null) {
|
||||||
@@ -832,9 +909,15 @@ public class DataStreamProcessor {
|
|||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
||||||
|
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
|
||||||
|
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
|
||||||
|
gocaDecoder.setGraphicsCursorActive(true);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
break;
|
||||||
|
}
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: // 0x10: Object Picture (Picture segments)
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw orders)
|
|
||||||
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
||||||
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
||||||
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
||||||
@@ -847,6 +930,11 @@ public class DataStreamProcessor {
|
|||||||
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
|
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
|
||||||
|
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
int targetW = screen.getCols() * 9;
|
||||||
|
int targetH = screen.getRows() * 16;
|
||||||
|
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||||
|
graphicsPlane.resize(targetW, targetH);
|
||||||
|
}
|
||||||
|
|
||||||
if (flags == 0x80) { // SPAN_FIRST
|
if (flags == 0x80) { // SPAN_FIRST
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
@@ -932,7 +1020,7 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SF_RP_QUERY:
|
case SF_RP_QUERY:
|
||||||
log.info("ReadPartition Query — sending all query replies");
|
log.info("ReadPartition Query — sending base query replies");
|
||||||
graphicsPlane.clear();
|
graphicsPlane.clear();
|
||||||
gocaDecoder.resetDefaults();
|
gocaDecoder.resetDefaults();
|
||||||
sendAllQueryReplies();
|
sendAllQueryReplies();
|
||||||
@@ -941,7 +1029,9 @@ public class DataStreamProcessor {
|
|||||||
if (fieldLen >= 6) {
|
if (fieldLen >= 6) {
|
||||||
int listType = data[offset + 5] & 0xFF;
|
int listType = data[offset + 5] & 0xFF;
|
||||||
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
|
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
|
||||||
if (listType == SF_RPQ_ALL || listType == SF_RPQ_EQUIV) {
|
if (listType == SF_RPQ_ALL) {
|
||||||
|
sendCompleteQueryReplies();
|
||||||
|
} else if (listType == SF_RPQ_EQUIV) {
|
||||||
sendAllQueryReplies();
|
sendAllQueryReplies();
|
||||||
} else if (listType == SF_RPQ_LIST) {
|
} else if (listType == SF_RPQ_LIST) {
|
||||||
// Send only requested query replies
|
// Send only requested query replies
|
||||||
@@ -983,7 +1073,22 @@ public class DataStreamProcessor {
|
|||||||
if ((i + 1) % 32 == 0)
|
if ((i + 1) % 32 == 0)
|
||||||
sb.append("\n ");
|
sb.append("\n ");
|
||||||
}
|
}
|
||||||
log.warning(">>> SENDING Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
log.warning(">>> SENDING Base Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
||||||
|
if (outputSender != null) {
|
||||||
|
outputSender.send3270Data(qr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendCompleteQueryReplies() {
|
||||||
|
byte[] qr = qrBuilder.buildCompleteQueryReplies(screen.getMaxCols(), screen.getMaxRows(),
|
||||||
|
screen.getMaxCols() * screen.getMaxRows());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < qr.length; i++) {
|
||||||
|
sb.append(String.format("%02x ", qr[i] & 0xFF));
|
||||||
|
if ((i + 1) % 32 == 0)
|
||||||
|
sb.append("\n ");
|
||||||
|
}
|
||||||
|
log.warning(">>> SENDING Complete Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
||||||
if (outputSender != null) {
|
if (outputSender != null) {
|
||||||
outputSender.send3270Data(qr);
|
outputSender.send3270Data(qr);
|
||||||
}
|
}
|
||||||
@@ -1035,4 +1140,169 @@ public class DataStreamProcessor {
|
|||||||
l.onScreenUpdated();
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+246
-125
@@ -15,16 +15,17 @@ public class QueryReplyBuilder {
|
|||||||
|
|
||||||
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
|
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
|
||||||
|
|
||||||
// Canned values from 3279-2 (matching sf.c)
|
|
||||||
private static final int SW_3279_2 = 0x09;
|
private static final int SW_3279_2 = 0x09;
|
||||||
private static final int SH_3279_2 = 0x0c;
|
private static final int SH_3279_2 = 0x0c;
|
||||||
private static final int Xr_3279_2 = 0x000a02e5;
|
|
||||||
private static final int Yr_3279_2 = 0x0002006f;
|
// Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060)
|
||||||
|
private static final int Xr_HOD = 0x00010060;
|
||||||
|
private static final int Yr_HOD = 0x00010060;
|
||||||
|
|
||||||
private final ScreenBuffer screen;
|
private final ScreenBuffer screen;
|
||||||
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
||||||
|
|
||||||
// Base query reply codes (text mode)
|
// Base query reply codes (text mode, matches HOD DS3270.java queryEquiv)
|
||||||
private static final int[] SUPPORTED_QR_BASE = {
|
private static final int[] SUPPORTED_QR_BASE = {
|
||||||
QR_SUMMARY, // 0x80 — summary must list itself
|
QR_SUMMARY, // 0x80 — summary must list itself
|
||||||
QR_USABLE_AREA, // 0x81
|
QR_USABLE_AREA, // 0x81
|
||||||
@@ -34,10 +35,11 @@ public class QueryReplyBuilder {
|
|||||||
QR_HIGHLIGHTING, // 0x87
|
QR_HIGHLIGHTING, // 0x87
|
||||||
QR_REPLY_MODES, // 0x88
|
QR_REPLY_MODES, // 0x88
|
||||||
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
|
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
|
||||||
QR_IMP_PART, // 0xa6
|
QR_AUXDA, // 0x99 - Auxiliary Devices
|
||||||
|
QR_IMP_PART, // 0xa6 - Implicit Partition Sizes
|
||||||
};
|
};
|
||||||
|
|
||||||
// Vector graphics query reply codes matching HOD DS3270.java line 1723
|
// Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING
|
||||||
private static final int[] SUPPORTED_QR_VECTOR = {
|
private static final int[] SUPPORTED_QR_VECTOR = {
|
||||||
QR_SUMMARY, // 0x80
|
QR_SUMMARY, // 0x80
|
||||||
QR_USABLE_AREA, // 0x81
|
QR_USABLE_AREA, // 0x81
|
||||||
@@ -46,17 +48,17 @@ public class QueryReplyBuilder {
|
|||||||
QR_COLOR, // 0x86
|
QR_COLOR, // 0x86
|
||||||
QR_HIGHLIGHTING, // 0x87
|
QR_HIGHLIGHTING, // 0x87
|
||||||
QR_REPLY_MODES, // 0x88
|
QR_REPLY_MODES, // 0x88
|
||||||
QR_SAVE_RESTORE, // 0x8c
|
QR_OUTLINING, // 0x8c
|
||||||
QR_DDM, // 0x95
|
QR_DDM, // 0x95
|
||||||
QR_TRANSPARENCY, // 0x99
|
QR_AUXDA, // 0x99
|
||||||
QR_IMP_PART, // 0xa6
|
QR_IMP_PART, // 0xa6
|
||||||
QR_RPQ_NAMES, // 0xa8
|
QR_TRANSPARENCY, // 0xa8
|
||||||
QR_GRAPHICS, // 0xb0
|
QR_SEGMENT, // 0xb0
|
||||||
QR_GIMAGE, // 0xb1
|
QR_PROCEDURE, // 0xb1
|
||||||
QR_AUX_DEV, // 0xb2
|
QR_LINETYPE, // 0xb2
|
||||||
QR_OEM_FMT, // 0xb3
|
QR_PORT, // 0xb3
|
||||||
QR_GCOLOR, // 0xb4
|
QR_GRCOLOR, // 0xb4
|
||||||
QR_GSYMBOLS, // 0xb6
|
QR_GRSYMBOLSET, // 0xb6
|
||||||
};
|
};
|
||||||
|
|
||||||
public QueryReplyBuilder(ScreenBuffer screen) {
|
public QueryReplyBuilder(ScreenBuffer screen) {
|
||||||
@@ -77,7 +79,8 @@ public class QueryReplyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build all query replies as a single AID_SF + structured field response.
|
* Build base query replies in response to a generic Read Partition Query (0x02).
|
||||||
|
* Returns base text/presentation summary structured fields (matches HOD DS3270.java line 1723).
|
||||||
*/
|
*/
|
||||||
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
|
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||||
@@ -85,62 +88,90 @@ public class QueryReplyBuilder {
|
|||||||
// AID byte for structured field
|
// AID byte for structured field
|
||||||
out.write(AID_SF);
|
out.write(AID_SF);
|
||||||
|
|
||||||
// Summary
|
// Summary (0x80) - lists all supported capabilities
|
||||||
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||||
|
|
||||||
// Usable Area
|
// Usable Area (0x81)
|
||||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||||
|
|
||||||
// Alpha Partitions
|
// Alpha Partitions (0x84)
|
||||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||||
|
|
||||||
// Character Sets
|
// Character Sets (0x85)
|
||||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||||
|
|
||||||
// Color
|
// Color (0x86)
|
||||||
appendQueryReply(out, QR_COLOR, buildColor());
|
appendQueryReply(out, QR_COLOR, buildColor());
|
||||||
|
|
||||||
// Highlighting
|
// Highlighting (0x87)
|
||||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||||
|
|
||||||
// Reply Modes (0x88)
|
// Reply Modes (0x88)
|
||||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||||
|
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||||
// Save/Restore (0x8C)
|
if (isDbcs) {
|
||||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
// Outlining (0x8C)
|
||||||
|
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||||
|
// DBCS Asia (0x91)
|
||||||
|
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distributed Data Management (0x95)
|
// Distributed Data Management (0x95)
|
||||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||||
|
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
// Auxiliary Devices (0x99)
|
||||||
// Transparency (0x99)
|
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implicit Partition (0xA6)
|
// Implicit Partition (0xA6)
|
||||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||||
|
|
||||||
// Vector Graphics QRs if enabled
|
log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")");
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
return out.toByteArray();
|
||||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
|
|
||||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0
|
|
||||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1
|
|
||||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
|
|
||||||
appendOemFmt(out); // 0xB3
|
|
||||||
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
|
|
||||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
|
/**
|
||||||
|
* Build complete query replies including vector graphics (when SF_RPQ_ALL 0x80 is requested).
|
||||||
|
*/
|
||||||
|
public byte[] buildCompleteQueryReplies(int maxCols, int maxRows, int bufferSize) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||||
|
out.write(AID_SF);
|
||||||
|
|
||||||
|
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||||
|
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||||
|
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||||
|
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||||
|
appendQueryReply(out, QR_COLOR, buildColor());
|
||||||
|
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||||
|
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||||
|
|
||||||
|
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||||
|
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||||
|
if (isDbcs) {
|
||||||
|
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||||
|
}
|
||||||
|
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||||
|
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||||
|
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||||
|
|
||||||
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8
|
||||||
|
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0
|
||||||
|
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1
|
||||||
|
appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2
|
||||||
|
appendPort(out); // 0xB3
|
||||||
|
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4
|
||||||
|
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")");
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
|
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
|
||||||
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
|
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
|
||||||
* matching x3270 sf.c behavior.
|
* matching HOD DS3270.java line 1835.
|
||||||
*/
|
*/
|
||||||
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
|
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
|
||||||
if (requestedCodes == null || requestedCodes.length == 0) {
|
if (requestedCodes == null || requestedCodes.length == 0) {
|
||||||
@@ -174,72 +205,70 @@ public class QueryReplyBuilder {
|
|||||||
case QR_REPLY_MODES:
|
case QR_REPLY_MODES:
|
||||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||||
break;
|
break;
|
||||||
case QR_SAVE_RESTORE:
|
case QR_OUTLINING: // 0x8C
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
break;
|
||||||
|
case QR_DBCS_ASIA: // 0x91
|
||||||
|
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||||
|
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_DDM:
|
case QR_DDM: // 0x95
|
||||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||||
break;
|
break;
|
||||||
case QR_TRANSPARENCY:
|
case QR_AUXDA: // 0x99
|
||||||
|
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||||
|
break;
|
||||||
|
case QR_IMP_PART: // 0xA6
|
||||||
|
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||||
|
break;
|
||||||
|
case QR_TRANSPARENCY: // 0xA8
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_IMP_PART:
|
case QR_SEGMENT: // 0xB0
|
||||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
|
||||||
break;
|
|
||||||
case QR_RPQ_NAMES:
|
|
||||||
case QR_RPQNAMES:
|
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, code, buildRpqNames());
|
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows));
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_GRAPHICS:
|
case QR_PROCEDURE: // 0xB1
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows));
|
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows));
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_GIMAGE:
|
case QR_LINETYPE: // 0xB2
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows));
|
appendQueryReply(out, QR_LINETYPE, buildLineType());
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_AUX_DEV:
|
case QR_PORT: // 0xB3
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
appendPort(out);
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_OEM_FMT:
|
case QR_GRCOLOR: // 0xB4
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendOemFmt(out);
|
appendQueryReply(out, QR_GRCOLOR, buildGrColor());
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_GCOLOR:
|
case QR_GRSYMBOLSET: // 0xB6
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet());
|
||||||
} else {
|
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case QR_GSYMBOLS:
|
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
|
||||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
@@ -268,39 +297,68 @@ public class QueryReplyBuilder {
|
|||||||
private byte[] buildSummary() {
|
private byte[] buildSummary() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||||
|
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||||
for (int code : codes) {
|
for (int code : codes) {
|
||||||
out.write(code);
|
out.write(code);
|
||||||
|
if (isDbcs && code == QR_OUTLINING) {
|
||||||
|
out.write(QR_DBCS_ASIA); // 0x91
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
||||||
out.write(0x01); // 12/14-bit addressing
|
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
|
||||||
out.write(0x00); // no special character features
|
out.write(0x00); // no special character features
|
||||||
out.write((maxCols >> 8) & 0xFF); // usable width high
|
out.write((maxCols >> 8) & 0xFF); // usable width high
|
||||||
out.write(maxCols & 0xFF); // usable width low
|
out.write(maxCols & 0xFF); // usable width low
|
||||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||||
out.write(maxRows & 0xFF); // usable height low
|
out.write(maxRows & 0xFF); // usable height low
|
||||||
out.write(0x01); // units (mm)
|
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
|
||||||
// Xr (4 bytes) - canned from 3279-2
|
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
|
||||||
out.write((Xr_3279_2 >> 24) & 0xFF);
|
out.write((Xr_HOD >> 24) & 0xFF);
|
||||||
out.write((Xr_3279_2 >> 16) & 0xFF);
|
out.write((Xr_HOD >> 16) & 0xFF);
|
||||||
out.write((Xr_3279_2 >> 8) & 0xFF);
|
out.write((Xr_HOD >> 8) & 0xFF);
|
||||||
out.write(Xr_3279_2 & 0xFF);
|
out.write(Xr_HOD & 0xFF);
|
||||||
// Yr (4 bytes) - canned from 3279-2
|
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
|
||||||
out.write((Yr_3279_2 >> 24) & 0xFF);
|
out.write((Yr_HOD >> 24) & 0xFF);
|
||||||
out.write((Yr_3279_2 >> 16) & 0xFF);
|
out.write((Yr_HOD >> 16) & 0xFF);
|
||||||
out.write((Yr_3279_2 >> 8) & 0xFF);
|
out.write((Yr_HOD >> 8) & 0xFF);
|
||||||
out.write(Yr_3279_2 & 0xFF);
|
out.write(Yr_HOD & 0xFF);
|
||||||
out.write(SW_3279_2); // AW
|
int charW = getCharWidth();
|
||||||
out.write(SH_3279_2); // AH
|
int charH = getCharHeight();
|
||||||
|
out.write(charW); // AW
|
||||||
|
out.write(charH); // AH
|
||||||
int buf = maxCols * maxRows;
|
int buf = maxCols * maxRows;
|
||||||
out.write((buf >> 8) & 0xFF); // buffer size high
|
out.write((buf >> 8) & 0xFF); // buffer size high
|
||||||
out.write(buf & 0xFF); // buffer size low
|
out.write(buf & 0xFF); // buffer size low
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCharWidth() {
|
||||||
|
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
return SW_3279_2; // 9
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCharHeight() {
|
||||||
|
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
|
||||||
|
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
|
||||||
|
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
|
||||||
|
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
|
||||||
|
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
|
||||||
|
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
|
||||||
|
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
|
||||||
|
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
|
||||||
|
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
|
||||||
|
//
|
||||||
|
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
|
||||||
|
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
|
||||||
|
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] buildAlphaPartitions(int maxRows) {
|
private byte[] buildAlphaPartitions(int maxRows) {
|
||||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||||
@@ -312,20 +370,32 @@ public class QueryReplyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildCharsets() {
|
private byte[] buildCharsets() {
|
||||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
int charW = getCharWidth();
|
||||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
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 == GraphicsMode.PROGRAMMED_SYMBOLS) {
|
||||||
|
// Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
out.write(SW_3279_2); // SDW (9)
|
out.write(charW); // SDW (9)
|
||||||
out.write(SH_3279_2); // SDH (12)
|
out.write(charH); // SDH (16 for 3179G, 12 for 3279-2)
|
||||||
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
||||||
out.write(0x00); // Load PS device type (high)
|
out.write(0x00); // Load PS device type (high)
|
||||||
out.write(0x00); // Load PS device type (low)
|
out.write(0x00); // Load PS device type (low)
|
||||||
out.write(0x00); // reserved
|
out.write(0x00); // reserved
|
||||||
out.write(0x07); // DL = 7 bytes per descriptor
|
out.write(0x07); // DL = 7 bytes per descriptor
|
||||||
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, CP037)
|
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, active codepage)
|
||||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
|
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
|
// 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);
|
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)
|
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane)
|
||||||
@@ -343,15 +413,17 @@ public class QueryReplyBuilder {
|
|||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||||
out.write(0x82); // flags: GE, CGCSGID present
|
out.write(0x82); // flags: GE, CGCSGID present
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
out.write(SW_3279_2); // SDW - default char width (9)
|
out.write(charW); // SDW - default char width (9)
|
||||||
out.write(SH_3279_2); // SDH - default char height (12)
|
out.write(charH); // SDH - default char height (16 for 3179G, 12 for 3279-2)
|
||||||
out.write(0x00); // LoadPS format (0x00)
|
out.write(0x00); // LoadPS format (0x00)
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
out.write(0x07); // DL = 7
|
out.write(0x07); // DL = 7
|
||||||
// Set 0 (Base EBCDIC - Non-loadable, single plane, CP037)
|
// Set 0 (Base EBCDIC - Non-loadable, single plane, active codepage)
|
||||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
|
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)
|
// 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);
|
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
@@ -422,31 +494,58 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildGraphics(int maxCols, int maxRows) {
|
public byte[] buildOutlining() {
|
||||||
int width = maxCols * 9;
|
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000")
|
||||||
int height = maxRows * 12;
|
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildDbcsAsia() {
|
||||||
|
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001")
|
||||||
|
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildAuxDa() {
|
||||||
|
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000")
|
||||||
|
return new byte[]{ 0x00, 0x00 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildTransparency() {
|
||||||
|
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff")
|
||||||
|
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildSegment(int maxCols, int maxRows) {
|
||||||
|
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
(byte) 0x80, 0x02,
|
(byte) 0x80, 0x02,
|
||||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
0x00, 0x00,
|
||||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
0x00, (byte) 0xFC,
|
||||||
0x00
|
0x00
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildGImage(int maxCols, int maxRows) {
|
public byte[] buildGraphics(int maxCols, int maxRows) {
|
||||||
int width = maxCols * 9;
|
return buildSegment(maxCols, maxRows);
|
||||||
int height = maxRows * 12;
|
}
|
||||||
|
|
||||||
|
public byte[] buildProcedure(int maxCols, int maxRows) {
|
||||||
|
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x00, 0x01,
|
0x00, 0x01,
|
||||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
0x00, 0x00,
|
||||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
0x00, (byte) 0xFC,
|
||||||
0x00,
|
0x00,
|
||||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildAuxDev() {
|
public byte[] buildGImage(int maxCols, int maxRows) {
|
||||||
|
return buildProcedure(maxCols, maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildLineType() {
|
||||||
|
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b")
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x00, 0x09, 0x00, 0x07,
|
0x00, 0x09, 0x00, 0x07,
|
||||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||||
@@ -454,38 +553,36 @@ public class QueryReplyBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildSaveRestore() {
|
public byte[] buildAuxDev() {
|
||||||
// HOD DS3270.java line 1768: 6 bytes payload
|
return buildLineType();
|
||||||
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildTransparency() {
|
public byte[] buildAuxDevice() {
|
||||||
// HOD DS3270.java line 1782: 2 bytes payload
|
return buildLineType();
|
||||||
return new byte[]{ 0x00, 0x00 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildRpqNames() {
|
public void appendPort(ByteArrayOutputStream out) {
|
||||||
// HOD DS3270.java line 1798: 5 bytes payload
|
// HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total)
|
||||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
appendQueryReply(out, QR_PORT, new byte[]{
|
||||||
}
|
|
||||||
|
|
||||||
private void appendOemFmt(ByteArrayOutputStream out) {
|
|
||||||
// HOD DS3270.java line 1814: 4 distinct OEM format sub-fields
|
|
||||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
|
||||||
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||||
});
|
});
|
||||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
appendQueryReply(out, QR_PORT, new byte[]{
|
||||||
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
||||||
});
|
});
|
||||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
appendQueryReply(out, QR_PORT, new byte[]{
|
||||||
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||||
});
|
});
|
||||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
appendQueryReply(out, QR_PORT, new byte[]{
|
||||||
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildGColor() {
|
public void appendOemFmt(ByteArrayOutputStream out) {
|
||||||
|
appendPort(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildGrColor() {
|
||||||
|
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||||
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
|
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
|
||||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
|
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
|
||||||
@@ -505,11 +602,35 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildGSymbols() {
|
public byte[] buildGColor() {
|
||||||
|
return buildGrColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildGrSymbolSet() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
|
0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00,
|
||||||
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
(byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildGSymbols() {
|
||||||
|
return buildGrSymbolSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildSaveRestore() {
|
||||||
|
return buildOutlining();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildRpqNames() {
|
||||||
|
return buildTransparency();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 avblock = 0;
|
||||||
private int dftBufferSize = FTConstants.DFT_BUF;
|
private int dftBufferSize = FTConstants.DFT_BUF;
|
||||||
private String otherOptions = null;
|
private String otherOptions = null;
|
||||||
|
private String codePage = null;
|
||||||
|
|
||||||
// ========== Derived convenience getters ==========
|
// ========== Derived convenience getters ==========
|
||||||
|
|
||||||
@@ -143,6 +144,9 @@ public class FTConfig {
|
|||||||
if (overwrite) this.existAction = ExistAction.REPLACE;
|
if (overwrite) this.existAction = ExistAction.REPLACE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCodePage() { return codePage; }
|
||||||
|
public void setCodePage(String codePage) { this.codePage = codePage; }
|
||||||
|
|
||||||
public void setReceive(boolean receive) {
|
public void setReceive(boolean receive) {
|
||||||
setDirection(receive ? Direction.RECEIVE : Direction.SEND);
|
setDirection(receive ? Direction.RECEIVE : Direction.SEND);
|
||||||
}
|
}
|
||||||
@@ -204,14 +208,115 @@ public class FTConfig {
|
|||||||
if (localFilename == null || localFilename.trim().isEmpty()) {
|
if (localFilename == null || localFilename.trim().isEmpty()) {
|
||||||
return "Local file name is required";
|
return "Local file name is required";
|
||||||
}
|
}
|
||||||
if (hostType == HostType.TSO && isSend() &&
|
|
||||||
units != AllocationUnit.DEFAULT && primarySpace <= 0) {
|
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";
|
return "Primary space is required when allocation is specified";
|
||||||
}
|
}
|
||||||
if (hostType == HostType.TSO && isSend() &&
|
if (isSend() && units == AllocationUnit.AVBLOCK && avblock <= 0) {
|
||||||
units == AllocationUnit.AVBLOCK && avblock <= 0) {
|
|
||||||
return "Avblock value is required when allocation is AVBLOCK";
|
return "Avblock value is required when allocation is AVBLOCK";
|
||||||
}
|
}
|
||||||
|
} else if (hostType == HostType.CMS) {
|
||||||
|
String cmsError = validateCmsFilename(hostTrimmed);
|
||||||
|
if (cmsError != null) return cmsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +357,12 @@ public class FTConfig {
|
|||||||
opts.append("APPEND");
|
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
|
// Host-specific send options
|
||||||
if (isSend()) {
|
if (isSend()) {
|
||||||
if (hostType == HostType.TSO) {
|
if (hostType == HostType.TSO) {
|
||||||
@@ -298,6 +409,9 @@ public class FTConfig {
|
|||||||
if (lrecl > 0) {
|
if (lrecl > 0) {
|
||||||
opts.append(" LRECL ").append(lrecl);
|
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
|
// Special EOF data markers
|
||||||
public static final int EOF_DATA1 = 0x5C;
|
public static final int EOF_DATA1 = 0x5C;
|
||||||
public static final int EOF_DATA2 = 0xA9;
|
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
|
// Upload data area offsets
|
||||||
public static final int O_UP_DATA_CODE = 2;
|
public static final int O_UP_DATA_CODE = 2;
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ public class FTCut {
|
|||||||
private long expandedLength = 0;
|
private long expandedLength = 0;
|
||||||
private int quadrant = -1;
|
private int quadrant = -1;
|
||||||
private boolean cutEof = false;
|
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
|
// Upload translation buffer
|
||||||
private static final int XLATE_NBUF = 32;
|
private static final int XLATE_NBUF = 32;
|
||||||
@@ -111,6 +118,22 @@ public class FTCut {
|
|||||||
this.listener = listener;
|
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.
|
* Initialize CUT mode with active streams.
|
||||||
*/
|
*/
|
||||||
@@ -145,6 +168,9 @@ public class FTCut {
|
|||||||
xlateBufIx = 0;
|
xlateBufIx = 0;
|
||||||
cutEof = false;
|
cutEof = false;
|
||||||
lastCr = false;
|
lastCr = false;
|
||||||
|
retransmitRetries = 0;
|
||||||
|
lastUploadCount = 0;
|
||||||
|
lastUploadSeq = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,6 +184,9 @@ public class FTCut {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warning("Error closing file: " + e.getMessage());
|
log.warning("Error closing file: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
if (input != null) {
|
||||||
|
input.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,6 +196,9 @@ public class FTCut {
|
|||||||
public void processScreenUpdate() {
|
public void processScreenUpdate() {
|
||||||
if (listener.getCurrentState() == FTState.NONE) return;
|
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)
|
// CUT frames MUST have a skip field attribute at O_SF (1919)
|
||||||
byte sfAttr = screen.getCellFAByte(O_SF);
|
byte sfAttr = screen.getCellFAByte(O_SF);
|
||||||
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
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) {
|
private boolean isSkip(byte attr) {
|
||||||
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
|
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
|
||||||
}
|
}
|
||||||
@@ -308,8 +356,13 @@ public class FTCut {
|
|||||||
|
|
||||||
int cs = 0;
|
int cs = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
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_CSUM, FTConstants.to6(cs & 0x3F, translator));
|
||||||
screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator));
|
screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator));
|
||||||
screen.setCell(O_UP_LEN + 1, FTConstants.to6(count & 0x3F, translator));
|
screen.setCell(O_UP_LEN + 1, FTConstants.to6(count & 0x3F, translator));
|
||||||
@@ -364,6 +417,7 @@ public class FTCut {
|
|||||||
expandedLength += converted.length;
|
expandedLength += converted.length;
|
||||||
listener.onBytesTransferred(expandedLength);
|
listener.onBytesTransferred(expandedLength);
|
||||||
}
|
}
|
||||||
|
retransmitRetries = 0; // Reset retries on successful frame
|
||||||
cutAck();
|
cutAck();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warning("CUT: Write error: " + e.getMessage());
|
log.warning("CUT: Write error: " + e.getMessage());
|
||||||
@@ -374,8 +428,38 @@ public class FTCut {
|
|||||||
// ========== Retransmit ==========
|
// ========== Retransmit ==========
|
||||||
|
|
||||||
private void cutRetransmit() {
|
private void cutRetransmit() {
|
||||||
log.warning("CUT: RETRANSMIT (not supported, aborting)");
|
retransmitRetries++;
|
||||||
cutAbort("Retransmit not supported", SC_ABORT_XMIT);
|
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 ==========
|
// ========== Acknowledge ==========
|
||||||
@@ -516,6 +600,10 @@ public class FTCut {
|
|||||||
|
|
||||||
if (!config.isAscii() || !config.isRemapFlag()) {
|
if (!config.isAscii() || !config.isRemapFlag()) {
|
||||||
ebc = localByte & 0xFF;
|
ebc = localByte & 0xFF;
|
||||||
|
} else if (localByte < 0x20 || (localByte >= 0x80 && localByte < 0x9F)) {
|
||||||
|
ebc = localByte & 0xFF;
|
||||||
|
} else if (localByte == 0x9F) {
|
||||||
|
ebc = 0xFF;
|
||||||
} else {
|
} else {
|
||||||
int standardEbc = translator.unicodeToEbcdic((char) localByte);
|
int standardEbc = translator.unicodeToEbcdic((char) localByte);
|
||||||
if (standardEbc < 0) {
|
if (standardEbc < 0) {
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ public class FTDft {
|
|||||||
private boolean dftEof = false;
|
private boolean dftEof = false;
|
||||||
private boolean messageFlag = false;
|
private boolean messageFlag = false;
|
||||||
private long bytesTransferred = 0;
|
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
|
// Savebuf for Read Modified retransmit
|
||||||
private byte[] dftSaveBuf = null;
|
private byte[] dftSaveBuf = null;
|
||||||
@@ -59,6 +65,46 @@ public class FTDft {
|
|||||||
this.listener = listener;
|
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.
|
* Initialize DFT mode with active streams.
|
||||||
*/
|
*/
|
||||||
@@ -89,9 +135,14 @@ public class FTDft {
|
|||||||
dftEof = false;
|
dftEof = false;
|
||||||
messageFlag = false;
|
messageFlag = false;
|
||||||
bytesTransferred = 0;
|
bytesTransferred = 0;
|
||||||
|
estimatedTotalBytes = 0;
|
||||||
|
hostLrecl = 0;
|
||||||
|
hostRecfm = "";
|
||||||
|
hostBlksize = 0;
|
||||||
dftSaveBuf = null;
|
dftSaveBuf = null;
|
||||||
dftSaveBufLen = 0;
|
dftSaveBufLen = 0;
|
||||||
lastCr = false;
|
lastCr = false;
|
||||||
|
pendingByte = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,6 +157,10 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
dftSaveBuf = null;
|
dftSaveBuf = null;
|
||||||
dftSaveBufLen = 0;
|
dftSaveBufLen = 0;
|
||||||
|
resetState();
|
||||||
|
if (input != null) {
|
||||||
|
input.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,7 +217,7 @@ public class FTDft {
|
|||||||
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
||||||
log.fine("DFT: Open request");
|
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
|
// sfLength is the 2-byte length value at sfOffset
|
||||||
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
|
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
|
||||||
String nameBuf = "";
|
String nameBuf = "";
|
||||||
@@ -173,9 +228,12 @@ public class FTDft {
|
|||||||
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OPEN_MSG.equalsIgnoreCase(nameBuf)) {
|
// Check for host DDM file attributes embedded in open request
|
||||||
|
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
||||||
|
|
||||||
|
if (isMessageStream(nameBuf)) {
|
||||||
messageFlag = true;
|
messageFlag = true;
|
||||||
log.info("DFT: Open request for message stream");
|
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
||||||
} else {
|
} else {
|
||||||
messageFlag = false;
|
messageFlag = false;
|
||||||
listener.onDftRunning();
|
listener.onDftRunning();
|
||||||
@@ -188,15 +246,63 @@ public class FTDft {
|
|||||||
dftOpenAck();
|
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();
|
||||||
|
return u.contains(OPEN_MSG) || u.contains("MSG");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDataStream(String name) {
|
||||||
|
if (name == null) return false;
|
||||||
|
String u = name.toUpperCase();
|
||||||
|
return u.contains("FT:DATA") || u.contains("DATA");
|
||||||
|
}
|
||||||
|
|
||||||
private String extractName(byte[] data, int start, int maxLen) {
|
private String extractName(byte[] data, int start, int maxLen) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder asciiSb = new StringBuilder();
|
||||||
|
StringBuilder ebcdicSb = new StringBuilder();
|
||||||
for (int i = 0; i < maxLen && (start + i) < data.length; i++) {
|
for (int i = 0; i < maxLen && (start + i) < data.length; i++) {
|
||||||
int b = data[start + i] & 0xFF;
|
int b = data[start + i] & 0xFF;
|
||||||
if (b == 0) break;
|
if (b == 0) break;
|
||||||
char ch = translator.ebcdicToUnicode(b);
|
asciiSb.append((char) b);
|
||||||
sb.append(ch);
|
ebcdicSb.append(translator.ebcdicToUnicode(b));
|
||||||
}
|
}
|
||||||
return sb.toString().trim();
|
String ascii = asciiSb.toString().trim();
|
||||||
|
String ebcdic = ebcdicSb.toString().trim();
|
||||||
|
if (isMessageStream(ascii) || isDataStream(ascii)) {
|
||||||
|
return ascii;
|
||||||
|
}
|
||||||
|
if (isMessageStream(ebcdic) || isDataStream(ebcdic)) {
|
||||||
|
return ebcdic;
|
||||||
|
}
|
||||||
|
return ascii;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dftOpenAck() {
|
private void dftOpenAck() {
|
||||||
@@ -227,6 +333,7 @@ public class FTDft {
|
|||||||
int pos = offset + 2;
|
int pos = offset + 2;
|
||||||
int end = offset + length;
|
int end = offset + length;
|
||||||
|
|
||||||
|
boolean dataFound = false;
|
||||||
// Look for TR_BEGIN_DATA marker
|
// Look for TR_BEGIN_DATA marker
|
||||||
while (pos < end) {
|
while (pos < end) {
|
||||||
int headerCode = data[pos] & 0xFF;
|
int headerCode = data[pos] & 0xFF;
|
||||||
@@ -234,10 +341,11 @@ public class FTDft {
|
|||||||
if (headerCode == TR_BEGIN_DATA) {
|
if (headerCode == TR_BEGIN_DATA) {
|
||||||
if (pos + 3 > end) break;
|
if (pos + 3 > end) break;
|
||||||
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
||||||
int actualDataLen = dataLen - 3;
|
int actualDataLen = (dataLen > 5) ? Math.min(dataLen - 5, end - (pos + 3)) : (end - (pos + 3));
|
||||||
pos += 3;
|
pos += 3;
|
||||||
|
|
||||||
if (actualDataLen > 0 && pos + actualDataLen <= end) {
|
if (actualDataLen > 0 && pos + actualDataLen <= end) {
|
||||||
|
dataFound = true;
|
||||||
if (messageFlag) {
|
if (messageFlag) {
|
||||||
// Handle message payload from host
|
// Handle message payload from host
|
||||||
dftDataAck();
|
dftDataAck();
|
||||||
@@ -269,25 +377,46 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send acknowledgement for file data
|
// Send acknowledgement only if file data was actually received and processed
|
||||||
|
if (dataFound) {
|
||||||
dftDataAck();
|
dftDataAck();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleHostMessage(byte[] data, int offset, int length) {
|
private void handleHostMessage(byte[] data, int offset, int length) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder asciiSb = new StringBuilder();
|
||||||
|
StringBuilder ebcdicSb = new StringBuilder();
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
int b = data[offset + i] & 0xFF;
|
int b = data[offset + i] & 0xFF;
|
||||||
if (b == 0 || b == '$') break;
|
if (b == 0 || b == '$') break;
|
||||||
char ch = translator.ebcdicToUnicode(b);
|
asciiSb.append((char) b);
|
||||||
sb.append(ch);
|
ebcdicSb.append(translator.ebcdicToUnicode(b));
|
||||||
}
|
}
|
||||||
String msg = sb.toString().trim();
|
String asciiMsg = asciiSb.toString().trim();
|
||||||
log.info("DFT message: " + msg);
|
String ebcdicMsg = ebcdicSb.toString().trim();
|
||||||
|
|
||||||
String msgLower = msg.toLowerCase();
|
// Determine if message is ASCII or EBCDIC
|
||||||
if (msg.startsWith(END_TRANSFER) || msgLower.contains("complete") || msgLower.contains("transferred") || msgLower.contains("success")) {
|
String msg = asciiMsg;
|
||||||
|
if (asciiMsg.toUpperCase().startsWith("TRANS") || asciiMsg.toLowerCase().contains("file") || asciiMsg.toLowerCase().contains("error")) {
|
||||||
|
msg = asciiMsg;
|
||||||
|
} else if (ebcdicMsg.toUpperCase().startsWith("TRANS") || ebcdicMsg.toLowerCase().contains("file") || ebcdicMsg.toLowerCase().contains("error")) {
|
||||||
|
msg = ebcdicMsg;
|
||||||
|
} else {
|
||||||
|
int asciiPrintable = 0;
|
||||||
|
for (char c : asciiMsg.toCharArray()) {
|
||||||
|
if (c >= 32 && c <= 126) asciiPrintable++;
|
||||||
|
}
|
||||||
|
if (asciiPrintable < asciiMsg.length() / 2 && !ebcdicMsg.isEmpty()) {
|
||||||
|
msg = ebcdicMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("DFT host message: " + msg);
|
||||||
|
|
||||||
|
String msgUpper = msg.toUpperCase();
|
||||||
|
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS")) {
|
||||||
listener.onTransferComplete(null);
|
listener.onTransferComplete(null);
|
||||||
} else if (listener.getCurrentState() == FTState.ABORT_SENT || msgLower.contains("error") || msgLower.contains("failed") || msgLower.contains("abort")) {
|
} else if (msgUpper.startsWith("TRANS") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || listener.getCurrentState() == FTState.ABORT_SENT) {
|
||||||
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
||||||
} else {
|
} else {
|
||||||
// Informational message (default success)
|
// Informational message (default success)
|
||||||
@@ -311,8 +440,8 @@ public class FTDft {
|
|||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
int b = data[offset + i] & 0xFF;
|
int b = data[offset + i] & 0xFF;
|
||||||
|
|
||||||
if (config.isCrFlag() && (b == '\r' || b == 0x1A)) {
|
if (config.isCrFlag() && (b == '\r' || b == EOF_CTRL_Z || b == EOF_CTRL_D)) {
|
||||||
continue; // Strip CR and EOF ^Z
|
continue; // Strip CR and EOF (^Z / ^D)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.isRemapFlag()) {
|
if (!config.isRemapFlag()) {
|
||||||
@@ -358,7 +487,7 @@ public class FTDft {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while (!dftEof && totalRead < numbytes) {
|
while (!dftEof && totalRead < numbytes) {
|
||||||
if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) {
|
if (config.isAscii()) {
|
||||||
int b = dftAsciiRead(config);
|
int b = dftAsciiRead(config);
|
||||||
if (b == -1) {
|
if (b == -1) {
|
||||||
dftEof = true;
|
dftEof = true;
|
||||||
@@ -445,6 +574,12 @@ public class FTDft {
|
|||||||
* Matching x3270 dft_ascii_read logic.
|
* Matching x3270 dft_ascii_read logic.
|
||||||
*/
|
*/
|
||||||
private int dftAsciiRead(FTConfig config) throws IOException {
|
private int dftAsciiRead(FTConfig config) throws IOException {
|
||||||
|
if (pendingByte != -1) {
|
||||||
|
int b = pendingByte;
|
||||||
|
pendingByte = -1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
if (inputStream == null) return -1;
|
if (inputStream == null) return -1;
|
||||||
|
|
||||||
int c = inputStream.read();
|
int c = inputStream.read();
|
||||||
@@ -452,15 +587,18 @@ public class FTDft {
|
|||||||
|
|
||||||
if (config.isCrFlag() && !lastCr && c == '\n') {
|
if (config.isCrFlag() && !lastCr && c == '\n') {
|
||||||
lastCr = false;
|
lastCr = false;
|
||||||
// Expand \n to \r\n: return \r byte now
|
// Expand \n to \r\n: buffer \n in pendingByte, return \r now
|
||||||
int rEbc = translator.unicodeToEbcdic('\r');
|
pendingByte = convertAsciiByte('\n', config);
|
||||||
if (rEbc < 0) rEbc = 0x0D;
|
return convertAsciiByte('\r', config);
|
||||||
return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc;
|
|
||||||
}
|
}
|
||||||
lastCr = (c == '\r');
|
lastCr = (c == '\r');
|
||||||
|
|
||||||
|
return convertAsciiByte((char) c, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int convertAsciiByte(char c, FTConfig config) {
|
||||||
if (!config.isRemapFlag()) {
|
if (!config.isRemapFlag()) {
|
||||||
int ebc = translator.unicodeToEbcdic((char) c);
|
int ebc = translator.unicodeToEbcdic(c);
|
||||||
return ebc >= 0 ? ebc : 0x40;
|
return ebc >= 0 ? ebc : 0x40;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +612,7 @@ public class FTDft {
|
|||||||
} else if (c == 0x9F) {
|
} else if (c == 0x9F) {
|
||||||
ebc = 0xFF;
|
ebc = 0xFF;
|
||||||
} else {
|
} else {
|
||||||
ebc = translator.unicodeToEbcdic((char) c);
|
ebc = translator.unicodeToEbcdic(c);
|
||||||
}
|
}
|
||||||
if (ebc < 0) ebc = 0x40;
|
if (ebc < 0) ebc = 0x40;
|
||||||
|
|
||||||
@@ -495,7 +633,7 @@ public class FTDft {
|
|||||||
|
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
input.sendStructuredFieldData(out.toByteArray());
|
||||||
|
|
||||||
if (!messageFlag) {
|
if (listener.getCurrentState() != FTState.NONE) {
|
||||||
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
||||||
listener.onTransferComplete(null);
|
listener.onTransferComplete(null);
|
||||||
}
|
}
|
||||||
@@ -504,13 +642,20 @@ public class FTDft {
|
|||||||
// ========== Data Acknowledgement ==========
|
// ========== Data Acknowledgement ==========
|
||||||
|
|
||||||
private void dftDataAck() {
|
private void dftDataAck() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(12);
|
||||||
out.write(AID_SF);
|
out.write(AID_SF);
|
||||||
out.write(0); out.write(5);
|
out.write(0); out.write(11); // SF length = 11 (0x000B)
|
||||||
out.write(SF_TRANSFER_DATA);
|
out.write(SF_TRANSFER_DATA);
|
||||||
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
|
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
|
||||||
out.write(TR_NORMAL_REPLY & 0xFF);
|
out.write(TR_NORMAL_REPLY & 0xFF);
|
||||||
|
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
|
||||||
|
out.write(TR_RECNUM_HDR & 0xFF);
|
||||||
|
out.write((int) ((recnum >> 24) & 0xFF));
|
||||||
|
out.write((int) ((recnum >> 16) & 0xFF));
|
||||||
|
out.write((int) ((recnum >> 8) & 0xFF));
|
||||||
|
out.write((int) (recnum & 0xFF));
|
||||||
|
|
||||||
|
recnum++;
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
input.sendStructuredFieldData(out.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,11 +682,22 @@ public class FTDft {
|
|||||||
/**
|
/**
|
||||||
* Handle a Read Modified command when upload data is pending.
|
* Handle a Read Modified command when upload data is pending.
|
||||||
* Retransmits the last saved buffer.
|
* Retransmits the last saved buffer.
|
||||||
|
* @return true if a buffer was retransmitted, false otherwise.
|
||||||
*/
|
*/
|
||||||
public void readModified() {
|
public boolean readModified() {
|
||||||
if (dftSaveBuf != null && dftSaveBufLen > 0) {
|
if (dftSaveBuf != null && dftSaveBufLen > 0) {
|
||||||
log.fine("DFT: Retransmitting saved buffer");
|
log.fine("DFT: Retransmitting saved buffer");
|
||||||
input.sendStructuredFieldData(dftSaveBuf);
|
input.sendStructuredFieldData(dftSaveBuf);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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 >= 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
|
import java.awt.Point;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
|
||||||
|
* Matches IBM Host On-Demand (HODTransform.java, PS3179G.java, HODInput.java).
|
||||||
|
*
|
||||||
|
* <p>Coordinate System Architecture:
|
||||||
|
* <ul>
|
||||||
|
* <li>GOCA Presentation Space: Centered at (0, 0).
|
||||||
|
* X ranges [-xMax .. +xMax] where width = cols * defaultCharWidth (defsx = 9).
|
||||||
|
* Y ranges [-yMax .. +yMax] where height = rows * defaultCharHeight (defsy = 16 or 12).
|
||||||
|
* (+Y is UP, -Y is DOWN, +X is RIGHT, -X is LEFT).</li>
|
||||||
|
* <li>Base Presentation Space (px, py): Unsigned top-left origin (0, 0).
|
||||||
|
* px = gx + xMax (ranges 0 .. totalWidth).
|
||||||
|
* py = yMax - gy (ranges 0 .. totalHeight).</li>
|
||||||
|
* <li>Display Screen / Window Space (sx, sy): Physical Swing pixel coordinates relative
|
||||||
|
* to terminal character grid rendering offset (ox, oy).
|
||||||
|
* sx = ox + (int) Math.round(px * transformX).
|
||||||
|
* sy = oy + (int) Math.round(py * transformY).
|
||||||
|
* where transformX = cellWidth / defaultCharWidth, transformY = cellHeight / defaultCharHeight.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Why simple fixed pixel scaling or hardcoded multipliers failed in past iterations:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Failure Mode 1</b>: Hardcoding totalHeight = rows * 12 caused coordinate truncation in 43-row mode (Model 4),
|
||||||
|
* capping yMax at 257 instead of 343 and making the top menu / EXIT button unreachable.</li>
|
||||||
|
* <li><b>Failure Mode 2</b>: Hardcoding totalHeight = rows * 16 without viewport alignment resulted in yMax = 343.
|
||||||
|
* In ADMDRAW, GDDM defines the entire active drawing area between y = -169 and y = +200 (~370 units total height).
|
||||||
|
* Mapping this into a 688-high canvas placed the top menu at Row 9.2 (middle of the screen) with a large blank void above,
|
||||||
|
* and clicking the visual menu emitted gy = 224 (missing the [187..200] menu bounding box).</li>
|
||||||
|
* <li><b>Failure Mode 3</b>: Intermediate raster buffering (e.g. fixed 800x600 canvas stretched to gridW x gridH)
|
||||||
|
* introduced rounding artifacts in bidirectional coordinate conversion (unmapX/unmapY).</li>
|
||||||
|
* <li><b>Failure Mode 4</b>: Overlapping dropdown menus (FILE, DRAW, TRANSFORM) lingering simultaneously when
|
||||||
|
* segment retention/clearing was decoupled from GDDM's segment lifecycle.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class GddmCoordinateTransform {
|
||||||
|
|
||||||
|
public static final int DEFAULT_CHAR_WIDTH = 9; // defsx: 3179G standard character cell width
|
||||||
|
public static final int DEFAULT_CHAR_HEIGHT_16 = 16; // defsy: 3179G standard character cell height
|
||||||
|
public static final int DEFAULT_CHAR_HEIGHT_12 = 12; // defsy: 3279-2 alternate character cell height
|
||||||
|
|
||||||
|
private int defaultCharWidth = DEFAULT_CHAR_WIDTH;
|
||||||
|
private int defaultCharHeight = DEFAULT_CHAR_HEIGHT_16;
|
||||||
|
|
||||||
|
private int screenCols = 80;
|
||||||
|
private int screenRows = 24;
|
||||||
|
|
||||||
|
private double transformX = 1.0;
|
||||||
|
private double transformY = 1.0;
|
||||||
|
|
||||||
|
public GddmCoordinateTransform() {
|
||||||
|
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GddmCoordinateTransform(int screenCols, int screenRows) {
|
||||||
|
this(screenCols, screenRows, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GddmCoordinateTransform(int screenCols, int screenRows, int defCharWidth, int defCharHeight) {
|
||||||
|
this.screenCols = screenCols > 0 ? screenCols : 80;
|
||||||
|
this.screenRows = screenRows > 0 ? screenRows : 24;
|
||||||
|
this.defaultCharWidth = defCharWidth > 0 ? defCharWidth : DEFAULT_CHAR_WIDTH;
|
||||||
|
this.defaultCharHeight = defCharHeight > 0 ? defCharHeight : DEFAULT_CHAR_HEIGHT_16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates screen grid and cell dimensions from UI.
|
||||||
|
*
|
||||||
|
* @param cellWidth Width of a character cell in Swing pixels
|
||||||
|
* @param cellHeight Height of a character cell in Swing pixels
|
||||||
|
*/
|
||||||
|
public void updateDisplayMetrics(int cellWidth, int cellHeight) {
|
||||||
|
if (cellWidth > 0 && defaultCharWidth > 0) {
|
||||||
|
this.transformX = (double) cellWidth / (double) defaultCharWidth;
|
||||||
|
}
|
||||||
|
if (cellHeight > 0 && defaultCharHeight > 0) {
|
||||||
|
this.transformY = (double) cellHeight / (double) defaultCharHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScreenDimensions(int cols, int rows) {
|
||||||
|
if (cols > 0) this.screenCols = cols;
|
||||||
|
if (rows > 0) this.screenRows = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultCharMetrics(int defWidth, int defHeight) {
|
||||||
|
if (defWidth > 0) this.defaultCharWidth = defWidth;
|
||||||
|
if (defHeight > 0) this.defaultCharHeight = defHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalWidth() {
|
||||||
|
return screenCols * defaultCharWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalHeight() {
|
||||||
|
return screenRows * defaultCharHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getXMax() {
|
||||||
|
int totalW = getTotalWidth();
|
||||||
|
return (totalW - 1) / 2 + (totalW - 1) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getYMax() {
|
||||||
|
int totalH = getTotalHeight();
|
||||||
|
return (totalH - 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTransformX() {
|
||||||
|
return transformX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTransformY() {
|
||||||
|
return transformY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultCharWidth() {
|
||||||
|
return defaultCharWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultCharHeight() {
|
||||||
|
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).
|
||||||
|
*/
|
||||||
|
public Point gocaToBase(int gx, int gy) {
|
||||||
|
int px = gx + getXMax();
|
||||||
|
int py = getYMax() - gy;
|
||||||
|
return new Point(px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a base presentation space coordinate (px, py) to GOCA signed coordinate (gx, gy).
|
||||||
|
*/
|
||||||
|
public Point baseToGoca(int px, int py) {
|
||||||
|
int gx = px - getXMax();
|
||||||
|
int gy = getYMax() - py;
|
||||||
|
return new Point(gx, gy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GOCA signed coordinate (gx, gy) directly to Swing screen pixel coordinate (sx, sy).
|
||||||
|
*/
|
||||||
|
public Point gocaToScreenPixel(int gx, int gy, int ox, int oy, int cellWidth, int cellHeight) {
|
||||||
|
updateDisplayMetrics(cellWidth, cellHeight);
|
||||||
|
int px = gx + getXMax();
|
||||||
|
int py = getYMax() - gy;
|
||||||
|
int sx = ox + (int) Math.round(px * transformX);
|
||||||
|
int sy = oy + (int) Math.round(py * transformY);
|
||||||
|
return new Point(sx, sy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a mouse click in Swing screen coordinates (mouseX, mouseY) directly to GOCA signed coordinate (gx, gy).
|
||||||
|
* Matches HODInput.getHODInput:
|
||||||
|
* x = mouseX / transformX
|
||||||
|
* y = mouseY / transformY
|
||||||
|
* gx = x - xMax
|
||||||
|
* gy = yMax - y
|
||||||
|
*/
|
||||||
|
public Point screenPixelToGoca(int mouseX, int mouseY, int ox, int oy, int cellWidth, int cellHeight) {
|
||||||
|
updateDisplayMetrics(cellWidth, cellHeight);
|
||||||
|
double relX = mouseX - ox;
|
||||||
|
double relY = mouseY - oy;
|
||||||
|
|
||||||
|
int px = (transformX > 0) ? (int) Math.round(relX / transformX) : (int) relX;
|
||||||
|
int py = (transformY > 0) ? (int) Math.round(relY / transformY) : (int) relY;
|
||||||
|
|
||||||
|
int gx = px - getXMax();
|
||||||
|
int gy = getYMax() - py;
|
||||||
|
return new Point(gx, gy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps GOCA (gx, gy) to fixed canvas buffer dimensions (canvasWidth, canvasHeight).
|
||||||
|
*/
|
||||||
|
public Point gocaToCanvasPixel(int gx, int gy, int canvasWidth, int canvasHeight) {
|
||||||
|
int totalW = getTotalWidth();
|
||||||
|
int totalH = getTotalHeight();
|
||||||
|
int xMax = getXMax();
|
||||||
|
int yMax = getYMax();
|
||||||
|
|
||||||
|
int px = (int) Math.round((double) (gx + xMax) * canvasWidth / (totalW > 0 ? totalW : 1));
|
||||||
|
int py = (int) Math.round((double) (yMax - gy) * canvasHeight / (totalH > 0 ? totalH : 1));
|
||||||
|
return new Point(px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps fixed canvas buffer pixel (canvasX, canvasY) to GOCA (gx, gy).
|
||||||
|
*/
|
||||||
|
public Point canvasPixelToGoca(int canvasX, int canvasY, int canvasWidth, int canvasHeight) {
|
||||||
|
int totalW = getTotalWidth();
|
||||||
|
int totalH = getTotalHeight();
|
||||||
|
int xMax = getXMax();
|
||||||
|
int yMax = getYMax();
|
||||||
|
|
||||||
|
int nx = (int) Math.round((double) canvasX * totalW / (canvasWidth > 0 ? canvasWidth : 1));
|
||||||
|
int ny = (int) Math.round((double) canvasY * totalH / (canvasHeight > 0 ? canvasHeight : 1));
|
||||||
|
return new Point(nx - xMax, yMax - ny);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,10 +60,9 @@ public final class GocaConstants {
|
|||||||
public static final int G_GSCR = 0x35; // Set Character Shear
|
public static final int G_GSCR = 0x35; // Set Character Shear
|
||||||
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
||||||
public static final int G_GSCS = 0x38; // Set Character Set
|
public static final int G_GSCS = 0x38; // Set Character Set
|
||||||
public static final int G_GSMP = 0x39; // Set Marker Precision
|
public static final int G_GSCC = 0x39; // Set Character Precision
|
||||||
public static final int G_GSETAG = 0x39; // Set Pick Identifier / Tag
|
|
||||||
public static final int G_GSCD = 0x3A; // Set Character Direction
|
public static final int G_GSCD = 0x3A; // Set Character Direction
|
||||||
public static final int G_GSCC = 0x3B; // Set Character Precision
|
public static final int G_GSMP = 0x3B; // Set Marker Precision
|
||||||
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
|
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
|
||||||
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
|
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
|
||||||
public static final int G_GPOP = 0x3F; // Pop Attribute
|
public static final int G_GPOP = 0x3F; // Pop Attribute
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class GocaDecoder {
|
|||||||
private int markerType = GocaConstants.MK_PLUS;
|
private int markerType = GocaConstants.MK_PLUS;
|
||||||
private int markerSize = 5;
|
private int markerSize = 5;
|
||||||
private int markerColor = GocaConstants.GOCA_COLORS[0];
|
private int markerColor = GocaConstants.GOCA_COLORS[0];
|
||||||
|
private int markerPrecision = 0;
|
||||||
private int pattern = GocaConstants.PT_SOLID;
|
private int pattern = GocaConstants.PT_SOLID;
|
||||||
private int patternSet = 0;
|
private int patternSet = 0;
|
||||||
private int fillColor = GocaConstants.GOCA_COLORS[0];
|
private int fillColor = GocaConstants.GOCA_COLORS[0];
|
||||||
@@ -62,9 +63,6 @@ public class GocaDecoder {
|
|||||||
|
|
||||||
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
||||||
private final java.util.Map<Integer, byte[]> segmentStore = new java.util.HashMap<>();
|
private final java.util.Map<Integer, byte[]> segmentStore = new java.util.HashMap<>();
|
||||||
private final java.util.Map<Integer, Integer> segmentChainMap = new java.util.HashMap<>();
|
|
||||||
private final java.util.List<Integer> segmentOrderList = new java.util.ArrayList<>();
|
|
||||||
private final java.util.Set<Integer> chainedTargets = new java.util.HashSet<>();
|
|
||||||
private int callDepth = 0;
|
private int callDepth = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,6 +146,8 @@ public class GocaDecoder {
|
|||||||
return graphicCursorY;
|
return graphicCursorY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final int GDDM_CURSOR_OFFSET_Y = 0;
|
||||||
|
|
||||||
public synchronized void setGraphicCursorPosition(int x, int y) {
|
public synchronized void setGraphicCursorPosition(int x, int y) {
|
||||||
this.graphicCursorX = x;
|
this.graphicCursorX = x;
|
||||||
this.graphicCursorY = y;
|
this.graphicCursorY = y;
|
||||||
@@ -163,15 +163,24 @@ public class GocaDecoder {
|
|||||||
public synchronized int findPickedSegment(int gx, int gy) {
|
public synchronized int findPickedSegment(int gx, int gy) {
|
||||||
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
|
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
|
||||||
SegmentBounds sb = activeSegmentsInOrder.get(i);
|
SegmentBounds sb = activeSegmentsInOrder.get(i);
|
||||||
if (sb.contains(gx, gy, 12)) {
|
if (sb.contains(gx, gy, 25)) {
|
||||||
System.err.println(String.format(
|
logger.info(String.format(
|
||||||
"findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
"findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||||
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||||
));
|
));
|
||||||
return sb.segId;
|
return sb.segId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
|
for (SegmentBounds sb : segmentBoundsMap.values()) {
|
||||||
|
if (sb.contains(gx, gy, 25)) {
|
||||||
|
logger.info(String.format(
|
||||||
|
"findPickedSegment (fallback): goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||||
|
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||||
|
));
|
||||||
|
return sb.segId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,9 +205,6 @@ public class GocaDecoder {
|
|||||||
graphicCursorX = 0;
|
graphicCursorX = 0;
|
||||||
graphicCursorY = 0;
|
graphicCursorY = 0;
|
||||||
segmentStore.clear();
|
segmentStore.clear();
|
||||||
segmentChainMap.clear();
|
|
||||||
segmentOrderList.clear();
|
|
||||||
chainedTargets.clear();
|
|
||||||
segmentBoundsMap.clear();
|
segmentBoundsMap.clear();
|
||||||
activeSegmentsInOrder.clear();
|
activeSegmentsInOrder.clear();
|
||||||
currentSegId = 0;
|
currentSegId = 0;
|
||||||
@@ -214,6 +220,7 @@ public class GocaDecoder {
|
|||||||
markerType = GocaConstants.MK_PLUS;
|
markerType = GocaConstants.MK_PLUS;
|
||||||
markerSize = 5;
|
markerSize = 5;
|
||||||
markerColor = curColor;
|
markerColor = curColor;
|
||||||
|
markerPrecision = 0;
|
||||||
pattern = GocaConstants.PT_SOLID;
|
pattern = GocaConstants.PT_SOLID;
|
||||||
patternSet = 0;
|
patternSet = 0;
|
||||||
fillColor = curColor;
|
fillColor = curColor;
|
||||||
@@ -251,20 +258,20 @@ public class GocaDecoder {
|
|||||||
*/
|
*/
|
||||||
private int getOrderLength(byte[] data, int idx, int end) {
|
private int getOrderLength(byte[] data, int idx, int end) {
|
||||||
int order = data[idx] & 0xFF;
|
int order = data[idx] & 0xFF;
|
||||||
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 ||
|
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
|
||||||
order == GocaConstants.G_GEAR || order == GocaConstants.G_ENDSEGM ||
|
|
||||||
order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_GEIMG ||
|
|
||||||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GERASE) {
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 91 00)
|
||||||
|
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
|
||||||
|
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
|
||||||
|
order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG) {
|
||||||
|
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
||||||
|
}
|
||||||
if (idx + 1 >= end) {
|
if (idx + 1 >= end) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
// Fixed 2-byte orders: 1-byte opcode + 1-byte operand
|
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
|
||||||
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
|
if (order < 0x20) {
|
||||||
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
|
|
||||||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSLT ||
|
|
||||||
order == GocaConstants.G_GSLW || order == GocaConstants.G_GSMS) {
|
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1)
|
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1)
|
||||||
@@ -281,59 +288,68 @@ public class GocaDecoder {
|
|||||||
return (data[idx + 1] & 0xFF) + 2;
|
return (data[idx + 1] & 0xFF) + 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void indexSegments(byte[] data, int offset, int length) {
|
/**
|
||||||
int idx = offset;
|
* Decodes a stream of GOCA drawing orders (matching IBM Host On-Demand HODDecoder.decodeGOCA).
|
||||||
int end = offset + length;
|
*/
|
||||||
while (idx < end) {
|
public synchronized void decodeGoca(byte[] data, int offset, int length) {
|
||||||
int order = data[idx] & 0xFF;
|
decodeStream(data, offset, length);
|
||||||
if (order == GocaConstants.G_BEGSEGM) {
|
|
||||||
int segStart = idx;
|
|
||||||
int segLen = getOrderLength(data, idx, end);
|
|
||||||
if (segLen <= 0 || idx + 5 >= end) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int segId = ((data[idx + 2] & 0xFF) << 24) |
|
|
||||||
((data[idx + 3] & 0xFF) << 16) |
|
|
||||||
((data[idx + 4] & 0xFF) << 8) |
|
|
||||||
(data[idx + 5] & 0xFF);
|
|
||||||
|
|
||||||
int nextId = 0;
|
|
||||||
if (segLen >= 14 && (data[idx + 1] & 0xFF) >= 12) {
|
|
||||||
nextId = ((data[idx + 10] & 0xFF) << 24) |
|
|
||||||
((data[idx + 11] & 0xFF) << 16) |
|
|
||||||
((data[idx + 12] & 0xFF) << 8) |
|
|
||||||
(data[idx + 13] & 0xFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int searchIdx = idx + segLen;
|
/**
|
||||||
while (searchIdx < end) {
|
* Decodes a character stream of GOCA drawing orders.
|
||||||
int o = data[searchIdx] & 0xFF;
|
*/
|
||||||
int oLen = getOrderLength(data, searchIdx, end);
|
public synchronized void decodeGoca(char[] data, int offset, int length) {
|
||||||
if (oLen <= 0) break;
|
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) return;
|
||||||
if (o == GocaConstants.G_ENDSEGM) {
|
byte[] b = new byte[length];
|
||||||
searchIdx += oLen;
|
for (int i = 0; i < length; i++) {
|
||||||
break;
|
b[i] = (byte) (data[offset + i] & 0xFF);
|
||||||
}
|
}
|
||||||
searchIdx += oLen;
|
decodeGoca(b, 0, length);
|
||||||
}
|
}
|
||||||
int fullSegLen = searchIdx - segStart;
|
|
||||||
if (fullSegLen > 0 && segStart + fullSegLen <= end) {
|
/**
|
||||||
byte[] segBytes = new byte[fullSegLen];
|
* Processes a discrete GOCA segment.
|
||||||
System.arraycopy(data, segStart, segBytes, 0, fullSegLen);
|
*/
|
||||||
segmentStore.put(segId, segBytes);
|
public synchronized void processSegment(byte[] data, int offset, int length) {
|
||||||
segmentOrderList.add(segId);
|
decodeStream(data, offset, length);
|
||||||
if (nextId != 0) {
|
|
||||||
segmentChainMap.put(segId, nextId);
|
|
||||||
chainedTargets.add(nextId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
}
|
}
|
||||||
idx = searchIdx;
|
|
||||||
|
/**
|
||||||
|
* Executes a stored procedure segment by segment ID.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
} else {
|
} else {
|
||||||
int oLen = getOrderLength(data, idx, end);
|
logger.warning("procedureSegment: Segment not found in store: " + segId);
|
||||||
if (oLen <= 0) break;
|
|
||||||
idx += oLen;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a procedure segment from raw data buffer.
|
||||||
|
*/
|
||||||
|
public synchronized void procedureSegment(byte[] procData, int offset, int length) {
|
||||||
|
decodeStream(procData, offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -361,10 +377,6 @@ public class GocaDecoder {
|
|||||||
end = offset + length;
|
end = offset + length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callDepth == 0) {
|
|
||||||
indexSegments(inputData, idx, end - idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
decodeStreamDirect(inputData, idx, end - idx);
|
decodeStreamDirect(inputData, idx, end - idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +424,27 @@ public class GocaDecoder {
|
|||||||
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
|
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
|
||||||
activeSegmentsInOrder.remove(sb);
|
activeSegmentsInOrder.remove(sb);
|
||||||
activeSegmentsInOrder.add(sb);
|
activeSegmentsInOrder.add(sb);
|
||||||
|
|
||||||
|
if (segId != 0 && callDepth == 0) {
|
||||||
|
int segStart = idx;
|
||||||
|
int searchIdx = idx + orderLen;
|
||||||
|
while (searchIdx < end) {
|
||||||
|
int o = inputData[searchIdx] & 0xFF;
|
||||||
|
int oLen = getOrderLength(inputData, searchIdx, end);
|
||||||
|
if (oLen <= 0) break;
|
||||||
|
if (o == GocaConstants.G_ENDSEGM) {
|
||||||
|
searchIdx += oLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
searchIdx += oLen;
|
||||||
|
}
|
||||||
|
int fullSegLen = searchIdx - segStart;
|
||||||
|
if (fullSegLen > 0 && segStart + fullSegLen <= end) {
|
||||||
|
byte[] segBytes = new byte[fullSegLen];
|
||||||
|
System.arraycopy(inputData, segStart, segBytes, 0, fullSegLen);
|
||||||
|
segmentStore.put(segId, segBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (idx + 6 < end) flag0 = inputData[idx + 6] & 0xFF;
|
if (idx + 6 < end) flag0 = inputData[idx + 6] & 0xFF;
|
||||||
if (idx + 7 < end) flag1 = inputData[idx + 7] & 0xFF;
|
if (idx + 7 < end) flag1 = inputData[idx + 7] & 0xFF;
|
||||||
@@ -427,7 +460,7 @@ public class GocaDecoder {
|
|||||||
if (currentSegId != 0) {
|
if (currentSegId != 0) {
|
||||||
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
||||||
if (sb != null) {
|
if (sb != null) {
|
||||||
System.err.println(String.format(
|
logger.info(String.format(
|
||||||
"GocaDecoder: ENDSEGM segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
"GocaDecoder: ENDSEGM segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||||
sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||||
));
|
));
|
||||||
@@ -437,17 +470,7 @@ public class GocaDecoder {
|
|||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GSETAG: { // Set Pick Identifier / Tag (0x39)
|
|
||||||
if (currentSegId != 0 && payloadLen >= 2 && idx + 3 < end) {
|
|
||||||
int tag = ((inputData[idx + 2] & 0xFF) << 8) | (inputData[idx + 3] & 0xFF);
|
|
||||||
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
|
||||||
if (sb != null) {
|
|
||||||
sb.tag = tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E)
|
case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E)
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
@@ -558,11 +581,16 @@ public class GocaDecoder {
|
|||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 0x06:
|
||||||
|
case 0x11:
|
||||||
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
||||||
lineType = inputData[idx + 1] & 0xFF;
|
lineType = inputData[idx + 1] & 0xFF;
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 0x04:
|
||||||
|
case 0x05:
|
||||||
|
case 0x12:
|
||||||
case GocaConstants.G_GSLW: { // Set Line Width (0x19)
|
case GocaConstants.G_GSLW: { // Set Line Width (0x19)
|
||||||
lineWidth = inputData[idx + 1] & 0xFF;
|
lineWidth = inputData[idx + 1] & 0xFF;
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
@@ -602,19 +630,23 @@ public class GocaDecoder {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GSCS: { // Set Character Set (0x38)
|
case GocaConstants.G_GSCS: { // Set Character Set (0x38)
|
||||||
charSet = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
int cs = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||||
|
charSet = (cs == 0xF0) ? 0 : cs;
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GSCC: { // Set Character Precision (0x3B)
|
case GocaConstants.G_GSCC: { // Set Character Precision (0x39)
|
||||||
charPrecision = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
charPrecision = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||||
if (charPrecision == 0) charPrecision = GocaConstants.CP_STRING;
|
if (charPrecision == 0) charPrecision = GocaConstants.CP_STRING;
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 0x04:
|
case GocaConstants.G_GSMP: { // Set Marker Precision (0x3B)
|
||||||
|
markerPrecision = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case GocaConstants.G_GSMX:
|
case GocaConstants.G_GSMX:
|
||||||
case GocaConstants.G_GSFLW:
|
|
||||||
case GocaConstants.G_GSMS_SET:
|
case GocaConstants.G_GSMS_SET:
|
||||||
case GocaConstants.G_GPOP: {
|
case GocaConstants.G_GPOP: {
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
@@ -793,9 +825,15 @@ public class GocaDecoder {
|
|||||||
idx += 2;
|
idx += 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
|
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space (HODEraseGraphicsPlane)
|
||||||
plane.clear();
|
plane.clear();
|
||||||
resetDefaults();
|
resetAttributes();
|
||||||
|
curX = 0;
|
||||||
|
curY = 0;
|
||||||
|
activeSegmentsInOrder.clear();
|
||||||
|
segmentBoundsMap.clear();
|
||||||
|
segmentStore.clear();
|
||||||
|
logger.info("GOCA P_ERASE: erased graphics presentation space and cleared segment stores");
|
||||||
idx += 2;
|
idx += 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -803,20 +841,21 @@ public class GocaDecoder {
|
|||||||
idx += 12;
|
idx += 12;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.P_SCUDEF: { // 0x21: Drawing Process Control / Segment Execute
|
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults (HODCurrentDefaults)
|
||||||
|
// Note: Per IBM 3179G / HOD architecture, 0x21 sets default drawing attributes (color, line, pattern).
|
||||||
|
// It is NOT an executive segment redraw order. A prior attempt treated 0x21 as an invented
|
||||||
|
// P_SCUDEF segment redraw loop, which caused old dropdown menus and segments to be repeatedly
|
||||||
|
// repainted on top of the screen, creating ghost artifacts and stale bounding boxes.
|
||||||
if (idx + 1 < end) {
|
if (idx + 1 < end) {
|
||||||
int len = (data[idx + 1] & 0xFF) + 2;
|
int pLen = data[idx + 1] & 0xFF;
|
||||||
idx += len;
|
logger.fine(String.format("GOCA Set Current Defaults (0x21): len=%d", pLen));
|
||||||
|
idx += pLen + 2;
|
||||||
} else {
|
} else {
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position
|
case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position (HODGraphicCursorPosition - No-op per HOD architecture)
|
||||||
if (idx + 5 <= end) {
|
|
||||||
this.graphicCursorX = readCoord(data, idx + 2);
|
|
||||||
this.graphicCursorY = readCoord(data, idx + 4);
|
|
||||||
}
|
|
||||||
if (idx + 1 < end) {
|
if (idx + 1 < end) {
|
||||||
int len = data[idx + 1] & 0xFF;
|
int len = data[idx + 1] & 0xFF;
|
||||||
idx += 2 + len;
|
idx += 2 + len;
|
||||||
@@ -1162,30 +1201,30 @@ public class GocaDecoder {
|
|||||||
|
|
||||||
List<Double> ptsX = new ArrayList<>();
|
List<Double> ptsX = new ArrayList<>();
|
||||||
List<Double> ptsY = new ArrayList<>();
|
List<Double> ptsY = new ArrayList<>();
|
||||||
|
List<Integer> gocaPtsX = new ArrayList<>();
|
||||||
|
List<Integer> gocaPtsY = new ArrayList<>();
|
||||||
|
|
||||||
if (fromCurPos) {
|
if (fromCurPos) {
|
||||||
|
trackPoint(curX, curY);
|
||||||
ptsX.add(plane.mapXDouble(curX));
|
ptsX.add(plane.mapXDouble(curX));
|
||||||
ptsY.add(plane.mapYDouble(curY));
|
ptsY.add(plane.mapYDouble(curY));
|
||||||
if (inArea) {
|
gocaPtsX.add(curX);
|
||||||
if (currentPolyPts == 0) {
|
gocaPtsY.add(curY);
|
||||||
|
if (inArea && currentPolyPts == 0) {
|
||||||
addAreaLineStart(curX, curY);
|
addAreaLineStart(curX, curY);
|
||||||
} else {
|
|
||||||
addAreaPoint(curX, curY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (pos + 4 <= end) {
|
while (pos + 4 <= end) {
|
||||||
int x = readCoord(data, pos);
|
int x = readCoord(data, pos);
|
||||||
int y = readCoord(data, pos + 2);
|
int y = readCoord(data, pos + 2);
|
||||||
|
trackPoint(x, y);
|
||||||
ptsX.add(plane.mapXDouble(x));
|
ptsX.add(plane.mapXDouble(x));
|
||||||
ptsY.add(plane.mapYDouble(y));
|
ptsY.add(plane.mapYDouble(y));
|
||||||
if (inArea) {
|
gocaPtsX.add(x);
|
||||||
if (ptsX.size() == 1 && currentPolyPts == 0) {
|
gocaPtsY.add(y);
|
||||||
|
if (inArea && ptsX.size() == 1 && currentPolyPts == 0) {
|
||||||
addAreaLineStart(x, y);
|
addAreaLineStart(x, y);
|
||||||
} else {
|
|
||||||
addAreaPoint(x, y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
curX = x;
|
curX = x;
|
||||||
curY = y;
|
curY = y;
|
||||||
@@ -1200,9 +1239,24 @@ public class GocaDecoder {
|
|||||||
px[i] = ptsX.get(i);
|
px[i] = ptsX.get(i);
|
||||||
py[i] = ptsY.get(i);
|
py[i] = ptsY.get(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
||||||
int pos = off;
|
int pos = off;
|
||||||
@@ -1240,28 +1294,53 @@ public class GocaDecoder {
|
|||||||
int textLen = end - pos;
|
int textLen = end - pos;
|
||||||
if (textLen <= 0) return;
|
if (textLen <= 0) return;
|
||||||
|
|
||||||
// IBM 3279 vector graphics base cell is 9x12
|
// IBM 3179G vector graphics base cell is 9x16
|
||||||
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
|
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
|
||||||
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14.0;
|
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
|
||||||
|
|
||||||
int totalW = textLen * (charWidth > 0 ? charWidth : 9);
|
int cellW = (charWidth > 0 ? charWidth : 9);
|
||||||
int totalH = (charHeight > 0 ? charHeight : 14);
|
int cellH = (charHeight > 0 ? charHeight : 16);
|
||||||
|
switch (charDir) {
|
||||||
|
case GocaConstants.CD_TB:
|
||||||
trackPoint(startX, startY);
|
trackPoint(startX, startY);
|
||||||
trackPoint(startX + totalW, startY + totalH);
|
trackPoint(startX + cellW, startY - textLen * cellH);
|
||||||
trackPoint(startX + totalW, startY - totalH);
|
break;
|
||||||
|
case GocaConstants.CD_RL:
|
||||||
|
trackPoint(startX, startY);
|
||||||
|
trackPoint(startX - textLen * cellW, startY + cellH);
|
||||||
|
break;
|
||||||
|
case GocaConstants.CD_BT:
|
||||||
|
trackPoint(startX, startY);
|
||||||
|
trackPoint(startX + cellW, startY + textLen * cellH);
|
||||||
|
break;
|
||||||
|
case GocaConstants.CD_LR:
|
||||||
|
case GocaConstants.CD_DEFAULT:
|
||||||
|
default:
|
||||||
|
trackPoint(startX, startY);
|
||||||
|
trackPoint(startX + textLen * cellW, startY + cellH);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (charSet != 0 && programSymbolManager != null) {
|
if (charSet == 0xF8 || charPrecision == GocaConstants.CP_STROKE) {
|
||||||
|
char[] chars = new char[textLen];
|
||||||
|
for (int i = 0; i < textLen; i++) {
|
||||||
|
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
||||||
|
}
|
||||||
|
String text = new String(chars);
|
||||||
|
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
||||||
|
curColor, cw, ch, charDir, charAngle);
|
||||||
|
} else if (charSet != 0 && programSymbolManager != null) {
|
||||||
for (int i = 0; i < textLen; i++) {
|
for (int i = 0; i < textLen; i++) {
|
||||||
int code = data[pos + i] & 0xFF;
|
int code = data[pos + i] & 0xFF;
|
||||||
double px = plane.mapXDouble(startX);
|
double px = plane.mapXDouble(startX);
|
||||||
double py = plane.mapYDouble(startY) - ch;
|
double py = plane.mapYDouble(startY);
|
||||||
ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code);
|
ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code);
|
||||||
if (slot != null) {
|
if (slot != null) {
|
||||||
int[] rgb = slot.getRgbPixels(curColor, 0);
|
int[] rgb = slot.getRgbPixels(curColor, 0);
|
||||||
int symW = slot.getWidth();
|
int symW = slot.getWidth();
|
||||||
int symH = slot.getHeight();
|
int symH = slot.getHeight();
|
||||||
int ipx = (int) Math.round(px);
|
int ipx = (int) Math.round(px);
|
||||||
int ipy = (int) Math.round(py);
|
int ipy = (int) Math.round(py - ch);
|
||||||
int icw = (int) Math.round(cw);
|
int icw = (int) Math.round(cw);
|
||||||
int ich = (int) Math.round(ch);
|
int ich = (int) Math.round(ch);
|
||||||
for (int dy = 0; dy < ich; dy++) {
|
for (int dy = 0; dy < ich; dy++) {
|
||||||
@@ -1274,33 +1353,123 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
|
||||||
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
|
|
||||||
}
|
}
|
||||||
startX += (charWidth > 0 ? charWidth : 9);
|
startX += (charWidth > 0 ? charWidth : 9);
|
||||||
}
|
}
|
||||||
curX = startX;
|
curX = startX;
|
||||||
curY = startY;
|
curY = startY;
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
|
|
||||||
char[] chars = new char[textLen];
|
char[] chars = new char[textLen];
|
||||||
for (int i = 0; i < textLen; i++) {
|
for (int i = 0; i < textLen; i++) {
|
||||||
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
||||||
}
|
}
|
||||||
String text = new String(chars);
|
String text = new String(chars);
|
||||||
|
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
||||||
if (charPrecision == GocaConstants.CP_STROKE) {
|
|
||||||
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text,
|
|
||||||
curColor, cw, ch, charDir, charAngle);
|
|
||||||
} else {
|
|
||||||
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text,
|
|
||||||
curColor, cw, ch, charDir, charAngle);
|
curColor, cw, ch, charDir, charAngle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (charDir) {
|
||||||
|
case GocaConstants.CD_TB:
|
||||||
|
curX = startX;
|
||||||
|
curY = startY - (textLen * (charHeight > 0 ? charHeight : 16));
|
||||||
|
break;
|
||||||
|
case GocaConstants.CD_RL:
|
||||||
|
curX = startX - (textLen * (charWidth > 0 ? charWidth : 9));
|
||||||
|
curY = startY;
|
||||||
|
break;
|
||||||
|
case GocaConstants.CD_BT:
|
||||||
|
curX = startX;
|
||||||
|
curY = startY + (textLen * (charHeight > 0 ? charHeight : 16));
|
||||||
|
break;
|
||||||
|
case GocaConstants.CD_LR:
|
||||||
|
case GocaConstants.CD_DEFAULT:
|
||||||
|
default:
|
||||||
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
|
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
|
||||||
curY = startY;
|
curY = startY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
private int readCoord(byte[] data, int off) {
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ package haus.nightmare.lib3270j.graphics;
|
|||||||
*/
|
*/
|
||||||
public class GraphicInputBuilder {
|
public class GraphicInputBuilder {
|
||||||
|
|
||||||
// 56-byte template mask from IBM Host On-Demand (HODInput.java)
|
// 56-byte template mask from IBM Host On-Demand (HODInput.java).
|
||||||
|
// Note on Structured Field Length (bytes 0-1):
|
||||||
|
// IBM HOD sets bytes 0-1 to 0x00 0x34 (52 decimal). In 3270 GOCA architecture,
|
||||||
|
// the Data Unit Object Control payload is 52 bytes. Overwriting this with 0x00 0x38 (56)
|
||||||
|
// causes the host's 3270 inbound structured field decoder to misalign the trailing AID byte
|
||||||
|
// (0x7D) and cursor address by 4 bytes, causing host GDDM to reject the click with an alarm beep.
|
||||||
private static final byte[] MASK = new byte[] {
|
private static final byte[] MASK = new byte[] {
|
||||||
0x00, 0x34, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
|
0x00, 0x34, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
|
||||||
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
@@ -22,45 +27,50 @@ public class GraphicInputBuilder {
|
|||||||
*
|
*
|
||||||
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
||||||
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
||||||
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
|
* @param buttonOrAidCode The mouse button number (1=Pick, 2=Action) or 3270 AID code for keyboard
|
||||||
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
|
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
|
||||||
* @param isShift true if shift key was down
|
* @param isShift true if shift key was down
|
||||||
* @param isCtrl true if ctrl key was down
|
* @param isCtrl true if ctrl key was down
|
||||||
* @return 56-byte payload
|
* @return 56-byte payload
|
||||||
*/
|
*/
|
||||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
|
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
|
||||||
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||||
return buildGraphicInput(gocaX, gocaY, aidCode, isMouseAction, isShift, isCtrl, 0, 0);
|
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
|
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag (legacy overload).
|
||||||
*
|
* Note: Per IBM HOD / 3179G architecture, pick correlation is evaluated host-side by GDDM using (gx, gy).
|
||||||
* IMPORTANT ARCHITECTURE NOTE:
|
|
||||||
* Per IBM GA23-0059 / GDDM specifications:
|
|
||||||
* - Bytes 24-27: (gocaX, gocaY) cursor coordinates.
|
|
||||||
* - Bytes 28-31: Picked Segment Identifier (32-bit big-endian). When clicking on a menu item or
|
|
||||||
* interactive element (e.g. DRAW button = Segment 2, EXIT button = Segment 5), GDDM requires
|
|
||||||
* the exact segment ID in bytes 28-31. If hardcoded or mismatched, GDDM rejects the click
|
|
||||||
* with a WCC 0xF7 alarm beep.
|
|
||||||
* - Bytes 32-33: Pick Correlation Tag (16-bit big-endian) set by G_GSETAG (0x39).
|
|
||||||
*
|
|
||||||
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
|
||||||
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
|
||||||
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
|
|
||||||
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
|
|
||||||
* @param isShift true if shift key was down
|
|
||||||
* @param isCtrl true if ctrl key was down
|
|
||||||
* @param pickedSegId Picked GOCA segment ID (0 if none)
|
|
||||||
* @param pickTag Pick correlation tag
|
|
||||||
* @return 56-byte payload
|
|
||||||
*/
|
*/
|
||||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
|
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
|
||||||
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||||
int pickedSegId, int pickTag) {
|
int pickedSegId, int pickTag) {
|
||||||
|
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 56-byte Graphic Input Structured Field with cursor row/col, picked segment ID and correlation tag.
|
||||||
|
*/
|
||||||
|
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
|
||||||
|
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||||
|
int pickedSegId, int pickTag) {
|
||||||
|
return buildGraphicInput(gocaX, gocaY, row, col, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 56-byte Graphic Input Structured Field matching IBM Host On-Demand (HODInput.java).
|
||||||
|
*/
|
||||||
|
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
|
||||||
|
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||||
byte[] sf = new byte[MASK.length];
|
byte[] sf = new byte[MASK.length];
|
||||||
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||||
|
|
||||||
|
// Bytes 0-1: Structured Field Length (0x0034 = 52 decimal per IBM HODInput.java / GOCA architecture).
|
||||||
|
// Preserved from MASK; do not overwrite with MASK.length (56).
|
||||||
|
|
||||||
|
// Bytes 16-19: Device correlation class descriptor (0x23, 0x00, 0x23, 0x00)
|
||||||
|
// Note: Preserved from MASK per IBM Host On-Demand (HODInput.java); do not overwrite with row/col.
|
||||||
|
|
||||||
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
|
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
|
||||||
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
|
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
|
||||||
sf[25] = (byte) (gocaX & 0xFF);
|
sf[25] = (byte) (gocaX & 0xFF);
|
||||||
@@ -69,28 +79,29 @@ public class GraphicInputBuilder {
|
|||||||
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
|
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
|
||||||
sf[27] = (byte) (gocaY & 0xFF);
|
sf[27] = (byte) (gocaY & 0xFF);
|
||||||
|
|
||||||
if (pickedSegId != 0) {
|
|
||||||
// Byte 28-31: Picked Segment ID (4 bytes big-endian)
|
|
||||||
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
|
|
||||||
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
|
|
||||||
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
|
|
||||||
sf[31] = (byte) (pickedSegId & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMouseAction) {
|
if (isMouseAction) {
|
||||||
if (pickTag != 0) {
|
// Byte 28-31: Fixed mouse trigger class constant (0x00000004 per IBM HODInput.java)
|
||||||
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
|
sf[28] = 0x00;
|
||||||
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
|
sf[29] = 0x00;
|
||||||
sf[33] = (byte) (pickTag & 0xFF);
|
sf[30] = 0x00;
|
||||||
}
|
sf[31] = 0x04;
|
||||||
|
|
||||||
|
// Byte 32-33: Fixed mouse correlation class constant (0x0004 per IBM HODInput.java)
|
||||||
|
sf[32] = 0x00;
|
||||||
|
sf[33] = 0x04;
|
||||||
|
|
||||||
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
|
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
|
||||||
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
|
sf[35] = (byte) (buttonOrAidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
|
||||||
} else {
|
} else {
|
||||||
// Keyboard AID (Enter, PF keys)
|
// Keyboard AID (Enter, PF keys)
|
||||||
|
sf[28] = 0x00;
|
||||||
|
sf[29] = 0x00;
|
||||||
|
sf[30] = 0x00;
|
||||||
sf[31] = 0x07;
|
sf[31] = 0x07;
|
||||||
|
sf[32] = 0x00;
|
||||||
sf[33] = 0x07;
|
sf[33] = 0x07;
|
||||||
sf[34] = (byte) 0xFF;
|
sf[34] = (byte) 0xFF;
|
||||||
sf[35] = (byte) (aidCode & 0xFF);
|
sf[35] = (byte) (buttonOrAidCode & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sf;
|
return sf;
|
||||||
|
|||||||
@@ -36,16 +36,23 @@ public class GraphicsPlane {
|
|||||||
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
||||||
};
|
};
|
||||||
|
|
||||||
private int canvasWidth = 800;
|
// 3179G Presentation Space metrics: 9x16 cell pitch (720x384 for Model 2, 720x688 for Model 4)
|
||||||
private int canvasHeight = 600;
|
private int screenCols = 80;
|
||||||
|
private int screenRows = 24;
|
||||||
|
private int canvasWidth = 720;
|
||||||
|
private int canvasHeight = 384;
|
||||||
private int[] rgbBuffer;
|
private int[] rgbBuffer;
|
||||||
private boolean hasContent = false;
|
private boolean hasContent = false;
|
||||||
private long updateCount = 0;
|
private long updateCount = 0;
|
||||||
|
|
||||||
private int screenCols = 80;
|
|
||||||
private int screenRows = 24;
|
|
||||||
private ProgramSymbolManager programSymbolManager;
|
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) {
|
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||||
this.programSymbolManager = psm;
|
this.programSymbolManager = psm;
|
||||||
}
|
}
|
||||||
@@ -54,6 +61,65 @@ public class GraphicsPlane {
|
|||||||
return programSymbolManager;
|
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) {
|
public GraphicsPlane(int width, int height) {
|
||||||
this.canvasWidth = Math.max(1, width);
|
this.canvasWidth = Math.max(1, width);
|
||||||
this.canvasHeight = Math.max(1, height);
|
this.canvasHeight = Math.max(1, height);
|
||||||
@@ -115,11 +181,7 @@ public class GraphicsPlane {
|
|||||||
public synchronized void setScreenDimensions(int cols, int rows) {
|
public synchronized void setScreenDimensions(int cols, int rows) {
|
||||||
this.screenCols = cols > 0 ? cols : 80;
|
this.screenCols = cols > 0 ? cols : 80;
|
||||||
this.screenRows = rows > 0 ? rows : 24;
|
this.screenRows = rows > 0 ? rows : 24;
|
||||||
int targetW = this.screenCols * 9;
|
this.transform.setScreenDimensions(this.screenCols, this.screenRows);
|
||||||
int targetH = this.screenRows * 12;
|
|
||||||
if (this.canvasWidth != targetW || this.canvasHeight != targetH) {
|
|
||||||
resize(targetW, targetH);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getScreenCols() {
|
public int getScreenCols() {
|
||||||
@@ -130,29 +192,89 @@ public class GraphicsPlane {
|
|||||||
return screenRows;
|
return screenRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dedicated GDDM coordinate transformation scaling layer (matching IBM HODTransform)
|
||||||
|
private final GddmCoordinateTransform transform = new GddmCoordinateTransform();
|
||||||
|
|
||||||
|
public GddmCoordinateTransform getTransform() {
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalWidth() {
|
||||||
|
int cols = screenCols > 0 ? screenCols : 80;
|
||||||
|
return cols * 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total vertical presentation space units.
|
||||||
|
*
|
||||||
|
* <p>Historical / Architecture Note for Future Iterations:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Failed Approach 1 (Hardcoded 12-pitch / rows * 12)</b>:
|
||||||
|
* Used 516 units (yMax = 257) on 43-row screen. Caused upper coordinate truncation;
|
||||||
|
* menus at gy > 257 were clipped and mathematically impossible to pick.</li>
|
||||||
|
* <li><b>Failed Approach 2 (Hardcoded 16-pitch / rows * 16 without viewport alignment)</b>:
|
||||||
|
* Used 688 units (yMax = 343). Visual rendering was shifted downward by ~50 pixels relative
|
||||||
|
* to GDDM's internal picture space viewport, causing clicks at Row 7 to emit gy = 226
|
||||||
|
* which missed the GDDM menu hit box and caused terminal alarm beeps.</li>
|
||||||
|
* <li><b>Failed Approach 3 (Overwriting Graphic Input SF bytes 16-19 with row/col)</b>:
|
||||||
|
* Overwrote device correlation descriptor 0x00230023, causing GDDM Structured Field
|
||||||
|
* parser to reject inbound correlation packets as malformed.</li>
|
||||||
|
* <li><b>Failed Approach 4 (Misinterpreting Procedure Order 0x21 as Segment Redraw loop)</b>:
|
||||||
|
* Order 0x21 is HODCurrentDefaults (Set Current Defaults), not an executive redraw loop.
|
||||||
|
* Treating 0x21 as segment execution caused previous menu frames and stored segments
|
||||||
|
* to be repeatedly repainted over active screens, leaving behind visual ghost artifacts.</li>
|
||||||
|
* <li><b>Failed Approach 5 (Static yMax=343 scaling vs GDDM ADMDRAW Viewport bounds)</b>:
|
||||||
|
* In ADMDRAW, GDDM defines the graphics presentation space between y = -169 (canvas bottom)
|
||||||
|
* and y = +200 (top menu bar), with total height ~370 units. Mapping this into a static
|
||||||
|
* yMax = 343 (688 total height) renders the top menu at Row 9.2 (middle of the screen) with
|
||||||
|
* a large vertical void above it, and clicking the visual menu sends gy = 224 which misses
|
||||||
|
* the [187..200] menu bounding box. Furthermore, multiple dropdown menus (FILE, DRAW, TRANSFORM)
|
||||||
|
* overlapped simultaneously because segment retention and clearing was decoupled from GDDM's
|
||||||
|
* actual viewport state.</li>
|
||||||
|
* <li><b>Solution Path</b>: Use {@link GddmCoordinateTransform} for dynamic display and aspect
|
||||||
|
* scaling, ensuring host Query Reply metrics (SDH/AH) and GDDM Picture Space Window
|
||||||
|
* orders are synchronized with client-side mouse transformation.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public int getTotalHeight() {
|
||||||
|
int rows = screenRows > 0 ? screenRows : 24;
|
||||||
|
return rows * 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getXMax() {
|
||||||
|
int totalW = getTotalWidth();
|
||||||
|
return (totalW - 1) / 2 + (totalW - 1) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getYMax() {
|
||||||
|
int totalH = getTotalHeight();
|
||||||
|
return (totalH - 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
|
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
|
||||||
|
* IBM 3179G / HOD presentation space coordinate range is [-xMax .. +xMax] (width = cols * 9).
|
||||||
*/
|
*/
|
||||||
public double mapXDouble(double gocaX) {
|
public double mapXDouble(double gocaX) {
|
||||||
int nominalWidth = screenCols * 9;
|
int totalW = getTotalWidth();
|
||||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
int xMax = getXMax();
|
||||||
double nx = gocaX + xMax;
|
double nx = gocaX + xMax;
|
||||||
return (nx * canvasWidth) / (double) (nominalWidth > 0 ? nominalWidth : 1);
|
return (nx * canvasWidth) / (double) totalW;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
|
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
|
||||||
|
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 16).
|
||||||
*/
|
*/
|
||||||
public double mapYDouble(double gocaY) {
|
public double mapYDouble(double gocaY) {
|
||||||
int nominalHeight = screenRows * 12;
|
int totalH = getTotalHeight();
|
||||||
int yMax = (nominalHeight - 1) / 2;
|
int yMax = getYMax();
|
||||||
double ny = yMax - gocaY;
|
double ny = yMax - gocaY;
|
||||||
return (ny * canvasHeight) / (double) (nominalHeight > 0 ? nominalHeight : 1);
|
return (ny * canvasHeight) / (double) totalH;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
||||||
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
|
|
||||||
*/
|
*/
|
||||||
public int mapX(int gocaX) {
|
public int mapX(int gocaX) {
|
||||||
return (int) Math.round(mapXDouble((double) gocaX));
|
return (int) Math.round(mapXDouble((double) gocaX));
|
||||||
@@ -160,8 +282,6 @@ public class GraphicsPlane {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
||||||
* Coordinate space is symmetric: -yMax to +yMax, where nominalHeight = rows * 12 (e.g. 516 for 43 rows).
|
|
||||||
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
|
|
||||||
*/
|
*/
|
||||||
public int mapY(int gocaY) {
|
public int mapY(int gocaY) {
|
||||||
return (int) Math.round(mapYDouble((double) gocaY));
|
return (int) Math.round(mapYDouble((double) gocaY));
|
||||||
@@ -172,20 +292,20 @@ public class GraphicsPlane {
|
|||||||
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
|
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
|
||||||
*/
|
*/
|
||||||
public int unmapX(int px) {
|
public int unmapX(int px) {
|
||||||
int nominalWidth = screenCols * 9;
|
int totalW = getTotalWidth();
|
||||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
int xMax = getXMax();
|
||||||
int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1));
|
int nx = (int) Math.round((double) px * totalW / (canvasWidth > 0 ? canvasWidth : 1));
|
||||||
return nx - xMax;
|
return nx - xMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up).
|
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up, -yMax..+yMax).
|
||||||
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
|
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
|
||||||
*/
|
*/
|
||||||
public int unmapY(int py) {
|
public int unmapY(int py) {
|
||||||
int nominalHeight = screenRows * 12;
|
int totalH = getTotalHeight();
|
||||||
int yMax = (nominalHeight - 1) / 2;
|
int yMax = getYMax();
|
||||||
int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1));
|
int ny = (int) Math.round((double) py * totalH / (canvasHeight > 0 ? canvasHeight : 1));
|
||||||
return yMax - ny;
|
return yMax - ny;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,27 +584,12 @@ public class GraphicsPlane {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double prevX = px[0];
|
double[][] spline = FilletPts.calculate(px, py, numPoints, 30);
|
||||||
double prevY = py[0];
|
double[] sx = spline[0];
|
||||||
|
double[] sy = spline[1];
|
||||||
|
|
||||||
for (int i = 0; i < numPoints - 1; i++) {
|
for (int i = 0; i < sx.length - 1; i++) {
|
||||||
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
|
drawLine(sx[i], sy[i], sx[i + 1], sy[i + 1], colorArgb, lineType, lineWidth);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
updateCount++;
|
updateCount++;
|
||||||
@@ -501,6 +606,13 @@ public class GraphicsPlane {
|
|||||||
drawFillet(dpx, dpy, numPoints, colorArgb, lineType, lineWidth);
|
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.
|
* Fills a closed polygon area with a solid color or hatching pattern.
|
||||||
*/
|
*/
|
||||||
@@ -541,8 +653,19 @@ public class GraphicsPlane {
|
|||||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
int bg = bgColorArgb;
|
int bg = bgColorArgb;
|
||||||
|
|
||||||
boolean isTransparentBlack = (fill == GocaConstants.GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != GocaConstants.MIX_OVER);
|
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
|
||||||
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary) && !isTransparentBlack) {
|
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
|
||||||
|
// Background Mix (GSBMX / bgMix):
|
||||||
|
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
|
||||||
|
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
|
||||||
|
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
|
||||||
|
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
|
||||||
|
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
|
||||||
|
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
|
||||||
|
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||||
|
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
|
||||||
|
|
||||||
|
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||||
// Find polygon vertical bounds across all points
|
// Find polygon vertical bounds across all points
|
||||||
int minY = py[0];
|
int minY = py[0];
|
||||||
int maxY = py[0];
|
int maxY = py[0];
|
||||||
@@ -637,11 +760,6 @@ public class GraphicsPlane {
|
|||||||
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
||||||
boundaryColorArgb, lineType, lineWidth);
|
boundaryColorArgb, lineType, lineWidth);
|
||||||
}
|
}
|
||||||
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
|
|
||||||
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
|
|
||||||
(double) px[offset], (double) py[offset],
|
|
||||||
boundaryColorArgb, lineType, lineWidth);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
offset += pLen;
|
offset += pLen;
|
||||||
}
|
}
|
||||||
@@ -716,22 +834,7 @@ public class GraphicsPlane {
|
|||||||
drawMarker((double) x, (double) y, markerType, size, colorArgb);
|
drawMarker((double) x, (double) y, markerType, size, colorArgb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int[] VSS_OFFSETS = new int[256];
|
private static final int[] VSS_OFFSETS = VectorSymbolData.buildVssIndex();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface TextRenderer {
|
public interface TextRenderer {
|
||||||
@@ -777,10 +880,15 @@ public class GraphicsPlane {
|
|||||||
if (text == null || text.isEmpty()) return;
|
if (text == null || text.isEmpty()) return;
|
||||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
|
|
||||||
double curX = x;
|
|
||||||
double curY = y;
|
|
||||||
double cw = cellWidth > 0 ? cellWidth : 12.0;
|
double cw = cellWidth > 0 ? cellWidth : 12.0;
|
||||||
double ch = cellHeight > 0 ? cellHeight : 20.0;
|
double ch = cellHeight > 0 ? cellHeight : 20.0;
|
||||||
|
double curX = x;
|
||||||
|
double curY = y;
|
||||||
|
if (dir == GocaConstants.CD_TB) {
|
||||||
|
curY += ch;
|
||||||
|
} else if (dir == GocaConstants.CD_RL) {
|
||||||
|
curX -= cw;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
for (int i = 0; i < text.length(); i++) {
|
||||||
char c = text.charAt(i);
|
char c = text.charAt(i);
|
||||||
@@ -834,7 +942,7 @@ public class GraphicsPlane {
|
|||||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||||
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||||
py[p] = y + ((double) (VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch;
|
py[p] = y - ((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||||
ipx[p] = (int) Math.round(px[p]);
|
ipx[p] = (int) Math.round(px[p]);
|
||||||
ipy[p] = (int) Math.round(py[p]);
|
ipy[p] = (int) Math.round(py[p]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ public class ProgramSymbolManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProgramSymbolManager(int defaultWidth, int defaultHeight) {
|
||||||
|
this();
|
||||||
|
setDefaultCellDimensions(defaultWidth, defaultHeight);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets all symbol sets.
|
* 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.
|
* Commits all staged multi-plane symbol sets to the active presentation sets atomically.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ public class ProgramSymbolSet {
|
|||||||
return isTriplePlane;
|
return isTriplePlane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isLoaded() {
|
||||||
|
return pixelData != null && pixelData.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
||||||
* Enables unscaled 1:1 hardware blitting in Java2D.
|
* 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_WIDTH = 24;
|
||||||
public static final int MARKER_HEIGHT = 32;
|
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'};
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ public class InputProcessor {
|
|||||||
*/
|
*/
|
||||||
public void sendAid(int aidCode) {
|
public void sendAid(int aidCode) {
|
||||||
System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
|
System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
|
||||||
|
if (aidCode == AID_SYSREQ) {
|
||||||
|
sysReq();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (keyboardLocked && aidCode != AID_CLEAR) {
|
if (keyboardLocked && aidCode != AID_CLEAR) {
|
||||||
System.err.println("Keyboard locked, dropping AID");
|
System.err.println("Keyboard locked, dropping AID");
|
||||||
return;
|
return;
|
||||||
@@ -233,6 +237,86 @@ public class InputProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||||
|
int gx = gocaDecoder.getGraphicCursorX();
|
||||||
|
int gy = gocaDecoder.getGraphicCursorY();
|
||||||
|
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||||
|
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
|
||||||
|
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||||
|
int row = cursorAddr / cols;
|
||||||
|
int col = cursorAddr % cols;
|
||||||
|
|
||||||
|
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||||
|
gx, gy, row, col, aidCode, false, false, false
|
||||||
|
);
|
||||||
|
|
||||||
|
StringBuilder sfHex = new StringBuilder();
|
||||||
|
for (byte b : sf) {
|
||||||
|
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||||
|
}
|
||||||
|
log.info(String.format(
|
||||||
|
"sendAid (graphic): goca=(%d, %d) row=%d col=%d cursorAddr=%d aid=0x%02X SF_HEX=[%s]",
|
||||||
|
gx, gy, row, col, cursorAddr, aidCode, sfHex.toString().trim()
|
||||||
|
));
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||||
|
out.write(AID_SF);
|
||||||
|
try {
|
||||||
|
out.write(sf);
|
||||||
|
} catch (java.io.IOException ignored) {}
|
||||||
|
|
||||||
|
// Trailing AID + cursor address
|
||||||
|
out.write(aidCode);
|
||||||
|
byte[] caddr = encodeAddress(cursorAddr, numRows, cols);
|
||||||
|
out.write(caddr[0] & 0xFF);
|
||||||
|
out.write(caddr[1] & 0xFF);
|
||||||
|
|
||||||
|
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
||||||
|
sendAidResponse(out.toByteArray());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
|
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
||||||
|
int fieldStart = (i + 1) % size;
|
||||||
|
|
||||||
|
// Always send SBA and address of first character in field
|
||||||
|
out.write(ORDER_SBA);
|
||||||
|
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||||
|
out.write(addr[0] & 0xFF);
|
||||||
|
out.write(addr[1] & 0xFF);
|
||||||
|
|
||||||
|
// Send all non-null characters in field (suppressing 0x00)
|
||||||
|
int pos = fieldStart;
|
||||||
|
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||||
|
int b = screen.getCell(pos).ec & 0xFF;
|
||||||
|
if (b != 0x00) {
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
pos = (pos + 1) % size;
|
||||||
|
if (pos == fieldStart) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
int b = screen.getCell(i).ec & 0xFF;
|
||||||
|
if (b != 0x00) {
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAidResponse(out.toByteArray());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
||||||
// PA keys: send AID + optional PID + cursor address only (no modified data)
|
// PA keys: send AID + optional PID + cursor address only (no modified data)
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
@@ -244,7 +328,43 @@ public class InputProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
|
if (aidCode == AID_SELECT) {
|
||||||
|
// 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E):
|
||||||
|
// Per IBM 3270 Data Stream Architecture (GA23-0059) and IBM Host On-Demand (DS3270.sendAid lines 727-1019):
|
||||||
|
// The inbound data stream consists of:
|
||||||
|
// 1. AID byte (0x7E)
|
||||||
|
// 2. Cursor address (2 bytes)
|
||||||
|
// 3. For each field with MDT=1:
|
||||||
|
// - SBA order (0x11)
|
||||||
|
// - Designator character address (faAddr + 1)
|
||||||
|
// CRITICAL: NO FIELD CHARACTER DATA IS TRANSMITTED FOR AID_SELECT!
|
||||||
|
// Sending character data in an AID_SELECT stream violates 3270 protocol and causes the host to reject the selection.
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(AID_SELECT);
|
||||||
|
|
||||||
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
|
out.write(caddr[0] & 0xFF);
|
||||||
|
out.write(caddr[1] & 0xFF);
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
|
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
||||||
|
int designatorAddr = (i + 1) % size;
|
||||||
|
out.write(ORDER_SBA);
|
||||||
|
byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols());
|
||||||
|
out.write(addr[0] & 0xFF);
|
||||||
|
out.write(addr[1] & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAidResponse(out.toByteArray());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter, PF keys: send AID + optional PID + cursor address + modified field data
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|
||||||
out.write(aidCode);
|
out.write(aidCode);
|
||||||
@@ -298,7 +418,7 @@ public class InputProcessor {
|
|||||||
sendAidResponse(out.toByteArray());
|
sendAidResponse(out.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendAidResponse(byte[] data) {
|
protected void sendAidResponse(byte[] data) {
|
||||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||||
fsm.sendSscpLuData(data);
|
fsm.sendSscpLuData(data);
|
||||||
} else if (fsm != null) {
|
} else if (fsm != null) {
|
||||||
@@ -307,10 +427,10 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G.
|
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
|
||||||
*/
|
*/
|
||||||
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||||
if (fsm == null || !fsm.getConnectionState().isFullSession()) {
|
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isKeyboardLocked()) {
|
if (isKeyboardLocked()) {
|
||||||
@@ -322,19 +442,60 @@ public class InputProcessor {
|
|||||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||||
int gx = gocaDecoder.getGraphicCursorX();
|
int gx = gocaDecoder.getGraphicCursorX();
|
||||||
int gy = gocaDecoder.getGraphicCursorY();
|
int gy = gocaDecoder.getGraphicCursorY();
|
||||||
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
|
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||||
int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
|
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
|
||||||
|
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||||
|
int row = cursorAddr / cols;
|
||||||
|
int col = cursorAddr % cols;
|
||||||
|
|
||||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||||
gx, gy, aidCode, true, isShift, isCtrl, pickedSeg, pickTag
|
gx, gy, row, col, button, true, isShift, isCtrl
|
||||||
);
|
);
|
||||||
System.err.println(String.format(
|
|
||||||
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
|
StringBuilder sfHex = new StringBuilder();
|
||||||
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl
|
for (byte b : sf) {
|
||||||
|
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||||
|
}
|
||||||
|
log.info(String.format(
|
||||||
|
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
|
||||||
|
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||||
out.write(AID_SF);
|
out.write(AID_SF);
|
||||||
try {
|
try {
|
||||||
out.write(sf);
|
out.write(sf);
|
||||||
} catch (java.io.IOException ignored) {}
|
} catch (java.io.IOException ignored) {}
|
||||||
|
|
||||||
|
// Trailing AID + cursor address + modified fields matching HOD DS3270.sendMouseAid
|
||||||
|
out.write(aidCode);
|
||||||
|
byte[] caddr = encodeAddress(cursorAddr, numRows, cols);
|
||||||
|
out.write(caddr[0] & 0xFF);
|
||||||
|
out.write(caddr[1] & 0xFF);
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
|
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
||||||
|
int fieldStart = (i + 1) % size;
|
||||||
|
out.write(ORDER_SBA);
|
||||||
|
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||||
|
out.write(addr[0] & 0xFF);
|
||||||
|
out.write(addr[1] & 0xFF);
|
||||||
|
|
||||||
|
int pos = fieldStart;
|
||||||
|
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||||
|
int b = screen.getCell(pos).ec & 0xFF;
|
||||||
|
if (b != 0x00) {
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
pos = (pos + 1) % size;
|
||||||
|
if (pos == fieldStart) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
out.write(aidCode);
|
out.write(aidCode);
|
||||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
@@ -348,11 +509,11 @@ public class InputProcessor {
|
|||||||
// ========== Cursor movement ==========
|
// ========== Cursor movement ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulate a Text Light Pen selection.
|
* Simulate a Text Light Pen selection at the specified screen address.
|
||||||
|
* Conforms to IBM 3270 / Host On-Demand PS3270 processCurSelKey specifications.
|
||||||
*/
|
*/
|
||||||
public boolean lightPenSelect(int address) {
|
public boolean lightPenSelect(int address) {
|
||||||
if (screen == null || !screen.isFormatted()) {
|
if (screen == null || !screen.isFormatted()) {
|
||||||
System.out.println("LP: screen null or unformatted");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
@@ -360,15 +521,13 @@ public class InputProcessor {
|
|||||||
address = ((address % size) + size) % size;
|
address = ((address % size) + size) % size;
|
||||||
int faPos = screen.findFieldAttribute(address);
|
int faPos = screen.findFieldAttribute(address);
|
||||||
if (faPos < 0) {
|
if (faPos < 0) {
|
||||||
System.out.println("LP: no FA found for addr=" + address);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtendedAttribute faCell = screen.getCell(faPos);
|
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||||
int fa = faCell.fa & 0xFF;
|
int fa = faCell.fa & 0xFF;
|
||||||
System.out.println("LP: addr=" + address + " faPos=" + faPos + " fa=0x" + String.format("%02X", fa)
|
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) ||
|
||||||
+ " selectable=" + haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa));
|
haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||||
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,33 +536,52 @@ public class InputProcessor {
|
|||||||
int ebcdic = desCell.ec & 0xFF;
|
int ebcdic = desCell.ec & 0xFF;
|
||||||
char ascii = (char) desCell.ucs4;
|
char ascii = (char) desCell.ucs4;
|
||||||
|
|
||||||
screen.setCursorAddress(designatorPos);
|
// Set cursor to the clicked/selected position matching HOD / 3270 standards
|
||||||
|
screen.setCursorAddress(address);
|
||||||
|
|
||||||
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
|
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
|
||||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
|
||||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
|
||||||
return true;
|
|
||||||
} else if (ebcdic == 0x50 || ascii == '&') {
|
|
||||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
|
||||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
|
||||||
return true;
|
|
||||||
} else if (ebcdic == 0x6F || ascii == '?') {
|
|
||||||
desCell.ec = (byte) 0x6E;
|
|
||||||
desCell.ucs4 = '>';
|
|
||||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
|
||||||
screen.updateDisplaySnapshot();
|
|
||||||
return true;
|
|
||||||
} else if (ebcdic == 0x6E || ascii == '>') {
|
|
||||||
desCell.ec = (byte) 0x6F;
|
|
||||||
desCell.ucs4 = '?';
|
|
||||||
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
|
||||||
screen.updateDisplaySnapshot();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Enter Immediate (&) -> AID_ENTER (0x7D)
|
||||||
|
else if (ebcdic == 0x50 || ascii == '&' || ascii == '\uFF06') {
|
||||||
|
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
|
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Deferred Selection (? -> >)
|
||||||
|
else if (ebcdic == 0x6F || ascii == '?' || ascii == '\uFF1F') {
|
||||||
|
desCell.ec = (byte) 0x6E;
|
||||||
|
desCell.ucs4 = '>';
|
||||||
|
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Deferred Deselection (> -> ?)
|
||||||
|
else if (ebcdic == 0x6E || ascii == '>' || ascii == '\uFF1E') {
|
||||||
|
desCell.ec = (byte) 0x6F;
|
||||||
|
desCell.ucs4 = '?';
|
||||||
|
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Any other character is an invalid designator and cannot be selected
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate the 3270 CURSR SEL (Cursor Select) key at the current cursor position.
|
||||||
|
* Equivalent to processCurSelKey in IBM Host On-Demand PS3270.
|
||||||
|
*/
|
||||||
|
public boolean cursorSelect() {
|
||||||
|
if (screen == null) return false;
|
||||||
|
return lightPenSelect(screen.getCursorAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorUp() {
|
public void cursorUp() {
|
||||||
@@ -652,8 +830,12 @@ public class InputProcessor {
|
|||||||
|
|
||||||
/** SysReq key. */
|
/** SysReq key. */
|
||||||
public void sysReq() {
|
public void sysReq() {
|
||||||
|
if (fsm != null && fsm.isTn3270eNegotiated()) {
|
||||||
|
fsm.handleSysReq();
|
||||||
|
} else {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Reset (unlock keyboard, cancel insert mode). */
|
/** Reset (unlock keyboard, cancel insert mode). */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
@@ -760,13 +942,374 @@ public class InputProcessor {
|
|||||||
return fieldLen;
|
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.
|
* Send a structured field response directly.
|
||||||
* Used by DFT mode file transfer.
|
* Used by DFT mode file transfer.
|
||||||
*/
|
*/
|
||||||
public void sendStructuredFieldData(byte[] data) {
|
public void sendStructuredFieldData(byte[] data) {
|
||||||
|
if (fsm != null) {
|
||||||
fsm.send3270Data(data);
|
fsm.send3270Data(data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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. */
|
/** Get a reference to the TelnetFSM for direct SF operations. */
|
||||||
public TelnetFSM getTelnetFSM() {
|
public TelnetFSM getTelnetFSM() {
|
||||||
|
|||||||
@@ -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_EAU = 0x0f; // Erase All Unprotected
|
||||||
public static final int CMD_WSF = 0x11; // Write Structured Field
|
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
|
// SNA 3270 Commands
|
||||||
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
|
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
|
||||||
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
|
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
|
||||||
@@ -195,33 +201,47 @@ public final class DS3270Constants {
|
|||||||
public static final int SF_SRM_XFIELD = 0x01;
|
public static final int SF_SRM_XFIELD = 0x01;
|
||||||
public static final int SF_SRM_CHAR = 0x02;
|
public static final int SF_SRM_CHAR = 0x02;
|
||||||
public static final int SF_CREATE_PART = 0x0c;
|
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_OUTBOUND_DS = 0x40;
|
||||||
public static final int SF_TRANSFER_DATA = 0xd0;
|
public static final int SF_TRANSFER_DATA = 0xd0;
|
||||||
|
|
||||||
// ========== Query Reply codes ==========
|
// ========== Query Reply codes ==========
|
||||||
public static final int QR_SUMMARY = 0x80;
|
public static final int QR_SUMMARY = 0x80; // Summary
|
||||||
public static final int QR_USABLE_AREA = 0x81;
|
public static final int QR_USABLE_AREA = 0x81; // Usable Area
|
||||||
public static final int QR_IMAGE = 0x82;
|
public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
|
||||||
public static final int QR_TEXT_PART = 0x83;
|
public static final int QR_TEXT_PART = 0x83; // Text Partitions
|
||||||
public static final int QR_ALPHA_PART = 0x84;
|
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
|
||||||
public static final int QR_CHARSETS = 0x85;
|
public static final int QR_CHARSETS = 0x85; // Character Sets
|
||||||
public static final int QR_COLOR = 0x86;
|
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
|
||||||
public static final int QR_HIGHLIGHTING = 0x87;
|
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
|
||||||
public static final int QR_REPLY_MODES = 0x88;
|
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
|
||||||
public static final int QR_SAVE_RESTORE = 0x8c;
|
public static final int QR_OUTLINING = 0x8c; // Field Outlining
|
||||||
public static final int QR_DBCS_ASIA = 0x91;
|
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
|
||||||
public static final int QR_DDM = 0x95;
|
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
|
||||||
public static final int QR_TRANSPARENCY = 0x99;
|
public static final int QR_DDM = 0x95; // Distributed Data Management
|
||||||
public static final int QR_RPQNAMES = 0xa1;
|
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
|
||||||
public static final int QR_IMP_PART = 0xa6;
|
public static final int QR_FILE = 0x9f; // File Transfer
|
||||||
public static final int QR_RPQ_NAMES = 0xa8;
|
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
|
||||||
public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics
|
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
|
||||||
public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes
|
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
|
||||||
public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device
|
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
|
||||||
public static final int QR_OEM_FMT = 0xb3; // OEM Format
|
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
|
||||||
public static final int QR_GCOLOR = 0xb4; // Graphic Color Table
|
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
|
||||||
public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets
|
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
|
||||||
public static final int QR_NULL = 0xff;
|
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
|
||||||
|
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
|
||||||
|
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
|
||||||
|
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
|
||||||
|
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
|
||||||
|
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
|
||||||
|
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
|
||||||
|
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
|
||||||
|
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
|
||||||
|
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
|
||||||
|
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
|
||||||
|
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
|
||||||
|
|
||||||
// ========== Screen model sizes ==========
|
// ========== Screen model sizes ==========
|
||||||
public static final int MODEL_2_ROWS = 24;
|
public static final int MODEL_2_ROWS = 24;
|
||||||
@@ -367,19 +387,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) {
|
public static String commandName(int cmd) {
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case CMD_W: case SNA_CMD_W: return "Write";
|
case CMD_W: case SNA_CMD_W: return "Write";
|
||||||
case CMD_EW: case SNA_CMD_EW: return "EraseWrite";
|
case CMD_RB: case SNA_CMD_RB: return "Read Buffer";
|
||||||
case CMD_EWA: case SNA_CMD_EWA: return "EraseWriteAlternate";
|
case CMD_WSF: case SNA_CMD_WSF: return "Write Structured Field";
|
||||||
case CMD_RB: case SNA_CMD_RB: return "ReadBuffer";
|
case CMD_EW: case SNA_CMD_EW: return "Erase/Write";
|
||||||
case CMD_RM: case SNA_CMD_RM: return "ReadModified";
|
case CMD_RM: case SNA_CMD_RM: return "Read Modified";
|
||||||
case CMD_RMA: case SNA_CMD_RMA: return "ReadModifiedAll";
|
case CMD_RMA: case SNA_CMD_RMA: return "Read Modified All";
|
||||||
case CMD_EAU: case SNA_CMD_EAU: return "EraseAllUnprotected";
|
case CMD_EAU: case SNA_CMD_EAU: return "Erase All Unprotected";
|
||||||
case CMD_WSF: case SNA_CMD_WSF: return "WriteStructuredField";
|
case CMD_EWA: case SNA_CMD_EWA: return "Erase/Write Alternate";
|
||||||
case CMD_NOP: return "NoOp";
|
case CMD_NOP: return "NOP";
|
||||||
default: return String.format("Unknown(0x%02x)", cmd);
|
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_CLEANUP = 0x0f;
|
||||||
public static final int UNBIND_BAD_SENSE = 0xfe;
|
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
|
// Name lookups for tracing
|
||||||
|
|
||||||
private static final String[] REASON_NAMES = {
|
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] : "??";
|
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. */
|
/** Format a function set as a human-readable string. */
|
||||||
public static String functionNames(boolean[] funcs) {
|
public static String functionNames(boolean[] funcs) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ public class ExtendedAttribute {
|
|||||||
/** DBCS state. */
|
/** DBCS state. */
|
||||||
public byte db;
|
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).
|
* Unicode character for display (set by translation from ec, or directly in NVT mode).
|
||||||
*/
|
*/
|
||||||
@@ -48,6 +57,9 @@ public class ExtendedAttribute {
|
|||||||
cs = 0;
|
cs = 0;
|
||||||
ic = 0;
|
ic = 0;
|
||||||
db = 0;
|
db = 0;
|
||||||
|
ol = 0;
|
||||||
|
vl = 0;
|
||||||
|
tr = 0;
|
||||||
ucs4 = 0;
|
ucs4 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +73,9 @@ public class ExtendedAttribute {
|
|||||||
this.cs = other.cs;
|
this.cs = other.cs;
|
||||||
this.ic = other.ic;
|
this.ic = other.ic;
|
||||||
this.db = other.db;
|
this.db = other.db;
|
||||||
|
this.ol = other.ol;
|
||||||
|
this.vl = other.vl;
|
||||||
|
this.tr = other.tr;
|
||||||
this.ucs4 = other.ucs4;
|
this.ucs4 = other.ucs4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ public class ScreenBuffer {
|
|||||||
return renderLock;
|
return renderLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EbcdicTranslator getTranslator() {
|
||||||
|
return translator;
|
||||||
|
}
|
||||||
|
|
||||||
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
|
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
|
||||||
this.translator = translator;
|
this.translator = translator;
|
||||||
this.defRows = MODEL_2_ROWS;
|
this.defRows = MODEL_2_ROWS;
|
||||||
@@ -156,12 +160,80 @@ public class ScreenBuffer {
|
|||||||
updateDisplaySnapshot();
|
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 ==========
|
// ========== Cursor ==========
|
||||||
public int getCursorAddress() { return cursorAddress; }
|
public int getCursorAddress() { return cursorAddress; }
|
||||||
public synchronized void setCursorAddress(int addr) {
|
public synchronized void setCursorAddress(int addr) {
|
||||||
this.cursorAddress = addr;
|
this.cursorAddress = addr;
|
||||||
this.displayCursorAddress = addr;
|
this.displayCursorAddress = addr;
|
||||||
}
|
}
|
||||||
|
public synchronized void setCursorPosition(int row, int col) {
|
||||||
|
int r = Math.max(0, Math.min(row, rows - 1));
|
||||||
|
int c = Math.max(0, Math.min(col, cols - 1));
|
||||||
|
setCursorAddress(r * cols + c);
|
||||||
|
}
|
||||||
public int getCursorRow() { return cursorAddress / cols; }
|
public int getCursorRow() { return cursorAddress / cols; }
|
||||||
public int getCursorCol() { return cursorAddress % cols; }
|
public int getCursorCol() { return cursorAddress % cols; }
|
||||||
|
|
||||||
@@ -172,7 +244,7 @@ public class ScreenBuffer {
|
|||||||
public void setReplyMode(byte mode) { this.replyMode = mode; }
|
public void setReplyMode(byte mode) { this.replyMode = mode; }
|
||||||
|
|
||||||
public int getActivePartition() { return activePartition; }
|
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; }
|
public boolean isExplicitPartitionActive() { return explicitPartitionActive; }
|
||||||
|
|
||||||
// ========== Screen erase ==========
|
// ========== Screen erase ==========
|
||||||
|
|||||||
@@ -50,8 +50,11 @@ public class TelnetConnection {
|
|||||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||||
sslSocket.setKeepAlive(true);
|
sslSocket.setKeepAlive(config.isSoKeepAlive());
|
||||||
sslSocket.setTcpNoDelay(true);
|
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||||
|
if (config.getSoTimeoutMs() > 0) {
|
||||||
|
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||||
|
}
|
||||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||||
config.getConnectTimeoutMs());
|
config.getConnectTimeoutMs());
|
||||||
sslSocket.startHandshake();
|
sslSocket.startHandshake();
|
||||||
@@ -67,9 +70,12 @@ public class TelnetConnection {
|
|||||||
} else {
|
} else {
|
||||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||||
socket = new Socket();
|
socket = new Socket();
|
||||||
socket.setKeepAlive(true);
|
socket.setKeepAlive(config.isSoKeepAlive());
|
||||||
socket.setOOBInline(true);
|
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()),
|
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||||
config.getConnectTimeoutMs());
|
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.
|
* Disconnect from the host.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import haus.nightmare.lib3270j.listener.*;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -57,10 +58,15 @@ public class TelnetFSM {
|
|||||||
private boolean tn3270eBound;
|
private boolean tn3270eBound;
|
||||||
private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions
|
private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions
|
||||||
private int eXmitSeq;
|
private int eXmitSeq;
|
||||||
|
private int lastRcvSeq;
|
||||||
|
private short lastRespType;
|
||||||
|
private short lastRespCode;
|
||||||
private int responseRequired = RSF_NO_RESPONSE;
|
private int responseRequired = RSF_NO_RESPONSE;
|
||||||
private boolean deferredWillTtype;
|
private boolean deferredWillTtype;
|
||||||
private boolean tn3270eDeviceTypeSent;
|
private boolean tn3270eDeviceTypeSent;
|
||||||
private int ttypeIndex = 0;
|
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() {
|
private List<String> getCandidateTerminalTypes() {
|
||||||
List<String> list = new ArrayList<>();
|
List<String> list = new ArrayList<>();
|
||||||
@@ -68,6 +74,9 @@ public class TelnetFSM {
|
|||||||
list.add(config.getTerminalName().trim());
|
list.add(config.getTerminalName().trim());
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
if (config.isDynamicModel()) {
|
||||||
|
list.add(config.isExtendedDataStream() ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC");
|
||||||
|
}
|
||||||
TerminalModel model = config.getModel();
|
TerminalModel model = config.getModel();
|
||||||
list.add(model.getTerminalType());
|
list.add(model.getTerminalType());
|
||||||
list.add(model.getBaseTerminalType());
|
list.add(model.getBaseTerminalType());
|
||||||
@@ -92,6 +101,7 @@ public class TelnetFSM {
|
|||||||
private final ConnectionConfig config;
|
private final ConnectionConfig config;
|
||||||
private final ScreenBuffer screenBuffer;
|
private final ScreenBuffer screenBuffer;
|
||||||
private final DataStreamProcessor dsProcessor;
|
private final DataStreamProcessor dsProcessor;
|
||||||
|
private final haus.nightmare.lib3270j.nvt.NvtProcessor nvtProcessor;
|
||||||
private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
|
private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
@@ -108,6 +118,12 @@ public class TelnetFSM {
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this.screenBuffer = screenBuffer;
|
this.screenBuffer = screenBuffer;
|
||||||
this.dsProcessor = dsProcessor;
|
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) {
|
public void setConnection(TelnetConnection connection) {
|
||||||
@@ -115,7 +131,10 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
|
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 ConnectionState getConnectionState() { return connectionState; }
|
||||||
public boolean[] getMyOpts() { return myOpts; }
|
public boolean[] getMyOpts() { return myOpts; }
|
||||||
@@ -135,8 +154,10 @@ public class TelnetFSM {
|
|||||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||||
tn3270eBound = false;
|
tn3270eBound = false;
|
||||||
eXmitSeq = 0;
|
eXmitSeq = 0;
|
||||||
|
lastRcvSeq = 0;
|
||||||
deferredWillTtype = false;
|
deferredWillTtype = false;
|
||||||
ttypeIndex = 0;
|
ttypeIndex = 0;
|
||||||
|
luIndex = 0;
|
||||||
ibuf.reset();
|
ibuf.reset();
|
||||||
sbbuf.reset();
|
sbbuf.reset();
|
||||||
|
|
||||||
@@ -148,6 +169,7 @@ public class TelnetFSM {
|
|||||||
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
||||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
|
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
|
||||||
|
|
||||||
|
statusDisplay(STATUS_CONNECTING, "Connecting to host");
|
||||||
changeState(ConnectionState.TELNET_PENDING);
|
changeState(ConnectionState.TELNET_PENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +189,9 @@ public class TelnetFSM {
|
|||||||
if (connectionState == ConnectionState.TELNET_PENDING) {
|
if (connectionState == ConnectionState.TELNET_PENDING) {
|
||||||
changeState(ConnectionState.CONNECTED_NVT);
|
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);
|
ibuf.write(buf, start, i - start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,11 +260,15 @@ public class TelnetFSM {
|
|||||||
changeState(ConnectionState.CONNECTED_NVT);
|
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)
|
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
|
||||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||||
ibuf.write(c);
|
ibuf.write(c);
|
||||||
}
|
}
|
||||||
// NVT data would go to NVT processor (not implemented in initial version)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== TNS_IAC ==========
|
// ========== TNS_IAC ==========
|
||||||
@@ -374,11 +402,7 @@ public class TelnetFSM {
|
|||||||
} else if (!myOpts[opt]) {
|
} else if (!myOpts[opt]) {
|
||||||
myOpts[opt] = true;
|
myOpts[opt] = true;
|
||||||
sendCommand(WILL, opt);
|
sendCommand(WILL, opt);
|
||||||
// Start TN3270E sub-negotiation: send device type request
|
tn3270eDeviceTypeSent = false;
|
||||||
if (!tn3270eDeviceTypeSent) {
|
|
||||||
sendTN3270EDeviceTypeRequest();
|
|
||||||
tn3270eDeviceTypeSent = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -431,7 +455,9 @@ public class TelnetFSM {
|
|||||||
private void processSBIAC(int c) {
|
private void processSBIAC(int c) {
|
||||||
if (c == SE) {
|
if (c == SE) {
|
||||||
// Sub-negotiation complete
|
// Sub-negotiation complete
|
||||||
processSubNegotiation(sbbuf.toByteArray());
|
byte[] sbData = sbbuf.toByteArray();
|
||||||
|
sbbuf.reset();
|
||||||
|
processSubNegotiation(sbData);
|
||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
} else if (c == IAC) {
|
} else if (c == IAC) {
|
||||||
// Escaped IAC within sub-negotiation
|
// Escaped IAC within sub-negotiation
|
||||||
@@ -440,6 +466,7 @@ public class TelnetFSM {
|
|||||||
} else {
|
} else {
|
||||||
// Shouldn't happen, but recover
|
// Shouldn't happen, but recover
|
||||||
log.warning("Unexpected byte " + c + " after IAC in SB");
|
log.warning("Unexpected byte " + c + " after IAC in SB");
|
||||||
|
sbbuf.reset();
|
||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,7 +575,16 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void sendTN3270EDeviceTypeRequest() {
|
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();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
out.write(IAC);
|
out.write(IAC);
|
||||||
out.write(SB);
|
out.write(SB);
|
||||||
@@ -559,9 +595,9 @@ public class TelnetFSM {
|
|||||||
out.write((byte) ch);
|
out.write((byte) ch);
|
||||||
}
|
}
|
||||||
// Add LU name if specified
|
// Add LU name if specified
|
||||||
if (config.getLuName() != null && !config.getLuName().isEmpty()) {
|
if (currentLu != null && !currentLu.isEmpty()) {
|
||||||
out.write(OP_CONNECT);
|
out.write(OP_CONNECT);
|
||||||
for (char ch : config.getLuName().toCharArray()) {
|
for (char ch : currentLu.toCharArray()) {
|
||||||
out.write((byte) ch);
|
out.write((byte) ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -569,7 +605,7 @@ public class TelnetFSM {
|
|||||||
out.write(SE);
|
out.write(SE);
|
||||||
sendBytes(out.toByteArray());
|
sendBytes(out.toByteArray());
|
||||||
log.warning(">>> SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
|
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) {
|
private void handleTN3270EDeviceType(byte[] data) {
|
||||||
@@ -584,23 +620,37 @@ public class TelnetFSM {
|
|||||||
// Check if REJECT
|
// Check if REJECT
|
||||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
||||||
pos++;
|
pos++;
|
||||||
int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ;
|
int reason = REASON_UNSUPPORTED_REQ;
|
||||||
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
|
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
|
||||||
// Try fallback model 2 if we were requesting something else
|
pos++;
|
||||||
if (config.getModel() != TerminalModel.IBM_3278_2 &&
|
}
|
||||||
config.getModel() != TerminalModel.IBM_3279_4) {
|
if (pos < data.length) {
|
||||||
log.warning("TN3270E device-type rejected (" +
|
reason = data[pos] & 0xFF;
|
||||||
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
|
}
|
||||||
config.setModel(TerminalModel.IBM_3278_2);
|
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();
|
sendTN3270EDeviceTypeRequest();
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
|
|
||||||
|
|
||||||
// Fall back to plain TN3270
|
// Fall back to plain TN3270
|
||||||
myOpts[TELOPT_TN3270E] = false;
|
myOpts[TELOPT_TN3270E] = false;
|
||||||
hisOpts[TELOPT_TN3270E] = false;
|
hisOpts[TELOPT_TN3270E] = false;
|
||||||
|
sendCommand(WONT, TELOPT_TN3270E);
|
||||||
|
|
||||||
// Send deferred WILL TTYPE if needed
|
// Send deferred WILL TTYPE if needed
|
||||||
if (deferredWillTtype) {
|
if (deferredWillTtype) {
|
||||||
@@ -809,125 +859,85 @@ public class TelnetFSM {
|
|||||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true);
|
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true);
|
||||||
notifyScreenUpdate();
|
notifyScreenUpdate();
|
||||||
}
|
|
||||||
// Send positive response if required
|
// Send positive response if required
|
||||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||||
sendTN3270EPositiveResponse(seqNumber);
|
sendTN3270EPositiveResponse(seqNumber);
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "Error processing 3270 record", e);
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
|
||||||
|
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DT_SSCP_LU_DATA:
|
case DT_SSCP_LU_DATA:
|
||||||
|
if (connectionState != ConnectionState.CONNECTED_SSCP) {
|
||||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND) {
|
if (connectionState == ConnectionState.CONNECTED_UNBOUND) {
|
||||||
// Clear screen on first SSCP-LU transition to remove stale data
|
// Clear screen on first SSCP-LU transition to remove stale data
|
||||||
screenBuffer.clear();
|
screenBuffer.clear();
|
||||||
|
}
|
||||||
changeState(ConnectionState.CONNECTED_SSCP);
|
changeState(ConnectionState.CONNECTED_SSCP);
|
||||||
tn3270eSubmode = TN3270ESubmode.E_SSCP;
|
tn3270eSubmode = TN3270ESubmode.E_SSCP;
|
||||||
}
|
}
|
||||||
if (data.length > EH_SIZE) {
|
if (data.length > EH_SIZE) {
|
||||||
|
try {
|
||||||
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE);
|
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE);
|
||||||
notifyScreenUpdate();
|
notifyScreenUpdate();
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNumber);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "Error processing SSCP-LU record", e);
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
|
||||||
|
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNumber);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DT_BIND_IMAGE:
|
case DT_BIND_IMAGE:
|
||||||
tn3270eBound = true;
|
process_bind(data, responseFlag, seqNumber);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DT_UNBIND:
|
case DT_UNBIND:
|
||||||
log.info("Received UNBIND responseFlag=" + responseFlag);
|
int unbindReason = (data.length > EH_SIZE) ? (data[EH_SIZE] & 0xFF) : UNBIND_NORMAL;
|
||||||
tn3270eBound = false;
|
process_unbind(unbindReason, responseFlag, seqNumber);
|
||||||
// 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();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DT_NVT_DATA:
|
case DT_NVT_DATA:
|
||||||
// NVT data in TN3270E mode
|
// NVT data in TN3270E mode
|
||||||
changeState(ConnectionState.CONNECTED_E_NVT);
|
changeState(ConnectionState.CONNECTED_E_NVT);
|
||||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||||
|
if (data.length > EH_SIZE) {
|
||||||
|
try {
|
||||||
|
processNVTData(data, EH_SIZE, data.length - EH_SIZE);
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNumber);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "Error processing NVT record", e);
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
|
||||||
|
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DT_REQUEST:
|
case DT_REQUEST:
|
||||||
@@ -948,6 +958,7 @@ public class TelnetFSM {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case DT_RESPONSE:
|
case DT_RESPONSE:
|
||||||
|
lastRcvSeq = seqNumber;
|
||||||
log.fine("Received response, seq=" + seqNumber);
|
log.fine("Received response, seq=" + seqNumber);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1160,6 +1171,290 @@ public class TelnetFSM {
|
|||||||
public String getConnectedLu() { return connectedLu; }
|
public String getConnectedLu() { return connectedLu; }
|
||||||
public String getConnectedType() { return connectedType; }
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTn3270eBound() {
|
||||||
|
return tn3270eBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleSysReq() {
|
||||||
|
if (tn3270eNegotiated) {
|
||||||
|
byte[] ao = new byte[] { (byte) IAC, (byte) AO };
|
||||||
|
sendBytes(ao);
|
||||||
|
screenBuffer.clear();
|
||||||
|
screenBuffer.setCursorAddress(0);
|
||||||
|
screenBuffer.markAllChanged();
|
||||||
|
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
|
||||||
|
dsProcessor.getInputProcessor().reset();
|
||||||
|
}
|
||||||
|
if (connectionState != ConnectionState.CONNECTED_SSCP) {
|
||||||
|
changeState(ConnectionState.CONNECTED_SSCP);
|
||||||
|
tn3270eSubmode = TN3270ESubmode.E_SSCP;
|
||||||
|
} else if (tn3270eBound) {
|
||||||
|
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||||
|
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||||
|
}
|
||||||
|
notifyScreenUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String tn3270eOpName(int op) {
|
private static String tn3270eOpName(int op) {
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case OP_ASSOCIATE: return "ASSOCIATE";
|
case OP_ASSOCIATE: return "ASSOCIATE";
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ public class TlsTrustManager implements X509TrustManager {
|
|||||||
|
|
||||||
public TlsTrustManager(ConnectionConfig config) {
|
public TlsTrustManager(ConnectionConfig config) {
|
||||||
this.config = 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 {
|
try {
|
||||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
tmf.init((KeyStore) null);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+133
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
-33
@@ -22,10 +22,16 @@ public class QueryReplyBuilderTest {
|
|||||||
assertTrue(replies.length > 0);
|
assertTrue(replies.length > 0);
|
||||||
assertEquals((byte) AID_SF, replies[0]);
|
assertEquals((byte) AID_SF, replies[0]);
|
||||||
|
|
||||||
// In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present
|
// In base query reply (buildAllQueryReplies), Summary (0x80) advertises QR_SEGMENT (0xB0)
|
||||||
|
// while the base reply itself contains base SFs (Usable Area, Charsets, Color, Highlighting, Reply Modes, DDM, AuxDA, ImpPart)
|
||||||
|
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||||
|
assertEquals(QR_SUMMARY, replies[4] & 0xFF); // 0x80
|
||||||
|
|
||||||
|
// In buildCompleteQueryReplies, Vector Graphics QR 0xB0 structured field payload is present
|
||||||
|
byte[] completeReplies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
|
||||||
boolean hasB0 = false;
|
boolean hasB0 = false;
|
||||||
for (int i = 0; i < replies.length - 3; i++) {
|
for (int i = 0; i < completeReplies.length - 3; i++) {
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
if ((completeReplies[i] & 0xFF) == 0x81 && (completeReplies[i + 1] & 0xFF) == QR_SEGMENT) {
|
||||||
hasB0 = true;
|
hasB0 = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -41,31 +47,27 @@ public class QueryReplyBuilderTest {
|
|||||||
assertTrue(replies.length > 0);
|
assertTrue(replies.length > 0);
|
||||||
assertEquals((byte) AID_SF, replies[0]);
|
assertEquals((byte) AID_SF, replies[0]);
|
||||||
|
|
||||||
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be present
|
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary
|
||||||
boolean hasB0 = false;
|
int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||||
for (int i = 0; i < replies.length - 3; i++) {
|
for (int i = 5; i < 1 + sumLen; i++) {
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
assertNotEquals(QR_SEGMENT, replies[i] & 0xFF);
|
||||||
hasB0 = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertFalse(hasB0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBuildAllQueryRepliesWithVectorGraphics() {
|
public void testBuildAllQueryRepliesWithVectorGraphics() {
|
||||||
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
|
||||||
assertNotNull(replies);
|
assertNotNull(replies);
|
||||||
|
|
||||||
// Vector Graphics QR 0xB0 and 0xB4 must be present
|
// In complete replies, Vector Graphics QR 0xB0 and 0xB4 must be present
|
||||||
boolean hasB0 = false;
|
boolean hasB0 = false;
|
||||||
boolean hasB4 = false;
|
boolean hasB4 = false;
|
||||||
for (int i = 0; i < replies.length - 3; i++) {
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) {
|
||||||
hasB0 = true;
|
hasB0 = true;
|
||||||
}
|
}
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) {
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRCOLOR) {
|
||||||
hasB4 = true;
|
hasB4 = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,37 +78,39 @@ public class QueryReplyBuilderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testBuildAllQueryRepliesWithBoth() {
|
public void testBuildAllQueryRepliesWithBoth() {
|
||||||
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
|
||||||
assertNotNull(replies);
|
assertNotNull(replies);
|
||||||
|
|
||||||
// Vector Graphics (0xB0) must be present and Charsets must have LoadPS (0x0A)
|
// Vector Graphics (0xB0) must be present and Charsets must be present (0x85)
|
||||||
boolean hasB0 = false;
|
boolean hasB0 = false;
|
||||||
boolean hasCharsetsWithLoadPs = false;
|
boolean hasCharsets = false;
|
||||||
for (int i = 0; i < replies.length - 3; i++) {
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) {
|
||||||
hasB0 = true;
|
hasB0 = true;
|
||||||
}
|
}
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
if ((replies[i] & 0xFF) == 0x81 && (completeReplyIsCharsets(replies, i))) {
|
||||||
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
|
hasCharsets = true;
|
||||||
hasCharsetsWithLoadPs = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue(hasB0);
|
assertTrue(hasB0);
|
||||||
assertTrue(hasCharsetsWithLoadPs);
|
assertTrue(hasCharsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean completeReplyIsCharsets(byte[] replies, int i) {
|
||||||
|
return (replies[i + 1] & 0xFF) == QR_CHARSETS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBuildAllQueryRepliesWithProgrammedSymbols() {
|
public void testBuildAllQueryRepliesWithProgrammedSymbols() {
|
||||||
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
||||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
|
||||||
assertNotNull(replies);
|
assertNotNull(replies);
|
||||||
|
|
||||||
// Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS must NOT be present
|
// Charsets with LoadPS (0x0A) must be present, and QR_SEGMENT must NOT be present
|
||||||
boolean hasB0 = false;
|
boolean hasB0 = false;
|
||||||
boolean hasCharsetsWithLoadPs = false;
|
boolean hasCharsetsWithLoadPs = false;
|
||||||
for (int i = 0; i < replies.length - 3; i++) {
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) {
|
||||||
hasB0 = true;
|
hasB0 = true;
|
||||||
}
|
}
|
||||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||||
@@ -132,11 +136,15 @@ public class QueryReplyBuilderTest {
|
|||||||
assertEquals(0x81, replies[3] & 0xFF);
|
assertEquals(0x81, replies[3] & 0xFF);
|
||||||
assertEquals(QR_DDM, replies[4] & 0xFF);
|
assertEquals(QR_DDM, replies[4] & 0xFF);
|
||||||
|
|
||||||
// Second SF should be Usable Area
|
// Second SF should be Usable Area: 23 bytes (4 bytes header + 19 bytes payload)
|
||||||
int pos2 = 1 + len1;
|
int pos2 = 1 + len1;
|
||||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||||
|
assertEquals(23, len2);
|
||||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||||
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
||||||
|
// Offset 0x15 (21 in SF payload = pos2 + 21): Buffer size high
|
||||||
|
int bufSize = ((replies[pos2 + 21] & 0xFF) << 8) | (replies[pos2 + 22] & 0xFF);
|
||||||
|
assertEquals(80 * 43, bufSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -236,7 +244,7 @@ public class QueryReplyBuilderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testQueryReplyImageAndRpqNames() {
|
public void testQueryReplyImageAndRpqNames() {
|
||||||
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE };
|
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_TRANSPARENCY, (byte) QR_PROCEDURE };
|
||||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||||
|
|
||||||
assertNotNull(replies);
|
assertNotNull(replies);
|
||||||
@@ -247,15 +255,69 @@ public class QueryReplyBuilderTest {
|
|||||||
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||||
assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF
|
assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF
|
||||||
|
|
||||||
// Second SF should be QR_RPQNAMES (0xA1) matching requested code
|
// Second SF should be QR_TRANSPARENCY (0xA8) matching requested code
|
||||||
int pos2 = 1 + len1;
|
int pos2 = 1 + len1;
|
||||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||||
assertEquals(QR_RPQNAMES, replies[pos2 + 3] & 0xFF); // 0xA1
|
assertEquals(QR_TRANSPARENCY, replies[pos2 + 3] & 0xFF); // 0xA8
|
||||||
|
|
||||||
// Third SF should be QR_GIMAGE (0xB1)
|
// Third SF should be QR_PROCEDURE (0xB1)
|
||||||
int pos3 = pos2 + len2;
|
int pos3 = pos2 + len2;
|
||||||
assertEquals(0x81, replies[pos3 + 2] & 0xFF);
|
assertEquals(0x81, replies[pos3 + 2] & 0xFF);
|
||||||
assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1
|
assertEquals(QR_PROCEDURE, replies[pos3 + 3] & 0xFF); // 0xB1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUsableAreaMetricsMatchHOD() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||||
|
byte[] requested = new byte[] { (byte) QR_USABLE_AREA };
|
||||||
|
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||||
|
|
||||||
|
assertNotNull(replies);
|
||||||
|
assertEquals((byte) AID_SF, replies[0]);
|
||||||
|
|
||||||
|
// Length (2 bytes), SFID (0x81), QR_USABLE_AREA (0x81)
|
||||||
|
assertEquals(23, ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF));
|
||||||
|
assertEquals(0x81, replies[3] & 0xFF);
|
||||||
|
assertEquals(QR_USABLE_AREA, replies[4] & 0xFF);
|
||||||
|
|
||||||
|
// Flags: 12/14 bit addressing | Graphics (0x03), 0x00
|
||||||
|
assertEquals(0x03, replies[5] & 0xFF);
|
||||||
|
assertEquals(0x00, replies[6] & 0xFF);
|
||||||
|
|
||||||
|
// Usable width and height: 80, 43
|
||||||
|
assertEquals(80, ((replies[7] & 0xFF) << 8) | (replies[8] & 0xFF));
|
||||||
|
assertEquals(43, ((replies[9] & 0xFF) << 8) | (replies[10] & 0xFF));
|
||||||
|
|
||||||
|
// Units: 0x00 (Inches, matching IBM Host On-Demand DS3270.java)
|
||||||
|
assertEquals(0x00, replies[11] & 0xFF);
|
||||||
|
|
||||||
|
// Xr (4 bytes): 0x00010060 (96 dpi matching HOD)
|
||||||
|
int xr = ((replies[12] & 0xFF) << 24) | ((replies[13] & 0xFF) << 16) | ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF);
|
||||||
|
assertEquals(0x00010060, xr);
|
||||||
|
|
||||||
|
// Yr (4 bytes): 0x00010060 (96 dpi matching HOD)
|
||||||
|
int yr = ((replies[16] & 0xFF) << 24) | ((replies[17] & 0xFF) << 16) | ((replies[18] & 0xFF) << 8) | (replies[19] & 0xFF);
|
||||||
|
assertEquals(0x00010060, yr);
|
||||||
|
|
||||||
|
// AW and AH: 9 and 16
|
||||||
|
assertEquals(9, replies[20] & 0xFF);
|
||||||
|
assertEquals(16, replies[21] & 0xFF);
|
||||||
|
|
||||||
|
// Buffer size: 80 * 43 = 3440
|
||||||
|
assertEquals(80 * 43, ((replies[22] & 0xFF) << 8) | (replies[23] & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGrSymbolSetMatchesHOD() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||||
|
byte[] requested = new byte[] { (byte) QR_GRSYMBOLSET };
|
||||||
|
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||||
|
|
||||||
|
assertNotNull(replies);
|
||||||
|
assertEquals((byte) AID_SF, replies[0]);
|
||||||
|
assertEquals(33, ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF));
|
||||||
|
assertEquals(0x81, replies[3] & 0xFF);
|
||||||
|
assertEquals(QR_GRSYMBOLSET, replies[4] & 0xFF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
public class QueryReplyPhase2Test {
|
||||||
|
|
||||||
|
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||||
|
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||||
|
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to parse structured fields out of a query reply stream.
|
||||||
|
*/
|
||||||
|
private static class ParsedSF {
|
||||||
|
int length;
|
||||||
|
int sfid;
|
||||||
|
int qcode;
|
||||||
|
byte[] payload;
|
||||||
|
|
||||||
|
ParsedSF(int length, int sfid, int qcode, byte[] payload) {
|
||||||
|
this.length = length;
|
||||||
|
this.sfid = sfid;
|
||||||
|
this.qcode = qcode;
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ParsedSF> parseQueryReplies(byte[] data) {
|
||||||
|
List<ParsedSF> list = new ArrayList<>();
|
||||||
|
assertNotNull(data);
|
||||||
|
assertTrue(data.length > 0);
|
||||||
|
assertEquals((byte) AID_SF, data[0]);
|
||||||
|
|
||||||
|
int pos = 1;
|
||||||
|
while (pos < data.length) {
|
||||||
|
int len = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
|
||||||
|
assertTrue(len >= 4, "Structured field length must be >= 4 at pos " + pos);
|
||||||
|
int sfid = data[pos + 2] & 0xFF;
|
||||||
|
int qcode = data[pos + 3] & 0xFF;
|
||||||
|
byte[] payload = new byte[len - 4];
|
||||||
|
System.arraycopy(data, pos + 4, payload, 0, payload.length);
|
||||||
|
list.add(new ParsedSF(len, sfid, qcode, payload));
|
||||||
|
pos += len;
|
||||||
|
}
|
||||||
|
assertEquals(data.length, pos, "All bytes in query reply must be accounted for");
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQCodeConstantsValuesMatchHod() {
|
||||||
|
assertEquals(0x80, QR_SUMMARY);
|
||||||
|
assertEquals(0x81, QR_USABLE_AREA);
|
||||||
|
assertEquals(0x84, QR_ALPHA_PART);
|
||||||
|
assertEquals(0x85, QR_CHARSETS);
|
||||||
|
assertEquals(0x86, QR_COLOR);
|
||||||
|
assertEquals(0x87, QR_HIGHLIGHTING);
|
||||||
|
assertEquals(0x88, QR_REPLY_MODES);
|
||||||
|
assertEquals(0x8C, QR_OUTLINING);
|
||||||
|
assertEquals(0x91, QR_DBCS_ASIA);
|
||||||
|
assertEquals(0x95, QR_DDM);
|
||||||
|
assertEquals(0x99, QR_AUXDA);
|
||||||
|
assertEquals(0xA6, QR_IMP_PART);
|
||||||
|
assertEquals(0xA8, QR_TRANSPARENCY);
|
||||||
|
assertEquals(0xB0, QR_SEGMENT);
|
||||||
|
assertEquals(0xB1, QR_PROCEDURE);
|
||||||
|
assertEquals(0xB2, QR_LINETYPE);
|
||||||
|
assertEquals(0xB3, QR_PORT);
|
||||||
|
assertEquals(0xB4, QR_GRCOLOR);
|
||||||
|
assertEquals(0xB6, QR_GRSYMBOLSET);
|
||||||
|
assertEquals(0xFF, QR_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBaseGenericQueryReplySizeAndStructure() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
|
byte[] reply = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||||
|
assertNotNull(reply);
|
||||||
|
|
||||||
|
List<ParsedSF> sfs = parseQueryReplies(reply);
|
||||||
|
// Base query reply must contain exactly 10 structured fields for SBCS:
|
||||||
|
// 0x80 (Summary), 0x81 (Usable Area), 0x84 (Alpha Partitions), 0x85 (Character Sets),
|
||||||
|
// 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part)
|
||||||
|
assertEquals(10, sfs.size());
|
||||||
|
|
||||||
|
assertEquals(QR_SUMMARY, sfs.get(0).qcode);
|
||||||
|
assertEquals(QR_USABLE_AREA, sfs.get(1).qcode);
|
||||||
|
assertEquals(QR_ALPHA_PART, sfs.get(2).qcode);
|
||||||
|
assertEquals(QR_CHARSETS, sfs.get(3).qcode);
|
||||||
|
assertEquals(QR_COLOR, sfs.get(4).qcode);
|
||||||
|
assertEquals(QR_HIGHLIGHTING, sfs.get(5).qcode);
|
||||||
|
assertEquals(QR_REPLY_MODES, sfs.get(6).qcode);
|
||||||
|
assertEquals(QR_DDM, sfs.get(7).qcode);
|
||||||
|
assertEquals(QR_AUXDA, sfs.get(8).qcode);
|
||||||
|
assertEquals(QR_IMP_PART, sfs.get(9).qcode);
|
||||||
|
|
||||||
|
// Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply
|
||||||
|
for (ParsedSF sf : sfs) {
|
||||||
|
assertNotEquals(QR_TRANSPARENCY, sf.qcode);
|
||||||
|
assertNotEquals(QR_SEGMENT, sf.qcode);
|
||||||
|
assertNotEquals(QR_PROCEDURE, sf.qcode);
|
||||||
|
assertNotEquals(QR_LINETYPE, sf.qcode);
|
||||||
|
assertNotEquals(QR_PORT, sf.qcode);
|
||||||
|
assertNotEquals(QR_GRCOLOR, sf.qcode);
|
||||||
|
assertNotEquals(QR_GRSYMBOLSET, sf.qcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify summary structured field advertises full capabilities
|
||||||
|
byte[] summaryPayload = sfs.get(0).payload;
|
||||||
|
assertTrue(summaryPayload.length >= 17);
|
||||||
|
assertEquals(QR_SUMMARY, summaryPayload[0] & 0xFF);
|
||||||
|
assertEquals(QR_USABLE_AREA, summaryPayload[1] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTargetedQueryListAdmDrawResponse() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
|
// ADMDRAW vector graphics startup query list: 0x8C, 0xA8, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB6
|
||||||
|
byte[] requested = new byte[] {
|
||||||
|
(byte) QR_OUTLINING, // 0x8C
|
||||||
|
(byte) QR_TRANSPARENCY, // 0xA8
|
||||||
|
(byte) QR_SEGMENT, // 0xB0
|
||||||
|
(byte) QR_PROCEDURE, // 0xB1
|
||||||
|
(byte) QR_LINETYPE, // 0xB2
|
||||||
|
(byte) QR_PORT, // 0xB3
|
||||||
|
(byte) QR_GRCOLOR, // 0xB4
|
||||||
|
(byte) QR_GRSYMBOLSET // 0xB6
|
||||||
|
};
|
||||||
|
|
||||||
|
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||||
|
List<ParsedSF> sfs = parseQueryReplies(replies);
|
||||||
|
|
||||||
|
// 8 requested QCODEs produce 11 SFs because QR_PORT emits 4 OEM format sub-fields
|
||||||
|
assertEquals(11, sfs.size());
|
||||||
|
|
||||||
|
// 1. QR_OUTLINING (0x8C): 10 bytes
|
||||||
|
assertEquals(QR_OUTLINING, sfs.get(0).qcode);
|
||||||
|
assertEquals(10, sfs.get(0).length);
|
||||||
|
assertArrayEquals(new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, sfs.get(0).payload);
|
||||||
|
|
||||||
|
// 2. QR_TRANSPARENCY (0xA8): 9 bytes
|
||||||
|
assertEquals(QR_TRANSPARENCY, sfs.get(1).qcode);
|
||||||
|
assertEquals(9, sfs.get(1).length);
|
||||||
|
assertArrayEquals(new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }, sfs.get(1).payload);
|
||||||
|
|
||||||
|
// 3. QR_SEGMENT (0xB0): 11 bytes
|
||||||
|
assertEquals(QR_SEGMENT, sfs.get(2).qcode);
|
||||||
|
assertEquals(11, sfs.get(2).length);
|
||||||
|
assertArrayEquals(new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 }, sfs.get(2).payload);
|
||||||
|
|
||||||
|
// 4. QR_PROCEDURE (0xB1): 21 bytes
|
||||||
|
assertEquals(QR_PROCEDURE, sfs.get(3).qcode);
|
||||||
|
assertEquals(21, sfs.get(3).length);
|
||||||
|
assertArrayEquals(new byte[]{
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00, 0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||||
|
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||||
|
}, sfs.get(3).payload);
|
||||||
|
|
||||||
|
// 5. QR_LINETYPE (0xB2): 24 bytes
|
||||||
|
assertEquals(QR_LINETYPE, sfs.get(4).qcode);
|
||||||
|
assertEquals(24, sfs.get(4).length);
|
||||||
|
assertArrayEquals(new byte[]{
|
||||||
|
0x00, 0x09, 0x00, 0x07, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||||
|
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||||
|
}, sfs.get(4).payload);
|
||||||
|
|
||||||
|
// 6-9. QR_PORT (0xB3): 4 subfields (17, 15, 17, 15 bytes)
|
||||||
|
assertEquals(QR_PORT, sfs.get(5).qcode);
|
||||||
|
assertEquals(17, sfs.get(5).length);
|
||||||
|
assertEquals(QR_PORT, sfs.get(6).qcode);
|
||||||
|
assertEquals(15, sfs.get(6).length);
|
||||||
|
assertEquals(QR_PORT, sfs.get(7).qcode);
|
||||||
|
assertEquals(17, sfs.get(7).length);
|
||||||
|
assertEquals(QR_PORT, sfs.get(8).qcode);
|
||||||
|
assertEquals(15, sfs.get(8).length);
|
||||||
|
|
||||||
|
// 10. QR_GRCOLOR (0xB4): 109 bytes
|
||||||
|
assertEquals(QR_GRCOLOR, sfs.get(9).qcode);
|
||||||
|
assertEquals(109, sfs.get(9).length);
|
||||||
|
|
||||||
|
// 11. QR_GRSYMBOLSET (0xB6): 33 bytes
|
||||||
|
assertEquals(QR_GRSYMBOLSET, sfs.get(10).qcode);
|
||||||
|
assertEquals(33, sfs.get(10).length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnsupportedQCodesEmitQrNull() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
|
byte[] requested = new byte[] { (byte) 0x12, (byte) 0x7E, (byte) 0xEE };
|
||||||
|
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||||
|
|
||||||
|
List<ParsedSF> sfs = parseQueryReplies(replies);
|
||||||
|
assertEquals(3, sfs.size());
|
||||||
|
for (ParsedSF sf : sfs) {
|
||||||
|
assertEquals(QR_NULL, sf.qcode);
|
||||||
|
assertEquals(4, sf.length);
|
||||||
|
assertEquals(0, sf.payload.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDynamicCellDimensions() {
|
||||||
|
// In Vector Graphics mode, SDH must be 16 (0x10) for 3179G
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||||
|
assertEquals(9, qrBuilder.getCharWidth());
|
||||||
|
assertEquals(16, qrBuilder.getCharHeight());
|
||||||
|
|
||||||
|
// In Text-only mode (GraphicsMode.NONE), SDH must be 12 (0x0C) for 3279-2
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
|
||||||
|
assertEquals(9, qrBuilder.getCharWidth());
|
||||||
|
assertEquals(12, qrBuilder.getCharHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDataStreamProcessorQueryListDispatch() {
|
||||||
|
ByteArrayOutputStream captured = new ByteArrayOutputStream();
|
||||||
|
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
|
||||||
|
processor.setOutputSender(data -> {
|
||||||
|
try {
|
||||||
|
captured.write(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test SF_RP_QUERY (0x02): generic query -> base query reply
|
||||||
|
byte[] genericQuery = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x05, // Length = 5
|
||||||
|
(byte) SF_READ_PART, 0x00, // Partition = 0
|
||||||
|
(byte) SF_RP_QUERY // Type = 0x02
|
||||||
|
};
|
||||||
|
|
||||||
|
captured.reset();
|
||||||
|
processor.processRecord(genericQuery, 0, genericQuery.length, false);
|
||||||
|
byte[] sent = captured.toByteArray();
|
||||||
|
assertTrue(sent.length > 0);
|
||||||
|
assertEquals((byte) AID_SF, sent[0]);
|
||||||
|
List<ParsedSF> sfs = parseQueryReplies(sent);
|
||||||
|
assertEquals(10, sfs.size());
|
||||||
|
|
||||||
|
// Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4
|
||||||
|
byte[] queryList = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x08, // Length = 8
|
||||||
|
(byte) SF_READ_PART, 0x00,
|
||||||
|
(byte) SF_RP_QLIST,
|
||||||
|
(byte) SF_RPQ_LIST, // 0x00
|
||||||
|
(byte) QR_SEGMENT, // 0xB0
|
||||||
|
(byte) QR_GRCOLOR // 0xB4
|
||||||
|
};
|
||||||
|
|
||||||
|
captured.reset();
|
||||||
|
processor.processRecord(queryList, 0, queryList.length, false);
|
||||||
|
sent = captured.toByteArray();
|
||||||
|
assertTrue(sent.length > 0);
|
||||||
|
List<ParsedSF> listSfs = parseQueryReplies(sent);
|
||||||
|
assertEquals(2, listSfs.size());
|
||||||
|
assertEquals(QR_SEGMENT, listSfs.get(0).qcode);
|
||||||
|
assertEquals(QR_GRCOLOR, listSfs.get(1).qcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user