8 Commits

Author SHA1 Message Date
rudi 0201c7c277 Fix Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 50s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m24s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m38s
Release j3270 / Build & Publish Release (push) Successful in 1m38s
2026-09-24 17:34:43 +00:00
rudi b61702a57c Fix Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 52s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m42s
Release j3270 / Build & Publish Release (push) Failing after 1m24s
2026-09-24 17:03:38 +00:00
rudi bd4c1f3040 Add Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m19s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m24s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m43s
Release j3270 / Build & Publish Release (push) Successful in 39s
2026-09-24 15:35:08 +00:00
rudi 0d6ab31f0a Connection storage
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 50s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s
2026-09-24 12:44:50 +00:00
rudi 1f0e36c561 Connection hardening and FileTransfer UI changes
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m32s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 2m7s
2026-09-24 04:15:36 +00:00
rudi 8286af19e6 IND$FILE Browse!
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m27s
2026-09-24 01:28:55 +00:00
rudi 3d3a939ece Codepage, ECL, and some UI updates
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m29s
2026-09-22 13:38:12 +00:00
rudi d05f1a4c45 Resize debugging
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 39s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s
2026-09-21 16:26:13 +00:00
59 changed files with 6065 additions and 325 deletions
@@ -27,6 +27,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private static final Logger log = Logger.getLogger(J3270App.class.getName()); private static final Logger log = Logger.getLogger(J3270App.class.getName());
// Active open windows tracking
private static final java.util.List<J3270App> openWindows = new java.util.concurrent.CopyOnWriteArrayList<>();
public static java.util.List<J3270App> getOpenWindows() {
return java.util.Collections.unmodifiableList(openWindows);
}
@FunctionalInterface
public interface ConnectChoiceHandler {
ConnectChoiceDialog.Choice askChoice(Window owner, String currentTarget);
}
private ConnectChoiceHandler connectChoiceHandler = null;
public void setConnectChoiceHandler(ConnectChoiceHandler handler) {
this.connectChoiceHandler = handler;
}
public ConnectChoiceHandler getConnectChoiceHandler() {
return connectChoiceHandler;
}
private final java.util.function.Consumer<UITheme> themeChangeListener = this::onThemeChanged;
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
private StatusBar statusBar; private StatusBar statusBar;
@@ -60,14 +84,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
public J3270App() { public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setBackground(Color.BLACK); setBackground(Color.BLACK);
AppIcon.applyTo(this); AppIcon.applyTo(this);
openWindows.add(this);
buildUI(); buildUI();
buildMenuBar(); buildMenuBar();
ThemeManager.addThemeChangeListener(this::onThemeChanged); ThemeManager.addThemeChangeListener(themeChangeListener);
pack(); pack();
setLocationRelativeTo(null); setLocationRelativeTo(null);
@@ -78,7 +104,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (client != null) { if (client != null) {
statusBar.updateStatus(); statusBar.updateStatus();
syncModeMenuItems(); syncModeMenuItems();
if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) { if (!isModalDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
if (!terminalPanel.hasFocus()) { if (!terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
@@ -87,25 +113,27 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}); });
refreshTimer.start(); refreshTimer.start();
setAutoRequestFocus(true);
addWindowListener(new WindowAdapter() { addWindowListener(new WindowAdapter() {
@Override @Override
public void windowClosing(WindowEvent e) { public void windowClosing(WindowEvent e) {
disconnect(); closeWindow();
if (refreshTimer != null)
refreshTimer.stop();
if (printerSessionDialog != null) {
printerSessionDialog.dispose();
}
if (screenHistoryDialog != null) {
screenHistoryDialog.dispose();
}
terminalPanel.dispose();
} }
@Override @Override
public void windowActivated(WindowEvent e) { public void windowActivated(WindowEvent e) {
if (!isAnyChildDialogActive()) { if (!isModalDialogActive() && terminalPanel != null) {
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocus();
}
}
});
addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
if (!isModalDialogActive() && terminalPanel != null) {
terminalPanel.requestFocus();
} }
} }
}); });
@@ -117,16 +145,20 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
Object src = me.getSource(); Object src = me.getSource();
if (src instanceof Component) { if (src instanceof Component) {
Window win = SwingUtilities.getWindowAncestor((Component) src); Window win = SwingUtilities.getWindowAncestor((Component) src);
if (win == J3270App.this && !isAnyChildDialogActive()) { if (win == J3270App.this && !isModalDialogActive()) {
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) { if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
J3270App.this.toFront(); J3270App.this.toFront();
J3270App.this.requestFocus();
} }
boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu) boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu)
|| SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null || SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null
|| SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null; || SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null;
if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) { if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocus();
SwingUtilities.invokeLater(() -> {
if (terminalPanel != null && !terminalPanel.hasFocus()) {
terminalPanel.requestFocus();
}
});
} }
} }
} }
@@ -134,19 +166,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, AWTEvent.MOUSE_EVENT_MASK); }, AWTEvent.MOUSE_EVENT_MASK);
} }
private boolean isAnyChildDialogActive() { private boolean isModalDialogActive() {
Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
if (focusedWindow != null && focusedWindow != this) {
return true;
}
if (findDialog != null && findDialog.isVisible()) return true;
if (scriptDialog != null && scriptDialog.isVisible()) return true;
if (macroDialog != null && macroDialog.isVisible()) return true;
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
if (screenHistoryDialog != null && screenHistoryDialog.isVisible()) return true;
for (Window owned : getOwnedWindows()) { for (Window owned : getOwnedWindows()) {
if (owned != null && owned.isVisible()) { if (owned instanceof Dialog && owned.isVisible() && ((Dialog) owned).isModal()) {
return true; return true;
} }
} }
@@ -197,6 +219,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
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("Close Window", KeyEvent.VK_W, this::closeWindow));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel))); fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog)); fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog));
fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen)); fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen));
@@ -204,12 +228,49 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
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, () -> {
disconnect(); for (J3270App win : new java.util.ArrayList<>(openWindows)) {
win.disconnect();
win.dispose();
}
System.exit(0); System.exit(0);
})); }));
menuBar.add(fileMenu); menuBar.add(fileMenu);
// 2. Edit menu // 2. Connection menu
JMenu connectionMenu = createMenu("Connection");
connectionMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
connectionMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
connectionMenu.addSeparator();
// Host Type Submenu (CMS/TSO/CICS)
JMenu hostTypeMenu = createMenu("Host Type");
ButtonGroup hostTypeGroup = new ButtonGroup();
for (haus.nightmare.lib3270j.ft.FTConfig.HostType ht : haus.nightmare.lib3270j.ft.FTConfig.HostType.values()) {
JRadioButtonMenuItem htItem = new JRadioButtonMenuItem(ht.name());
ThemeManager.styleMenuItem(htItem);
hostTypeGroup.add(htItem);
htItem.addActionListener(e -> setConnectionHostType(ht));
hostTypeMenu.add(htItem);
}
connectionMenu.add(hostTypeMenu);
connectionMenu.addSeparator();
// Saved Connections Submenu
JMenu savedHostsMenu = createMenu("Saved Connections");
connectionMenu.add(savedHostsMenu);
connectionMenu.addMenuListener(new javax.swing.event.MenuListener() {
@Override
public void menuSelected(javax.swing.event.MenuEvent e) {
updateConnectionMenu(hostTypeMenu, hostTypeGroup, savedHostsMenu);
}
@Override public void menuDeselected(javax.swing.event.MenuEvent e) {}
@Override public void menuCanceled(javax.swing.event.MenuEvent e) {}
});
menuBar.add(connectionMenu);
// 3. Edit menu
JMenu editMenu = createMenu("Edit"); JMenu editMenu = createMenu("Edit");
editMenu.add(createMenuItem("Cut", KeyEvent.VK_X, () -> terminalPanel.cutSelection())); editMenu.add(createMenuItem("Cut", KeyEvent.VK_X, () -> terminalPanel.cutSelection()));
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection())); editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
@@ -738,7 +799,20 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// ========== Connection management ========== // ========== Connection management ==========
private void showConnectDialog() { public void showConnectDialog() {
boolean wasConnected = isConnected();
if (wasConnected && (!GraphicsEnvironment.isHeadless() || connectChoiceHandler != null)) {
ConnectChoiceDialog.Choice choice = promptConnectConflict();
if (choice == ConnectChoiceDialog.Choice.NEW_WINDOW) {
J3270App newWindow = createNewWindow(null);
SwingUtilities.invokeLater(newWindow::showConnectDialog);
return;
} else if (choice == ConnectChoiceDialog.Choice.CANCEL) {
return;
}
// choice == ABORT_CURRENT: proceed to showConnectDialog in this window
}
ConnectDialog dialog = new ConnectDialog(this); ConnectDialog dialog = new ConnectDialog(this);
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost(); String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort(); int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort();
@@ -748,13 +822,76 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
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.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
dialog.setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
dialog.setVisible(true); dialog.setVisible(true);
if (dialog.isConfirmed()) { if (dialog.isConfirmed()) {
ConnectionConfig config = dialog.getConnectionConfig(); ConnectionConfig config = dialog.getConnectionConfig();
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
connect(config); connect(config, !wasConnected);
}
}
public void setConnectionHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ht) {
if (ht == null) return;
haus.nightmare.j3270.config.Settings.setDefaultHostType(ht.name());
if (client != null && client.getConfig() != null) {
client.getConfig().setFtHostType(ht);
String profileId = client.getConfig().getSavedProfileId();
if (profileId != null) {
haus.nightmare.j3270.storage.SavedHost saved = haus.nightmare.j3270.storage.HostStorage.findById(profileId);
if (saved != null) {
saved.setHostType(ht.name());
haus.nightmare.j3270.storage.HostStorage.saveHost(saved);
}
}
}
if (FileTransferDialog.getLastTransferState() != null) {
FileTransferDialog.getLastTransferState().hostType = ht;
}
}
private void updateConnectionMenu(JMenu hostTypeMenu, ButtonGroup hostTypeGroup, JMenu savedHostsMenu) {
haus.nightmare.lib3270j.ft.FTConfig.HostType currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
if (client != null && client.getConfig() != null) {
currentHt = client.getConfig().getFtHostType();
} else {
String def = haus.nightmare.j3270.config.Settings.getDefaultHostType();
try {
if (def != null) currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(def.trim().toUpperCase());
} catch (Exception ignored) {}
}
for (int i = 0; i < hostTypeMenu.getItemCount(); i++) {
JMenuItem item = hostTypeMenu.getItem(i);
if (item instanceof JRadioButtonMenuItem) {
JRadioButtonMenuItem rb = (JRadioButtonMenuItem) item;
if (rb.getText().equalsIgnoreCase(currentHt.name())) {
rb.setSelected(true);
}
}
}
savedHostsMenu.removeAll();
java.util.List<haus.nightmare.j3270.storage.SavedHost> savedList = haus.nightmare.j3270.storage.HostStorage.getSavedHosts();
if (savedList.isEmpty()) {
JMenuItem noneItem = new JMenuItem("(No Saved Connections)");
noneItem.setEnabled(false);
ThemeManager.styleMenuItem(noneItem);
savedHostsMenu.add(noneItem);
} else {
for (haus.nightmare.j3270.storage.SavedHost sh : savedList) {
String label = sh.getName();
if (label == null || label.trim().isEmpty()) {
label = sh.getHost() + ":" + sh.getPort();
}
String ht = (sh.getHostType() != null && !sh.getHostType().trim().isEmpty()) ? sh.getHostType().trim() : "TSO";
JMenuItem hostItem = new JMenuItem(label + " [" + ht + "]");
ThemeManager.styleMenuItem(hostItem);
hostItem.addActionListener(ev -> connect(sh.toConnectionConfig(), true));
savedHostsMenu.add(hostItem);
}
} }
} }
@@ -763,6 +900,79 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setVisible(true); dialog.setVisible(true);
} }
public void closeWindow() {
disconnect();
if (refreshTimer != null) {
refreshTimer.stop();
}
if (printerSessionDialog != null) {
printerSessionDialog.dispose();
}
if (screenHistoryDialog != null) {
screenHistoryDialog.dispose();
}
terminalPanel.dispose();
ThemeManager.removeThemeChangeListener(themeChangeListener);
openWindows.remove(this);
dispose();
if (openWindows.isEmpty() && !GraphicsEnvironment.isHeadless()) {
System.exit(0);
}
}
@Override
public void dispose() {
openWindows.remove(this);
ThemeManager.removeThemeChangeListener(themeChangeListener);
super.dispose();
}
public boolean isConnected() {
return client != null && (client.isConnected() ||
(client.getConnectionState() != null && client.getConnectionState().isConnected()));
}
public String getConnectedTarget() {
if (client != null && client.getConfig() != null) {
String profileName = client.getConfig().getSavedProfileName();
String host = client.getConfig().getHost();
int port = client.getConfig().getPort();
if (profileName != null && !profileName.trim().isEmpty()) {
return profileName + " (" + host + ":" + port + ")";
}
if (host != null && !host.trim().isEmpty()) {
return host + ":" + port;
}
}
if (lastHost != null && !lastHost.trim().isEmpty()) {
return lastHost + ":" + lastPort;
}
return "the current host";
}
public J3270App createNewWindow(ConnectionConfig config) {
J3270App newApp = new J3270App();
try {
Point loc = getLocation();
newApp.setLocation(loc.x + 30, loc.y + 30);
} catch (Exception ignored) {}
newApp.setVisible(true);
if (config != null) {
newApp.connect(config, false);
}
return newApp;
}
public ConnectChoiceDialog.Choice promptConnectConflict() {
if (connectChoiceHandler != null) {
return connectChoiceHandler.askChoice(this, getConnectedTarget());
}
if (GraphicsEnvironment.isHeadless()) {
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
}
return ConnectChoiceDialog.showPrompt(this, getConnectedTarget());
}
public TerminalPanel getTerminalPanel() { public TerminalPanel getTerminalPanel() {
return terminalPanel; return terminalPanel;
} }
@@ -775,7 +985,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return statusBar; return statusBar;
} }
void connect(ConnectionConfig config) { public void connect(ConnectionConfig config) {
connect(config, true);
}
public void connect(ConnectionConfig config, boolean promptIfConnected) {
if (promptIfConnected && isConnected() && (!GraphicsEnvironment.isHeadless() || connectChoiceHandler != null)) {
ConnectChoiceDialog.Choice choice = promptConnectConflict();
if (choice == ConnectChoiceDialog.Choice.NEW_WINDOW) {
createNewWindow(config);
return;
} else if (choice == ConnectChoiceDialog.Choice.CANCEL) {
return;
}
// choice == ABORT_CURRENT: proceed to connect in this window
}
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
@@ -784,11 +1009,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
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()); haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
if (config.getHostType() != null) {
haus.nightmare.j3270.config.Settings.setDefaultHostType(config.getHostType());
}
if (FileTransferDialog.getLastTransferState() != null && config.isSavedConnection()) {
FileTransferDialog.getLastTransferState().hostType = config.getFtHostType();
}
if (config.getGraphicsMode() != null) { if (config.getGraphicsMode() != null) {
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode()); haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
} }
disconnect(); Telnet3270Client oldClient = client;
if (oldClient != null) {
if (fileTransfer != null) {
fileTransfer.cancel();
fileTransfer = null;
}
if (macroDialog != null) {
macroDialog.stopMacro();
}
oldClient.removeConnectionListener(this);
oldClient.removeScreenUpdateListener(this);
client = null;
}
if (config.getCertificateVerifier() == null) { if (config.getCertificateVerifier() == null) {
config.setCertificateVerifier((chain, authType, exception) -> config.setCertificateVerifier((chain, authType, exception) ->
@@ -806,10 +1049,27 @@ 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);
terminalPanel.guardedPack(); int targetRows = 24;
int targetCols = 80;
TerminalModel model = config.getModel();
if (model != null) {
if (model.isDynamic()) {
targetRows = config.getDynamicRows();
targetCols = config.getDynamicCols();
} else {
targetRows = model.getAlternateRows();
targetCols = model.getAlternateCols();
}
}
terminalPanel.packForModel(targetRows, targetCols);
new Thread(() -> { new Thread(() -> {
try { try {
if (oldClient != null) {
try {
oldClient.disconnect(1000);
} catch (Exception ignored) {}
}
client.connect(); client.connect();
} catch (IOException e) { } catch (IOException e) {
log.log(Level.WARNING, "Connection failed", e); log.log(Level.WARNING, "Connection failed", e);
@@ -824,7 +1084,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
private void disconnect() { public void disconnect() {
disconnect(true);
}
public void disconnect(boolean resetUi) {
if (client != null) { if (client != null) {
if (fileTransfer != null) { if (fileTransfer != null) {
fileTransfer.cancel(); fileTransfer.cancel();
@@ -835,23 +1099,28 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
Telnet3270Client c = client; Telnet3270Client c = client;
client = null; client = null;
c.removeConnectionListener(this);
c.removeScreenUpdateListener(this);
new Thread(() -> { new Thread(() -> {
try { try {
c.disconnect(); c.disconnect(1000);
} catch (Exception ignored) {} } catch (Exception ignored) {}
}, "Disconnect-Thread").start(); }, "Disconnect-Thread").start();
Runnable uiReset = () -> { if (resetUi) {
terminalPanel.setClient(null); Runnable uiReset = () -> {
statusBar.setClient(null, terminalPanel); terminalPanel.setClient(null);
syncModeMenuItems(); statusBar.setClient(null, terminalPanel);
terminalPanel.repaint(); syncModeMenuItems();
setTitle("j3270 — Java TN3270 Terminal Emulator"); terminalPanel.repaint();
}; setTitle("j3270 — Java TN3270 Terminal Emulator");
if (SwingUtilities.isEventDispatchThread()) { };
uiReset.run(); if (SwingUtilities.isEventDispatchThread()) {
} else { uiReset.run();
SwingUtilities.invokeLater(uiReset); } else {
SwingUtilities.invokeLater(uiReset);
}
} }
} }
} }
@@ -903,7 +1172,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ? String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ?
" [" + client.getTelnetFSM().getConnectedLu() + "]" : ""; " [" + client.getTelnetFSM().getConnectedLu() + "]" : "";
setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev); setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev);
terminalPanel.guardedPack(); terminalPanel.revalidate();
terminalPanel.repaint();
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
}); });
@@ -954,7 +1224,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
@Override @Override
public void onScreenSizeChanged(int rows, int cols) { public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
terminalPanel.guardedPack(); terminalPanel.revalidate();
terminalPanel.repaint();
statusBar.updateStatus(); statusBar.updateStatus();
}); });
} }
@@ -1182,6 +1453,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
String configFile = null; String configFile = null;
String cliProxyUrl = null; String cliProxyUrl = null;
String cliProfileName = null; String cliProfileName = null;
String cliHostType = 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++) {
@@ -1200,6 +1472,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliAutoSysUnlock = true; cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) { } else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false; cliAutoSysUnlock = false;
} else if (arg.startsWith("--host-type=")) {
cliHostType = arg.substring(12).trim().toUpperCase();
} else if (("--host-type".equals(arg) || "-ht".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliHostType = args[++i].trim().toUpperCase();
} else if (arg.startsWith("--graphics=")) { } else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11)); cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) { } else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -1254,6 +1530,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final GraphicsMode finalGraphicsMode = cliGraphicsMode; final GraphicsMode finalGraphicsMode = cliGraphicsMode;
final String finalProxyUrl = cliProxyUrl; final String finalProxyUrl = cliProxyUrl;
final String finalProfileName = cliProfileName; final String finalProfileName = cliProfileName;
final String finalHostType = cliHostType;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App(); J3270App app = new J3270App();
@@ -1266,6 +1543,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
if (profile != null) { if (profile != null) {
ConnectionConfig config = profile.toConnectionConfig(); ConnectionConfig config = profile.toConnectionConfig();
if (finalHostType != null) config.setHostType(finalHostType);
if (finalTls) config.setUseTls(true); if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false); if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e); if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
@@ -1322,6 +1600,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalProxyUrl != null) { if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl); applyProxyUrl(config, finalProxyUrl);
} }
if (finalHostType != null) {
config.setHostType(finalHostType);
} else {
config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
}
app.connect(config); app.connect(config);
} else { } else {
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
@@ -1334,6 +1617,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost(); haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
if (autoHost != null) { if (autoHost != null) {
ConnectionConfig config = autoHost.toConnectionConfig(); ConnectionConfig config = autoHost.toConnectionConfig();
if (finalHostType != null) config.setHostType(finalHostType);
if (finalTls) config.setUseTls(true); if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false); if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e); if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
@@ -1362,6 +1646,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalProxyUrl != null) { if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl); applyProxyUrl(config, finalProxyUrl);
} }
if (finalHostType != null) {
config.setHostType(finalHostType);
} else {
config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
}
app.connect(config); app.connect(config);
} else { } else {
SwingUtilities.invokeLater(app::showConnectDialog); SwingUtilities.invokeLater(app::showConnectDialog);
@@ -163,6 +163,15 @@ public class Settings {
return getAutoConnectAutoReconnect(); return getAutoConnectAutoReconnect();
} }
public static String getDefaultHostType() {
return prefs.get("defaultHostType", "TSO");
}
public static void setDefaultHostType(String hostType) {
prefs.put("defaultHostType", (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO");
flushPrefs();
}
public static void setAutoReconnect(boolean autoReconnect) { public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect); setAutoConnectAutoReconnect(autoReconnect);
} }
@@ -738,6 +747,11 @@ public class Settings {
case "dynamic_cols": case "dynamic_cols":
setDynamicCols(Integer.parseInt(value)); setDynamicCols(Integer.parseInt(value));
break; break;
case "hostType":
case "hosttype":
case "host_type":
setDefaultHostType(value);
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break; case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
case "autoReconnect": case "autoReconnect":
@@ -945,6 +959,7 @@ public class Settings {
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e()); w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
} }
w.println("codePage = " + getCodePage()); w.println("codePage = " + getCodePage());
w.println("hostType = " + getDefaultHostType());
w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols()); w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
@@ -5,6 +5,7 @@ import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager; import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants; import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
@@ -28,6 +29,8 @@ public class FileTransferDialog extends JDialog {
public String lrecl = ""; public String lrecl = "";
public String blksize = ""; public String blksize = "";
public String space = ""; public String space = "";
public FTConfig.AllocationUnit units = FTConfig.AllocationUnit.TRACKS;
public String dirBlocks = "";
public String options = ""; public String options = "";
} }
@@ -82,6 +85,8 @@ public class FileTransferDialog extends JDialog {
private JTextField lreclField; private JTextField lreclField;
private JTextField blksizeField; private JTextField blksizeField;
private JTextField spaceField; private JTextField spaceField;
private JComboBox<FTConfig.AllocationUnit> unitsCombo;
private JTextField dirField;
// VM specific // VM specific
private JTextField optionsField; private JTextField optionsField;
@@ -220,7 +225,7 @@ public class FileTransferDialog extends JDialog {
formPanel.add(createRow("File Options:", optionsPanel2)); formPanel.add(createRow("File Options:", optionsPanel2));
// Host-specific options panel (TSO dataset allocation) // Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5)); JPanel hostOptsPanel = new JPanel(new GridLayout(3, 4, 5, 5));
hostOptsPanel.setOpaque(false); hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)")); hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
@@ -236,10 +241,24 @@ public class FileTransferDialog extends JDialog {
blksizeField = new JTextField(5); blksizeField = new JTextField(5);
hostOptsPanel.add(blksizeField); hostOptsPanel.add(blksizeField);
hostOptsPanel.add(new JLabel("SPACE:")); hostOptsPanel.add(new JLabel("Units:"));
unitsCombo = new JComboBox<>(new FTConfig.AllocationUnit[]{
FTConfig.AllocationUnit.TRACKS,
FTConfig.AllocationUnit.CYLINDERS,
FTConfig.AllocationUnit.AVBLOCK,
FTConfig.AllocationUnit.DEFAULT
});
unitsCombo.setSelectedItem(FTConfig.AllocationUnit.TRACKS);
hostOptsPanel.add(unitsCombo);
hostOptsPanel.add(new JLabel("SPACE (Pri,Sec):"));
spaceField = new JTextField(15); spaceField = new JTextField(15);
hostOptsPanel.add(spaceField); hostOptsPanel.add(spaceField);
hostOptsPanel.add(new JLabel("Directory (PDS):"));
dirField = new JTextField(5);
hostOptsPanel.add(dirField);
formPanel.add(Box.createRigidArea(new Dimension(0, 10))); formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(hostOptsPanel); formPanel.add(hostOptsPanel);
@@ -306,9 +325,25 @@ public class FileTransferDialog extends JDialog {
if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm); if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm);
if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl); if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl);
if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize); if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize);
if (lastTransferState.units != null) unitsCombo.setSelectedItem(lastTransferState.units);
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space); if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
if (lastTransferState.dirBlocks != null) dirField.setText(lastTransferState.dirBlocks);
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options); if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
} }
// When using a saved connection (or when no previous transfer state exists for this connection),
// use the configured host type (CMS/TSO/CICS) for File Transfers
if (coordinator != null && coordinator.getClient() != null) {
haus.nightmare.lib3270j.ConnectionConfig cfg = coordinator.getClient().getConfig();
if (cfg != null) {
FTConfig.HostType configuredType = cfg.getFtHostType();
if (configuredType != null) {
if (cfg.isSavedConnection() || lastTransferState == null) {
hostTypeCombo.setSelectedItem(configuredType);
}
}
}
}
} }
private void updateOptionStates() { private void updateOptionStates() {
@@ -327,7 +362,9 @@ public class FileTransferDialog extends JDialog {
recfmField.setEnabled(isSend && isTso); recfmField.setEnabled(isSend && isTso);
lreclField.setEnabled(isSend && isTso); lreclField.setEnabled(isSend && isTso);
blksizeField.setEnabled(isSend && isTso); blksizeField.setEnabled(isSend && isTso);
unitsCombo.setEnabled(isSend && isTso);
spaceField.setEnabled(isSend && isTso); spaceField.setEnabled(isSend && isTso);
dirField.setEnabled(isSend && isTso);
optionsField.setEnabled(isCms); optionsField.setEnabled(isCms);
} }
@@ -359,11 +396,26 @@ public class FileTransferDialog extends JDialog {
private void browseHostDirectory() { private void browseHostDirectory() {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String initialQuery = hostFileField.getText().trim(); String initialQuery = hostFileField.getText().trim();
if (hostType == FTConfig.HostType.CMS && (initialQuery.isEmpty() || initialQuery.equals("* * *") || initialQuery.equals("*"))) {
initialQuery = "* * A";
}
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery); HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
dialog.setVisible(true); dialog.setVisible(true);
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) { if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
hostFileField.setText(dialog.getSelectedHostFile()); hostFileField.setText(dialog.getSelectedHostFile());
HostDirectoryEntry entry = dialog.getSelectedEntry();
if (entry != null) {
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
recfmField.setText(entry.getRecfm().trim());
}
if (entry.getLrecl() > 0) {
lreclField.setText(String.valueOf(entry.getLrecl()));
}
if (entry.getBlksize() > 0) {
blksizeField.setText(String.valueOf(entry.getBlksize()));
}
}
boolean shouldAutofill = localFileField.getText().trim().isEmpty() || boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile)); (lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
if (shouldAutofill) { if (shouldAutofill) {
@@ -410,7 +462,10 @@ public class FileTransferDialog extends JDialog {
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());
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
config.setSpace(spaceField.getText().trim()); config.setSpace(spaceField.getText().trim());
config.setDirectoryBlocks(dirField.getText().trim());
config.setOptions(optionsField.getText().trim()); config.setOptions(optionsField.getText().trim());
recordSessionTransfer(); recordSessionTransfer();
@@ -439,7 +494,9 @@ public class FileTransferDialog extends JDialog {
state.recfm = recfmField.getText().trim(); state.recfm = recfmField.getText().trim();
state.lrecl = lreclField.getText().trim(); state.lrecl = lreclField.getText().trim();
state.blksize = blksizeField.getText().trim(); state.blksize = blksizeField.getText().trim();
state.units = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
state.space = spaceField.getText().trim(); state.space = spaceField.getText().trim();
state.dirBlocks = dirField.getText().trim();
state.options = optionsField.getText().trim(); state.options = optionsField.getText().trim();
lastTransferState = state; lastTransferState = state;
} }
@@ -459,7 +516,9 @@ public class FileTransferDialog extends JDialog {
public JTextField getRecfmField() { return recfmField; } public JTextField getRecfmField() { return recfmField; }
public JTextField getLreclField() { return lreclField; } public JTextField getLreclField() { return lreclField; }
public JTextField getBlksizeField() { return blksizeField; } public JTextField getBlksizeField() { return blksizeField; }
public JComboBox<FTConfig.AllocationUnit> getUnitsCombo() { return unitsCombo; }
public JTextField getSpaceField() { return spaceField; } public JTextField getSpaceField() { return spaceField; }
public JTextField getDirField() { return dirField; }
public JTextField getOptionsField() { return optionsField; } public JTextField getOptionsField() { return optionsField; }
private void applyTheme(Container container) { private void applyTheme(Container container) {
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.storage; package haus.nightmare.j3270.storage;
import java.io.*;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -109,6 +110,267 @@ public class HostStorage {
} }
} }
// ================= INI File Export / Import =================
/**
* Export all currently saved host connections to an INI file.
*/
public static synchronized void exportToIniFile(File file) throws IOException {
List<SavedHost> hosts = getSavedHosts();
exportToIniFile(file, hosts);
}
public static synchronized void exportToIniFile(String path) throws IOException {
exportToIniFile(new File(path));
}
/**
* Export a specified list of host connections to an INI file.
*/
public static void exportToIniFile(File file, List<SavedHost> hosts) throws IOException {
if (file == null) {
throw new IllegalArgumentException("Target file cannot be null");
}
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
w.println("; j3270 Saved Connections Configuration File");
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
w.println("; Connections count: " + (hosts != null ? hosts.size() : 0));
w.println();
if (hosts != null) {
for (SavedHost h : hosts) {
String section = (h.getName() != null && !h.getName().trim().isEmpty())
? h.getName().trim()
: (h.getHost() != null && !h.getHost().trim().isEmpty() ? h.getHost().trim() : "Connection");
w.println("[" + section + "]");
if (h.getId() != null && !h.getId().trim().isEmpty()) {
w.println("id = " + h.getId());
}
w.println("name = " + (h.getName() != null ? h.getName() : ""));
w.println("host = " + (h.getHost() != null ? h.getHost() : ""));
w.println("port = " + h.getPort());
w.println("model = " + h.getModel());
w.println("dynamicRows = " + h.getDynamicRows());
w.println("dynamicCols = " + h.getDynamicCols());
w.println("luName = " + (h.getLuName() != null ? h.getLuName() : ""));
w.println("autoConnect = " + h.isAutoConnect());
w.println("hostType = " + (h.getHostType() != null ? h.getHostType() : "TSO"));
w.println("useTls = " + h.isUseTls());
w.println("tlsVerifyCert = " + h.isTlsVerifyCert());
w.println("tn3270e = " + h.isTn3270e());
w.println("graphicsMode = " + (h.getGraphicsMode() != null ? h.getGraphicsMode() : "BOTH"));
w.println("codePage = " + (h.getCodePage() != null ? h.getCodePage() : "037"));
w.println("proxyType = " + (h.getProxyType() != null ? h.getProxyType() : "NONE"));
w.println("proxyHost = " + (h.getProxyHost() != null ? h.getProxyHost() : ""));
w.println("proxyPort = " + h.getProxyPort());
w.println("proxyUsername = " + (h.getProxyUsername() != null ? h.getProxyUsername() : ""));
w.println("proxyPassword = " + (h.getProxyPassword() != null ? h.getProxyPassword() : ""));
w.println();
}
}
}
log.info("Saved connections exported to: " + file.getAbsolutePath());
}
/**
* Load connections from an INI file without saving them to persistent storage.
*/
public static List<SavedHost> loadFromIniFile(File file) throws IOException {
if (file == null || !file.exists()) {
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
}
List<SavedHost> hosts = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
SavedHost current = null;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
if (current != null) {
hosts.add(current);
}
String section = line.substring(1, line.length() - 1).trim();
if (section.toLowerCase().startsWith("connection:")) {
section = section.substring("connection:".length()).trim();
} else if (section.toLowerCase().startsWith("host:")) {
section = section.substring("host:".length()).trim();
}
current = new SavedHost();
current.setName(section);
continue;
}
int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
String key = line.substring(0, eq).trim();
String value = line.substring(eq + 1).trim();
if (current == null) {
current = new SavedHost();
current.setName("Default");
}
applyIniEntry(current, key, value);
}
if (current != null) {
hosts.add(current);
}
}
return hosts;
}
public static List<SavedHost> loadFromIniFile(String path) throws IOException {
return loadFromIniFile(new File(path));
}
/**
* Import connections from an INI file and merge them into persistent storage.
* Updates existing profiles matching by ID or Name, and appends new profiles.
*/
public static synchronized List<SavedHost> importFromIniFile(File file) throws IOException {
List<SavedHost> imported = loadFromIniFile(file);
if (imported == null || imported.isEmpty()) {
return getSavedHosts();
}
List<SavedHost> current = new ArrayList<>(getSavedHosts());
for (SavedHost host : imported) {
int index = -1;
for (int i = 0; i < current.size(); i++) {
SavedHost existing = current.get(i);
if ((host.getId() != null && !host.getId().trim().isEmpty() && Objects.equals(existing.getId(), host.getId())) ||
(host.getName() != null && !host.getName().trim().isEmpty() &&
host.getName().equalsIgnoreCase(existing.getName()))) {
index = i;
break;
}
}
if (host.isAutoConnect()) {
for (SavedHost c : current) {
c.setAutoConnect(false);
}
}
if (index >= 0) {
if (host.getId() == null || host.getId().trim().isEmpty()) {
host.setId(current.get(index).getId());
}
current.set(index, host);
} else {
current.add(host);
}
}
saveAll(current);
return current;
}
public static synchronized List<SavedHost> importFromIniFile(String path) throws IOException {
return importFromIniFile(new File(path));
}
private static void applyIniEntry(SavedHost h, String key, String value) {
String lowerKey = key.toLowerCase();
switch (lowerKey) {
case "id":
if (!value.isEmpty()) h.setId(value);
break;
case "name":
case "profilename":
case "profile":
if (!value.isEmpty()) h.setName(value);
break;
case "host":
case "hostname":
case "address":
h.setHost(value);
break;
case "port":
h.setPort(parseInt(value, 23));
break;
case "model":
case "terminalmodel":
h.setModel(parseInt(value, 4));
break;
case "dynamicrows":
case "dynamic_rows":
case "rows":
h.setDynamicRows(parseInt(value, 62));
break;
case "dynamiccols":
case "dynamic_cols":
case "cols":
h.setDynamicCols(parseInt(value, 160));
break;
case "luname":
case "lu_name":
case "lu":
h.setLuName(value);
break;
case "autoconnect":
case "auto_connect":
h.setAutoConnect(Boolean.parseBoolean(value));
break;
case "hosttype":
case "host_type":
case "type":
h.setHostType(value.toUpperCase());
break;
case "usetls":
case "use_tls":
case "tls":
case "ssl":
h.setUseTls(Boolean.parseBoolean(value));
break;
case "tlsverifycert":
case "verifycert":
case "tls_verify_cert":
h.setTlsVerifyCert(Boolean.parseBoolean(value));
break;
case "tn3270e":
case "enabletn3270e":
h.setTn3270e(Boolean.parseBoolean(value));
break;
case "graphicsmode":
case "graphics_mode":
case "graphics":
h.setGraphicsMode(value.toUpperCase());
break;
case "codepage":
case "code_page":
case "charset":
h.setCodePage(value);
break;
case "proxytype":
case "proxy_type":
h.setProxyType(value.toUpperCase());
break;
case "proxyhost":
case "proxy_host":
h.setProxyHost(value);
break;
case "proxyport":
case "proxy_port":
h.setProxyPort(parseInt(value, 0));
break;
case "proxyusername":
case "proxy_username":
case "proxyuser":
h.setProxyUsername(value);
break;
case "proxypassword":
case "proxy_password":
case "proxypass":
h.setProxyPassword(value);
break;
default:
break;
}
}
// ================= JSON Serialization ================= // ================= JSON Serialization =================
public static String serializeHostsJson(List<SavedHost> hosts) { public static String serializeHostsJson(List<SavedHost> hosts) {
@@ -108,6 +108,10 @@ public class SavedHost {
} }
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls); ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
config.setSavedConnection(true);
config.setSavedProfileId(this.id);
config.setSavedProfileName(this.name);
config.setHostType(this.hostType != null ? this.hostType : "TSO");
if (model == 0 || tm.isDynamic()) { if (model == 0 || tm.isDynamic()) {
config.setDynamicDimensions(dynamicRows, dynamicCols); config.setDynamicDimensions(dynamicRows, dynamicCols);
} }
@@ -0,0 +1,146 @@
package haus.nightmare.j3270.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.KeyEvent;
/**
* Dialogue shown when a connection is initiated while j3270 is already connected.
* Asks the user whether the connection should be opened in a new window or
* if the current connection should be aborted.
*/
public class ConnectChoiceDialog extends JDialog {
public enum Choice {
NEW_WINDOW,
ABORT_CURRENT,
CANCEL
}
private Choice result = Choice.CANCEL;
private final JButton newWindowBtn;
private final JButton abortBtn;
private final JButton cancelBtn;
private final JLabel titleLabel;
private final JLabel line1;
private final JLabel line2;
public ConnectChoiceDialog(Window owner, String currentTarget) {
super(owner, "Active Connection", ModalityType.APPLICATION_MODAL);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setResizable(false);
JPanel mainPanel = new JPanel(new BorderLayout(16, 16));
mainPanel.setBorder(new EmptyBorder(20, 24, 20, 24));
// Message Panel
JPanel messagePanel = new JPanel();
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
messagePanel.setOpaque(false);
titleLabel = new JLabel("Connection Already Active");
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 2));
messagePanel.add(titleLabel);
messagePanel.add(Box.createVerticalStrut(10));
String targetText = (currentTarget != null && !currentTarget.trim().isEmpty())
? currentTarget.trim()
: "the host";
line1 = new JLabel("j3270 is currently connected to " + targetText + ".");
line1.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
messagePanel.add(line1);
messagePanel.add(Box.createVerticalStrut(6));
line2 = new JLabel("Would you like to open the connection in a new window or abort the current connection?");
line2.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
messagePanel.add(line2);
mainPanel.add(messagePanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setOpaque(false);
cancelBtn = new JButton("Cancel");
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
cancelBtn.addActionListener(e -> {
result = Choice.CANCEL;
dispose();
});
abortBtn = new JButton("Abort Current Connection");
ThemeManager.styleButton(abortBtn, ThemeManager.ButtonVariant.DANGER);
abortBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
abortBtn.addActionListener(e -> {
result = Choice.ABORT_CURRENT;
dispose();
});
newWindowBtn = new JButton("New Window");
ThemeManager.styleButton(newWindowBtn, ThemeManager.ButtonVariant.PRIMARY);
newWindowBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
newWindowBtn.addActionListener(e -> {
result = Choice.NEW_WINDOW;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(abortBtn);
buttonPanel.add(newWindowBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// ESC key cancels
getRootPane().registerKeyboardAction(e -> {
result = Choice.CANCEL;
dispose();
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
getRootPane().setDefaultButton(newWindowBtn);
pack();
if (owner != null) {
setLocationRelativeTo(owner);
} else {
setLocationRelativeTo(null);
}
}
public Choice getChoice() {
return result;
}
public JButton getNewWindowButton() {
return newWindowBtn;
}
public JButton getAbortButton() {
return abortBtn;
}
public JButton getCancelButton() {
return cancelBtn;
}
public JLabel getTitleLabel() {
return titleLabel;
}
public JLabel getLine1() {
return line1;
}
public JLabel getLine2() {
return line2;
}
public static Choice showPrompt(Window owner, String currentTarget) {
ConnectChoiceDialog dialog = new ConnectChoiceDialog(owner, currentTarget);
dialog.setVisible(true);
return dialog.getChoice();
}
}
@@ -36,6 +36,7 @@ public class ConnectDialog extends JDialog {
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo; private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JComboBox<String> codePageCombo; private JComboBox<String> codePageCombo;
private JTextField luField; private JTextField luField;
private JComboBox<String> hostTypeCombo;
private JCheckBox tlsCheckBox; private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox; private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox; private JCheckBox tn3270eCheckBox;
@@ -214,6 +215,25 @@ public class ConnectDialog extends JDialog {
gridy++; gridy++;
// Host Type (CMS/TSO/CICS)
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel hostTypeLabel = new JLabel("Host Type:");
hostTypeLabel.setFont(labelFont);
contentPanel.add(hostTypeLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
String[] hostTypes = {"TSO", "CMS", "CICS"};
hostTypeCombo = new JComboBox<>(hostTypes);
hostTypeCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(hostTypeCombo);
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
contentPanel.add(hostTypeCombo, gbc);
gridy++;
// 6. Graphics Mode // 6. Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = gridy; gbc.gridy = gridy;
@@ -565,6 +585,7 @@ public class ConnectDialog extends JDialog {
} else { } else {
selectedProfileId = null; selectedProfileId = null;
deleteProfileBtn.setEnabled(false); deleteProfileBtn.setEnabled(false);
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
} }
} }
@@ -590,6 +611,7 @@ public class ConnectDialog extends JDialog {
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62); dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160); dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
luField.setText(h.getLuName() != null ? h.getLuName() : ""); luField.setText(h.getLuName() != null ? h.getLuName() : "");
setInitialHostType(h.getHostType() != null ? h.getHostType() : "TSO");
tlsCheckBox.setSelected(h.isUseTls()); tlsCheckBox.setSelected(h.isUseTls());
verifyCertCheckBox.setEnabled(h.isUseTls()); verifyCertCheckBox.setEnabled(h.isUseTls());
verifyCertCheckBox.setSelected(h.isTlsVerifyCert()); verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
@@ -677,6 +699,7 @@ public class ConnectDialog extends JDialog {
int dRows = (Integer) dynamicRowsSpinner.getValue(); int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue(); int dCols = (Integer) dynamicColsSpinner.getValue();
String lu = luField.getText().trim(); String lu = luField.getText().trim();
String hostType = getSelectedHostType();
boolean tls = tlsCheckBox.isSelected(); boolean tls = tlsCheckBox.isSelected();
boolean verify = verifyCertCheckBox.isSelected(); boolean verify = verifyCertCheckBox.isSelected();
boolean tn3270e = tn3270eCheckBox.isSelected(); boolean tn3270e = tn3270eCheckBox.isSelected();
@@ -705,7 +728,7 @@ public class ConnectDialog extends JDialog {
String pPass = new String(proxyPassField.getPassword()); String pPass = new String(proxyPassField.getPassword());
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false, return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass); hostType, tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
} }
// ================= Connect Action ================= // ================= Connect Action =================
@@ -797,10 +820,14 @@ public class ConnectDialog extends JDialog {
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass); result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
} }
result.setHostType(getSelectedHostType());
if (selectedProfileId != null) { if (selectedProfileId != null) {
result.setSavedConnection(true);
result.setSavedProfileId(selectedProfileId);
SavedHost existing = HostStorage.findById(selectedProfileId); SavedHost existing = HostStorage.findById(selectedProfileId);
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty()) String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
? existing.getName() : host; ? existing.getName() : host;
result.setSavedProfileName(pName);
SavedHost updated = buildCurrentSavedHost(pName); SavedHost updated = buildCurrentSavedHost(pName);
HostStorage.saveHost(updated); HostStorage.saveHost(updated);
} }
@@ -902,4 +929,44 @@ public class ConnectDialog extends JDialog {
public String getProxyPassword() { public String getProxyPassword() {
return new String(proxyPassField.getPassword()); return new String(proxyPassField.getPassword());
} }
public void setInitialHostType(String hostType) {
if (hostType != null && hostTypeCombo != null) {
for (int i = 0; i < hostTypeCombo.getItemCount(); i++) {
if (hostTypeCombo.getItemAt(i).equalsIgnoreCase(hostType.trim())) {
hostTypeCombo.setSelectedIndex(i);
return;
}
}
}
}
public void setInitialHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType hostType) {
if (hostType != null) {
setInitialHostType(hostType.name());
}
}
public String getSelectedHostType() {
if (hostTypeCombo != null && hostTypeCombo.getSelectedItem() != null) {
return hostTypeCombo.getSelectedItem().toString().trim().toUpperCase();
}
return "TSO";
}
public haus.nightmare.lib3270j.ft.FTConfig.HostType getSelectedFtHostType() {
try {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(getSelectedHostType());
} catch (Exception e) {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
}
}
public JComboBox<String> getHostTypeCombo() {
return hostTypeCombo;
}
public String getSelectedProfileId() {
return selectedProfileId;
}
} }
@@ -3,15 +3,19 @@ package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client; import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry; import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryParser;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry; import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry; import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader; import javax.swing.table.JTableHeader;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.ArrayList; import java.util.ArrayList;
@@ -19,6 +23,7 @@ import java.util.List;
/** /**
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments. * Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
* Aligned with IBM Host On-Demand presentation space queries and clipboard parsing.
*/ */
public class HostDirectoryDialog extends JDialog { public class HostDirectoryDialog extends JDialog {
@@ -27,13 +32,16 @@ public class HostDirectoryDialog extends JDialog {
private JComboBox<FTConfig.HostType> hostTypeCombo; private JComboBox<FTConfig.HostType> hostTypeCombo;
private JTextField queryField; private JTextField queryField;
private JButton queryHostBtn;
private JTable table; private JTable table;
private DefaultTableModel tableModel; private DefaultTableModel tableModel;
private JLabel statusLabel; private JLabel statusLabel;
private String selectedHostFile = null; private String selectedHostFile = null;
private HostDirectoryEntry selectedEntry = null;
private boolean confirmed = false; private boolean confirmed = false;
private final List<HostDirectoryEntry> allEntries = new ArrayList<>();
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>(); private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) { public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
@@ -43,7 +51,7 @@ public class HostDirectoryDialog extends JDialog {
AppIcon.applyTo(this); AppIcon.applyTo(this);
buildUI(initialQuery); buildUI(initialQuery);
setSize(700, 420); setSize(840, 480);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
@@ -69,33 +77,68 @@ public class HostDirectoryDialog extends JDialog {
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS); if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO); else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
ThemeManager.styleComboBox(hostTypeCombo); ThemeManager.styleComboBox(hostTypeCombo);
hostTypeCombo.addActionListener(e -> {
setupTableColumns();
FTConfig.HostType ht = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (ht == FTConfig.HostType.CMS) {
String cur = queryField.getText().trim();
if (cur.isEmpty() || cur.equals("* * *") || cur.equals("*")) {
queryField.setText("* * A");
}
} else if (ht == FTConfig.HostType.TSO) {
String cur = queryField.getText().trim();
if (cur.equals("* * A") || cur.equals("* * *")) {
queryField.setText("");
}
}
if (client != null && client.getConnectionState().isFullSession()) {
queryHost();
} else {
parseCurrentScreen();
}
});
topPanel.add(hostTypeCombo, gbc); topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2; gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:"); JLabel queryLabel = new JLabel("Filter / Pattern:");
queryLabel.setFont(font); queryLabel.setFont(font);
topPanel.add(queryLabel, gbc); topPanel.add(queryLabel, gbc);
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); String defaultQuery = resolveDefaultQuery(initialHostType, initialQuery);
queryField = new JTextField(defaultQuery, 16);
queryField.setFont(ThemeManager.getMonospacedUiFont()); queryField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(queryField); queryField.addActionListener(e -> onFilterOrParse());
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
gbc.gridx = 4; gbc.gridx = 4;
gbc.weightx = 0; gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse"); JButton filterBtn = new JButton("Filter / Parse");
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(filterBtn, ThemeManager.ButtonVariant.DEFAULT);
parseBtn.setFont(font); filterBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery()); filterBtn.addActionListener(e -> onFilterOrParse());
topPanel.add(parseBtn, gbc); topPanel.add(filterBtn, gbc);
gbc.gridx = 5;
queryHostBtn = new JButton("Query Host");
ThemeManager.styleButton(queryHostBtn, ThemeManager.ButtonVariant.DEFAULT);
queryHostBtn.setFont(font);
queryHostBtn.setToolTipText("Send LISTCAT or LISTFILE query to active host session");
queryHostBtn.addActionListener(e -> queryHost());
topPanel.add(queryHostBtn, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH); mainPanel.add(topPanel, BorderLayout.NORTH);
// Table // Table
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel() {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
table = new JTable(tableModel); table = new JTable(tableModel);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(ThemeManager.getMonospacedUiFont()); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@@ -114,26 +157,42 @@ public class HostDirectoryDialog extends JDialog {
}); });
JScrollPane scrollPane = new JScrollPane(table); JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
ThemeManager.styleScrollPane(scrollPane); ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom // Bottom
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5)); JPanel bottomPanel = new JPanel(new BorderLayout(5, 6));
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 4, 4, 4));
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(font); statusLabel.setFont(font);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.NORTH);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
btnPanel.setOpaque(false); btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen"); JButton inspectBtn = new JButton("Inspect (LISTDS)");
ThemeManager.styleButton(inspectBtn, ThemeManager.ButtonVariant.DEFAULT);
inspectBtn.setFont(font);
inspectBtn.setToolTipText("Retrieve VTOC attributes (RECFM, LRECL, BLKSIZE) for the selected dataset via TSO LISTDS");
inspectBtn.addActionListener(e -> inspectSelectedDataset());
JButton pasteScreenBtn = new JButton("Parse Screen");
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteScreenBtn.setFont(font); pasteScreenBtn.setFont(font);
pasteScreenBtn.setToolTipText("Parse items currently visible on the 3270 presentation space");
pasteScreenBtn.addActionListener(e -> parseCurrentScreen()); pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton pasteClipBtn = new JButton("Paste Clipboard");
ThemeManager.styleButton(pasteClipBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteClipBtn.setFont(font);
pasteClipBtn.setToolTipText("Parse host filenames copied to the system clipboard (HoD compatible)");
pasteClipBtn.addActionListener(e -> parseClipboard());
JButton selectBtn = new JButton("Select Dataset"); JButton selectBtn = new JButton("Select Dataset");
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY); ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
selectBtn.setFont(font); selectBtn.setFont(font);
@@ -147,23 +206,28 @@ public class HostDirectoryDialog extends JDialog {
dispose(); dispose();
}); });
btnPanel.add(inspectBtn);
btnPanel.add(pasteScreenBtn); btnPanel.add(pasteScreenBtn);
btnPanel.add(pasteClipBtn);
btnPanel.add(Box.createHorizontalStrut(10)); btnPanel.add(Box.createHorizontalStrut(10));
btnPanel.add(selectBtn); btnPanel.add(selectBtn);
btnPanel.add(cancelBtn); btnPanel.add(cancelBtn);
bottomPanel.add(btnPanel, BorderLayout.EAST); bottomPanel.add(btnPanel, BorderLayout.SOUTH);
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
// Initial setup // Initial setup: columns and screen parse or host query
setupTableColumns(); setupTableColumns();
if (initialQuery != null && !initialQuery.isEmpty()) { if (client != null && client.getConnectionState().isFullSession()) {
runQuery(); queryHost();
} else { } else {
parseCurrentScreen(); parseCurrentScreen();
if (!queryField.getText().trim().isEmpty()) {
applyFilter();
}
} }
} }
@@ -174,76 +238,665 @@ public class HostDirectoryDialog extends JDialog {
tableModel.setColumnIdentifiers(new Object[]{ tableModel.setColumnIdentifiers(new Object[]{
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time" "Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
}); });
int[] widths = {120, 100, 80, 70, 70, 90, 170};
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
}
} else { } else {
tableModel.setColumnIdentifiers(new Object[]{ tableModel.setColumnIdentifiers(new Object[]{
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents" "Dataset Name", "Volume"
}); });
} int[] widths = {620, 160};
} for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
private void runQuery() { if (i == 0) {
setupTableColumns(); table.getColumnModel().getColumn(0).setMinWidth(250);
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 onFilterOrParse() {
String query = queryField.getText().trim();
// If text contains multiple lines, treat it as pasted raw text to parse
if (query.contains("\n") || query.contains("\r") || query.toUpperCase().contains("NONVSAM")
|| query.toUpperCase().contains("IN-CAT") || query.toUpperCase().contains("FILENAME")) {
parseText(query, "query text");
} else if (allEntries.isEmpty() && client != null && client.getConnectionState().isFullSession()) {
queryHost();
} else {
applyFilter();
}
}
private void queryHost() {
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("No active session to query.");
return;
}
String pattern = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
statusLabel.setText("Querying host (" + hostType + ")...");
if (queryHostBtn != null) queryHostBtn.setEnabled(false);
SwingWorker<String, Void> worker = new SwingWorker<>() {
@Override
protected String doInBackground() throws Exception {
if (client == null || client.getPS() == null) return "";
StringBuilder allPages = new StringBuilder();
int maxPages = 500;
if (hostType == FTConfig.HostType.CMS) {
// CMS Query:
// 1. Clear presentation space first to ensure clean state
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
// 2. Format CMS LISTFILE query command (default to minidisk A unless specified)
String cmsFilter = formatCMSFilter(pattern);
client.getPS().SendKeys("LISTFILE " + cmsFilter + " ( nohead alloc date[enter]");
Thread.sleep(100);
// 3. Multi-page collection loop:
// CMS pagination displays MORE... or HOLDING when a screen is full.
// When advanced with [clear], CMS flashes RUNNING while generating the next screen.
// We poll up to 10 seconds per page for MORE... / HOLDING or Ready;
for (int page = 0; page < maxPages; page++) {
long startWait = System.currentTimeMillis();
long timeoutMs = 10000; // 10 seconds per page
String screen = "";
boolean isMore = false;
while (System.currentTimeMillis() - startWait < timeoutMs) {
waitForKeyboardUnlock(client, 500);
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
screen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
String upper = screen.toUpperCase();
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
isMore = true;
break;
}
if (upper.contains("READY;") || upper.contains("READY(")) {
break;
}
// Still RUNNING or transitioning, wait and re-check
Thread.sleep(150);
}
allPages.append(screen).append("\n");
if (isMore) {
// In CMS, sending [clear] advances to the next page of output
client.getPS().SendKeys("[clear]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 3000);
} else {
// Ready; encountered or timed out -> listing complete
break;
}
}
// Automatically clear the listing from the terminal presentation space
// so the terminal screen is left clean for the user while keeping the dialog populated
if (client.getPS() != null) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
}
} else {
// TSO Query:
// Check if current screen is in ISPF (formatted screen or containing ===> or ISPF)
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
boolean hasCmdArrow = initialScreen.contains("===>");
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
String tsoCmd = "TSO LISTCAT ALL";
if (!pattern.isEmpty()) {
String dsnPattern = pattern.trim();
if (dsnPattern.startsWith("'") && dsnPattern.endsWith("'") && dsnPattern.length() > 2) {
dsnPattern = dsnPattern.substring(1, dsnPattern.length() - 1);
}
int paren = dsnPattern.indexOf('(');
if (paren > 0) {
dsnPattern = dsnPattern.substring(0, paren);
}
if (dsnPattern.equals("*") || dsnPattern.equals("* * *") || dsnPattern.equals("*.*")) {
tsoCmd = "TSO LISTCAT ALL";
} else {
String level = dsnPattern;
if (level.endsWith(".*")) {
level = level.substring(0, level.length() - 2);
} else if (level.endsWith("*")) {
level = level.substring(0, level.length() - 1);
}
if (level.isEmpty()) {
tsoCmd = "TSO LISTCAT ALL";
} else {
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
}
}
}
if (isIspf) {
// In ISPF, do NOT send [clear] because [clear] destroys the ISPF panel.
// Locate the command/option line (indicated by ===>) or home to first input field.
int arrowPos = initialScreen.indexOf("===>");
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
if (inputAddr > 0) {
client.getScreenBuffer().setCursorAddress(inputAddr);
client.getScreenBuffer().updateDisplaySnapshot();
} else {
client.getPS().SendKeys("[home]");
}
} else {
client.getPS().SendKeys("[home]");
}
client.getPS().SendKeys("[erase_eof]");
client.getPS().SendKeys(tsoCmd + "[enter]");
} else {
// In native TSO (ISPF closed, line mode READY prompt)
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
client.getPS().SendKeys(tsoCmd + "[enter]");
}
Thread.sleep(100);
// Multi-page collection loop for TSO:
// 1. Wait for output to start appearing after sending command
long startCmd = System.currentTimeMillis();
while (System.currentTimeMillis() - startCmd < 4000) {
waitForKeyboardUnlock(client, 200);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
String u = s.toUpperCase();
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|| u.contains("NO ENTRY") || s.contains(" --- ")
|| u.contains("--RECFM-") || u.contains("--VOLUMES--")) {
break;
}
Thread.sleep(80);
}
// 2. Multi-page collection
String prevScreen = "";
for (int page = 0; page < maxPages; page++) {
waitForKeyboardUnlock(client, 300);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
String upper = screen.toUpperCase();
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
break;
}
prevScreen = screen;
allPages.append(screen).append("\n");
if (upper.contains("***")) {
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")
|| upper.contains("--RECFM-") || upper.contains("--VOLUMES--")) {
// If screen shows dataset data without ***, send [enter] once to see if *** or more output is triggered
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 1000);
int r2 = client.getPS().getRows();
int c2 = client.getPS().getCols();
String s2 = client.getPS().copyString(0, 0, r2 - 1, c2 - 1);
if (s2.toUpperCase().contains("***")) {
prevScreen = s2;
allPages.append(s2).append("\n");
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
continue;
} else {
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
allPages.append(s2).append("\n");
}
break;
}
} else {
break;
}
}
// 3. Terminal clean-up: clear the list from the terminal presentation space
if (client.getPS() != null) {
if (isIspf) {
client.getPS().SendKeys("[enter]");
Thread.sleep(150);
waitForKeyboardUnlock(client, 1500);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|| currentScr.contains(" --- ") || currentScr.toUpperCase().contains("--RECFM-")
|| (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
}
}
return allPages.toString();
}
@Override
protected void done() {
if (queryHostBtn != null) queryHostBtn.setEnabled(true);
try {
String screenText = get();
parseText(screenText, "host query");
if (!pattern.isEmpty() && !allEntries.isEmpty()) {
applyFilter();
}
} catch (Exception ex) {
statusLabel.setText("Host query failed: " + ex.getMessage());
}
}
};
worker.execute();
}
private void inspectSelectedDataset() {
int row = table.getSelectedRow();
if (row < 0 || row >= currentEntries.size()) {
JOptionPane.showMessageDialog(this, "Please select an entry from the table to inspect.",
"No Selection", JOptionPane.INFORMATION_MESSAGE);
return;
}
HostDirectoryEntry entry = currentEntries.get(row);
if (!(entry instanceof TSODirectoryEntry)) {
JOptionPane.showMessageDialog(this, "Inspection is only supported for TSO datasets.",
"Not Supported", JOptionPane.INFORMATION_MESSAGE);
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "No active full 3270 session to query host.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
String dsn = tso.getDatasetName();
if (dsn == null || dsn.trim().isEmpty()) {
return;
}
statusLabel.setText("Inspecting " + dsn + " via TSO LISTDS...");
SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
@Override
protected String doInBackground() throws Exception {
StringBuilder output = new StringBuilder();
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
boolean hasCmdArrow = initialScreen.contains("===>");
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
String tsoCmd = "TSO LISTDS '" + dsn + "' STATUS";
if (isIspf) {
int arrowPos = initialScreen.indexOf("===>");
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
if (inputAddr > 0) {
client.getScreenBuffer().setCursorAddress(inputAddr);
client.getScreenBuffer().updateDisplaySnapshot();
} else {
client.getPS().SendKeys("[home]");
}
} else {
client.getPS().SendKeys("[home]");
}
client.getPS().SendKeys("[erase_eof]");
client.getPS().SendKeys(tsoCmd + "[enter]");
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
client.getPS().SendKeys(tsoCmd + "[enter]");
}
Thread.sleep(100);
// Wait for output to start appearing
long startCmd = System.currentTimeMillis();
while (System.currentTimeMillis() - startCmd < 4000) {
waitForKeyboardUnlock(client, 200);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
String u = s.toUpperCase();
if (u.contains("--RECFM-") || u.contains("--VOLUMES--") || u.contains("***")
|| u.contains("READY") || u.contains("NOT IN CATALOG") || u.contains("NOT FOUND")) {
break;
}
Thread.sleep(80);
}
String prevScreen = "";
for (int page = 0; page < 5; page++) {
waitForKeyboardUnlock(client, 300);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
String upper = screen.toUpperCase();
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
break;
}
prevScreen = screen;
output.append(screen).append("\n");
if (upper.contains("***")) {
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
} else {
break;
}
}
// Terminal cleanup
if (client.getPS() != null) {
if (isIspf) {
client.getPS().SendKeys("[enter]");
Thread.sleep(150);
waitForKeyboardUnlock(client, 1500);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
if (currentScr.toUpperCase().contains("--RECFM-") || (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
}
return output.toString();
}
@Override
protected void done() {
try {
String outText = get();
List<TSODirectoryEntry> parsed = TSODirectoryParser.parse(outText);
if (!parsed.isEmpty()) {
TSODirectoryEntry updated = parsed.get(0);
if (updated.getRecfm() != null) tso.setRecfm(updated.getRecfm());
if (updated.getLrecl() > 0) tso.setLrecl(updated.getLrecl());
if (updated.getBlksize() > 0) tso.setBlksize(updated.getBlksize());
if (updated.getDsorg() != null) tso.setDsorg(updated.getDsorg());
if (updated.getVolume() != null && (tso.getVolume() == null || tso.getVolume().equals("******") || tso.getVolume().isEmpty())) {
tso.setVolume(updated.getVolume());
}
// Update table row in view
int modelRow = table.convertRowIndexToModel(row);
if (modelRow >= 0 && modelRow < tableModel.getRowCount()) {
tableModel.setValueAt(tso.getVolume() != null ? tso.getVolume() : "", modelRow, 1);
}
statusLabel.setText("Attributes for " + dsn + ": RECFM=" + tso.getRecfm() + ", LRECL=" + tso.getLrecl() + ", BLKSIZE=" + tso.getBlksize() + ", VOL=" + tso.getVolume());
StringBuilder msg = new StringBuilder();
msg.append("Dataset: ").append(dsn).append("\n\n");
msg.append("Volume: ").append(tso.getVolume() != null ? tso.getVolume() : "N/A").append("\n");
msg.append("Record Format (RECFM): ").append(tso.getRecfm() != null ? tso.getRecfm() : "N/A").append("\n");
msg.append("Logical Record Length (LRECL): ").append(tso.getLrecl() > 0 ? String.valueOf(tso.getLrecl()) : "N/A").append("\n");
msg.append("Block Size (BLKSIZE): ").append(tso.getBlksize() > 0 ? String.valueOf(tso.getBlksize()) : "N/A").append("\n");
msg.append("Dataset Organization (DSORG): ").append(tso.getDsorg() != null ? tso.getDsorg() : "N/A").append("\n");
if (tso.getTracksAllocated() > 0) {
msg.append("Allocated Tracks: ").append(tso.getTracksAllocated()).append("\n");
}
if (tso.getExtents() > 0) {
msg.append("Extents: ").append(tso.getExtents()).append("\n");
}
JOptionPane.showMessageDialog(HostDirectoryDialog.this, msg.toString(), "Dataset Attributes (LISTDS)", JOptionPane.INFORMATION_MESSAGE);
} else {
statusLabel.setText("No VTOC attributes found for " + dsn);
}
} catch (Exception ex) {
statusLabel.setText("Inspection failed: " + ex.getMessage());
}
}
};
worker.execute();
}
private static boolean waitForKeyboardUnlock(Telnet3270Client client, long timeoutMs) {
if (client == null) return false;
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
boolean locked = false;
if (client.getInputProcessor() != null && client.getInputProcessor().isKeyboardLocked()) {
locked = true;
}
if (client.getOIA() != null && (client.getOIA().isKeyLocked() || client.getOIA().isXSystem())) {
locked = true;
}
if (!locked) {
return true;
}
try {
Thread.sleep(40);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
} }
private void parseCurrentScreen() { private void parseCurrentScreen() {
if (client == null || !client.getConnectionState().isFullSession()) { if (client == null || !client.getConnectionState().isFullSession() || client.getPS() == null) {
statusLabel.setText("No active session to parse."); statusLabel.setText("No active session to parse.");
return; return;
} }
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String screenText = client.getPS().copyString(0, 0, rows - 1, cols - 1);
parseText(screenText, "presentation space");
}
private void parseClipboard() {
try {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable contents = clipboard.getContents(null);
if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
String clipText = (String) contents.getTransferData(DataFlavor.stringFlavor);
if (clipText != null && !clipText.trim().isEmpty()) {
parseText(clipText, "clipboard");
return;
}
}
statusLabel.setText("Clipboard contains no text.");
} catch (Exception ex) {
statusLabel.setText("Error reading clipboard: " + ex.getMessage());
}
}
private void parseText(String text, String source) {
setupTableColumns(); setupTableColumns();
currentEntries.clear(); allEntries.clear();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String screenText = client.getPS().getString(0, client.getPS().getSize());
if (hostType == FTConfig.HostType.CMS) { if (hostType == FTConfig.HostType.CMS) {
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText); List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(text);
currentEntries.addAll(cmsEntries); allEntries.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 { } else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText); List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(text);
currentEntries.addAll(tsoEntries); allEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) { }
applyFilter();
if (allEntries.isEmpty()) {
statusLabel.setText("0 entries found from " + source + ". Use 'Query Host' or 'Paste Clipboard'.");
} else {
statusLabel.setText("Parsed " + allEntries.size() + " entries from " + source + ".");
}
}
private void applyFilter() {
tableModel.setRowCount(0);
currentEntries.clear();
String filter = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
for (HostDirectoryEntry entry : allEntries) {
if (!matchesFilter(entry, filter)) {
continue;
}
currentEntries.add(entry);
if (hostType == FTConfig.HostType.CMS && entry instanceof CMSDirectoryEntry) {
CMSDirectoryEntry cms = (CMSDirectoryEntry) entry;
tableModel.addRow(new Object[]{ tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(), cms.getFilename() != null ? cms.getFilename() : "",
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents() cms.getFiletype() != null ? cms.getFiletype() : "",
cms.getFilemode() != null ? cms.getFilemode() : "",
cms.getRecfm() != null ? cms.getRecfm() : "",
cms.getLrecl() > 0 ? String.valueOf(cms.getLrecl()) : "",
cms.getNumRecords() > 0 ? String.valueOf(cms.getNumRecords()) : "",
(cms.getDate() != null ? cms.getDate() : "") + " " + (cms.getTime() != null ? cms.getTime() : "")
});
} else if (entry instanceof TSODirectoryEntry) {
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
tableModel.addRow(new Object[]{
tso.getDatasetName() != null ? tso.getDatasetName() : "",
tso.getVolume() != null ? tso.getVolume() : ""
}); });
} }
} }
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space."); if (!allEntries.isEmpty()) {
statusLabel.setText(currentEntries.size() + " entries shown (" + allEntries.size() + " total).");
}
}
public static boolean matchesFilter(HostDirectoryEntry entry, String filter) {
if (filter == null || filter.trim().isEmpty()) {
return true;
}
String cleanFilter = filter.replace("'", "").replace("\"", "").trim();
if (cleanFilter.isEmpty() || cleanFilter.equals("*") || cleanFilter.equals("* * *") || cleanFilter.equals("*.*")) {
return true;
}
if (entry instanceof CMSDirectoryEntry) {
return matchesCmsFilter((CMSDirectoryEntry) entry, cleanFilter);
} else if (entry instanceof TSODirectoryEntry) {
return matchesTsoFilter((TSODirectoryEntry) entry, cleanFilter);
} else {
return matchesWildcard(entry.getName(), cleanFilter);
}
}
public static boolean matchesCmsFilter(CMSDirectoryEntry cms, String filter) {
String fn = cms.getFilename() != null ? cms.getFilename().trim() : "";
String ft = cms.getFiletype() != null ? cms.getFiletype().trim() : "";
String fm = cms.getFilemode() != null ? cms.getFilemode().trim() : "";
String[] tokens = filter.split("\\s+");
if (tokens.length >= 3) {
String fnPat = tokens[0];
String ftPat = tokens[1];
String fmPat = tokens[2];
return matchesWildcard(fn, fnPat)
&& matchesWildcard(ft, ftPat)
&& matchesCmsFilemode(fm, fmPat);
} else if (tokens.length == 2) {
String fnPat = tokens[0];
String ftPat = tokens[1];
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
} else {
String tok = tokens[0];
if (tok.contains(".")) {
int dot = tok.indexOf('.');
String fnPat = tok.substring(0, dot);
String ftPat = tok.substring(dot + 1);
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
}
if (matchesWildcard(fn, tok) || matchesWildcard(ft, tok) || matchesCmsFilemode(fm, tok)) {
return true;
}
return matchesWildcard(cms.getName(), tok);
}
}
public static boolean matchesCmsFilemode(String actualFm, String fmPattern) {
if (fmPattern == null || fmPattern.isEmpty() || fmPattern.equals("*")) {
return true;
}
if (actualFm == null || actualFm.isEmpty()) {
return false;
}
if (fmPattern.length() == 1 && Character.isLetter(fmPattern.charAt(0))) {
return actualFm.toUpperCase().startsWith(fmPattern.toUpperCase());
}
return matchesWildcard(actualFm, fmPattern);
}
public static boolean matchesTsoFilter(TSODirectoryEntry tso, String filter) {
String dsn = tso.getDatasetName() != null ? tso.getDatasetName().trim() : "";
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
return true;
}
if (filter.contains("*") || filter.contains("?")) {
return matchesWildcard(dsn, filter);
}
return dsn.toUpperCase().contains(filter.toUpperCase());
}
public static boolean matchesWildcard(String text, String pattern) {
if (text == null) text = "";
if (pattern == null || pattern.isEmpty() || pattern.equals("*")) return true;
StringBuilder regex = new StringBuilder("^");
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '*') {
regex.append(".*");
} else if (c == '?') {
regex.append(".");
} else if ("\\.[]{}()+^$|".indexOf(c) >= 0) {
regex.append("\\").append(c);
} else {
regex.append(Character.toUpperCase(c));
}
}
regex.append("$");
return text.toUpperCase().matches(regex.toString());
} }
private void onConfirmSelection() { private void onConfirmSelection() {
@@ -256,6 +909,7 @@ public class HostDirectoryDialog extends JDialog {
HostDirectoryEntry entry = currentEntries.get(row); HostDirectoryEntry entry = currentEntries.get(row);
selectedHostFile = entry.getName(); selectedHostFile = entry.getName();
selectedEntry = entry;
confirmed = true; confirmed = true;
dispose(); dispose();
} }
@@ -267,4 +921,50 @@ public class HostDirectoryDialog extends JDialog {
public String getSelectedHostFile() { public String getSelectedHostFile() {
return selectedHostFile; return selectedHostFile;
} }
public HostDirectoryEntry getSelectedEntry() {
return selectedEntry;
}
public List<HostDirectoryEntry> getAllEntries() {
return allEntries;
}
public List<HostDirectoryEntry> getCurrentEntries() {
return currentEntries;
}
public JTextField getQueryField() {
return queryField;
}
public static String resolveDefaultQuery(FTConfig.HostType hostType, String initialQuery) {
String defaultQuery = initialQuery != null ? initialQuery.trim() : "";
if (hostType == FTConfig.HostType.CMS && (defaultQuery.isEmpty() || defaultQuery.equals("* * *") || defaultQuery.equals("*"))) {
defaultQuery = "* * A";
}
return defaultQuery;
}
public static String formatCMSFilter(String pattern) {
if (pattern == null || pattern.trim().isEmpty()) {
return "* * A";
}
String clean = pattern.replace("'", "").replace("\"", "").trim();
if (clean.equals("* * *") || clean.equals("*")) {
return "* * A";
} else if (clean.contains(".")) {
int dot = clean.indexOf('.');
return clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
} else {
String[] parts = clean.split("\\s+");
if (parts.length == 1) {
return parts[0] + " * A";
} else if (parts.length == 2) {
return parts[0] + " " + parts[1] + " A";
} else {
return parts[0] + " " + parts[1] + " " + parts[2];
}
}
}
} }
@@ -11,12 +11,16 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader; import javax.swing.table.JTableHeader;
import javax.swing.border.LineBorder; import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder; import javax.swing.border.TitledBorder;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
public class SettingsDialog extends JDialog { public class SettingsDialog extends JDialog {
@@ -58,8 +62,7 @@ public class SettingsDialog extends JDialog {
// Connection tab // Connection tab
private JComboBox<Settings.StartupBehavior> startupBehaviorBox; private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
private JPanel autoConnectPanel; private JPanel autoConnectPanel;
private JTextField hostField; private JComboBox<SavedHost> savedHostBox;
private JTextField portField;
private JCheckBox autoReconnectCheck; private JCheckBox autoReconnectCheck;
private JSpinner dynamicRowsSpinner; private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner; private JSpinner dynamicColsSpinner;
@@ -83,6 +86,8 @@ public class SettingsDialog extends JDialog {
// Bottom action buttons // Bottom action buttons
private JButton btnImport; private JButton btnImport;
private JButton btnExport; private JButton btnExport;
private JButton btnImportConnections;
private JButton btnExportConnections;
// Colors tab component tracking // Colors tab component tracking
private final JPanel[] hostColorSwatches = new JPanel[16]; private final JPanel[] hostColorSwatches = new JPanel[16];
@@ -94,7 +99,7 @@ public class SettingsDialog extends JDialog {
AppIcon.applyTo(this); AppIcon.applyTo(this);
initComponents(); initComponents();
setSize(660, 540); setSize(680, 650);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
@@ -107,13 +112,18 @@ public class SettingsDialog extends JDialog {
tabbedPane.addTab("Connection", createConnectionPanel()); tabbedPane.addTab("Connection", createConnectionPanel());
tabbedPane.addTab("Entry Assist", createEntryAssistPanel()); tabbedPane.addTab("Entry Assist", createEntryAssistPanel());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
btnImport = new JButton("Import Config..."); btnImport = new JButton("Import Config...");
ThemeManager.styleButton(btnImport, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btnImport, ThemeManager.ButtonVariant.DEFAULT);
btnExport = new JButton("Export Config..."); btnExport = new JButton("Export Config...");
ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
btnImportConnections = new JButton("Import Connections...");
ThemeManager.styleButton(btnImportConnections, ThemeManager.ButtonVariant.DEFAULT);
btnExportConnections = new JButton("Export Connections...");
ThemeManager.styleButton(btnExportConnections, ThemeManager.ButtonVariant.DEFAULT);
JButton btnApply = new JButton("Apply"); JButton btnApply = new JButton("Apply");
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
@@ -131,6 +141,14 @@ public class SettingsDialog extends JDialog {
exportConfig(); exportConfig();
}); });
btnImportConnections.addActionListener((ActionEvent e) -> {
importConnections();
});
btnExportConnections.addActionListener((ActionEvent e) -> {
exportConnections();
});
btnOk.addActionListener((ActionEvent e) -> { btnOk.addActionListener((ActionEvent e) -> {
boolean success = applySettings(); boolean success = applySettings();
if (success) { if (success) {
@@ -146,12 +164,28 @@ public class SettingsDialog extends JDialog {
dispose(); dispose();
}); });
buttonPanel.add(btnImport); JPanel buttonPanel = new JPanel(new GridLayout(3, 1, 0, 4));
buttonPanel.add(btnExport); buttonPanel.setBorder(BorderFactory.createEmptyBorder(4, 8, 8, 8));
buttonPanel.add(Box.createHorizontalStrut(20));
buttonPanel.add(btnApply); JPanel row1 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
buttonPanel.add(btnCancel); row1.setOpaque(false);
buttonPanel.add(btnOk); row1.add(btnImport);
row1.add(btnExport);
JPanel row2 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
row2.setOpaque(false);
row2.add(btnImportConnections);
row2.add(btnExportConnections);
JPanel row3 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
row3.setOpaque(false);
row3.add(btnApply);
row3.add(btnCancel);
row3.add(btnOk);
buttonPanel.add(row1);
buttonPanel.add(row2);
buttonPanel.add(row3);
getContentPane().add(tabbedPane, BorderLayout.CENTER); getContentPane().add(tabbedPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH); getContentPane().add(buttonPanel, BorderLayout.SOUTH);
@@ -588,22 +622,33 @@ public class SettingsDialog extends JDialog {
acGbc.insets = new Insets(5, 5, 5, 5); acGbc.insets = new Insets(5, 5, 5, 5);
acGbc.anchor = GridBagConstraints.WEST; acGbc.anchor = GridBagConstraints.WEST;
JLabel hostLabel = new JLabel("Host:"); JLabel savedConnLabel = new JLabel("Saved Connection:");
acGbc.gridx = 0; acGbc.gridx = 0;
acGbc.gridy = 0; acGbc.gridy = 0;
autoConnectPanel.add(hostLabel, acGbc); autoConnectPanel.add(savedConnLabel, acGbc);
hostField = new JTextField(Settings.getAutoConnectHost(), 15); savedHostBox = new JComboBox<>();
savedHostBox.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof SavedHost) {
SavedHost sh = (SavedHost) value;
if (sh.getName() != null && !sh.getName().trim().isEmpty()) {
setText(sh.getName() + " (" + sh.getHost() + ":" + sh.getPort() + ")");
} else {
setText(sh.getHost() + ":" + sh.getPort());
}
} else if (value == null) {
setText("(No saved connections)");
}
return this;
}
});
refreshSavedHostsBox();
acGbc.gridx = 1; acGbc.gridx = 1;
autoConnectPanel.add(hostField, acGbc); acGbc.fill = GridBagConstraints.HORIZONTAL;
autoConnectPanel.add(savedHostBox, acGbc);
JLabel portLabel = new JLabel("Port:");
acGbc.gridx = 2;
autoConnectPanel.add(portLabel, acGbc);
portField = new JTextField(String.valueOf(Settings.getAutoConnectPort()), 4);
acGbc.gridx = 3;
autoConnectPanel.add(portField, acGbc);
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = 1;
@@ -658,6 +703,30 @@ public class SettingsDialog extends JDialog {
return panel; return panel;
} }
private void refreshSavedHostsBox() {
if (savedHostBox == null) return;
savedHostBox.removeAllItems();
List<SavedHost> savedHosts = HostStorage.getSavedHosts();
SavedHost currentAutoHost = HostStorage.getAutoConnectHost();
SavedHost toSelect = null;
for (SavedHost sh : savedHosts) {
savedHostBox.addItem(sh);
if (currentAutoHost != null && Objects.equals(sh.getId(), currentAutoHost.getId())) {
toSelect = sh;
}
}
if (toSelect != null) {
savedHostBox.setSelectedItem(toSelect);
savedHostBox.setEnabled(true);
} else if (!savedHosts.isEmpty()) {
savedHostBox.setSelectedIndex(0);
savedHostBox.setEnabled(true);
} else {
savedHostBox.addItem(null);
savedHostBox.setEnabled(false);
}
}
private JPanel createEntryAssistPanel() { private JPanel createEntryAssistPanel() {
JPanel panel = new JPanel(); JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
@@ -1187,11 +1256,32 @@ public class SettingsDialog extends JDialog {
if (behavior != null) { if (behavior != null) {
Settings.setStartupBehavior(behavior); Settings.setStartupBehavior(behavior);
} }
if (behavior == Settings.StartupBehavior.AUTO_CONNECT && hostField != null && portField != null) { if (behavior == Settings.StartupBehavior.AUTO_CONNECT && savedHostBox != null) {
Settings.setAutoConnectHost(hostField.getText().trim()); Object sel = savedHostBox.getSelectedItem();
try { if (sel instanceof SavedHost) {
Settings.setAutoConnectPort(Integer.parseInt(portField.getText().trim())); SavedHost selected = (SavedHost) sel;
} catch (NumberFormatException ignored) { List<SavedHost> hosts = HostStorage.getSavedHosts();
for (SavedHost h : hosts) {
h.setAutoConnect(Objects.equals(h.getId(), selected.getId()));
}
HostStorage.saveAll(hosts);
Settings.setAutoConnectHost(selected.getHost());
Settings.setAutoConnectPort(selected.getPort());
Settings.setAutoConnectTls(selected.isUseTls());
Settings.setAutoConnectVerifyCert(selected.isTlsVerifyCert());
Settings.setAutoConnectTn3270e(selected.isTn3270e());
}
} else if (behavior != Settings.StartupBehavior.AUTO_CONNECT) {
List<SavedHost> hosts = HostStorage.getSavedHosts();
boolean modified = false;
for (SavedHost h : hosts) {
if (h.isAutoConnect()) {
h.setAutoConnect(false);
modified = true;
}
}
if (modified) {
HostStorage.saveAll(hosts);
} }
} }
} }
@@ -1371,12 +1461,7 @@ public class SettingsDialog extends JDialog {
autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT); autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT);
} }
} }
if (hostField != null) { refreshSavedHostsBox();
hostField.setText(Settings.getAutoConnectHost());
}
if (portField != null) {
portField.setText(String.valueOf(Settings.getAutoConnectPort()));
}
if (autoReconnectCheck != null) { if (autoReconnectCheck != null) {
autoReconnectCheck.setSelected(Settings.getAutoConnectAutoReconnect()); autoReconnectCheck.setSelected(Settings.getAutoConnectAutoReconnect());
} }
@@ -1456,7 +1541,7 @@ public class SettingsDialog extends JDialog {
// ========== Import Config ========== // ========== Import Config ==========
void importConfigFile(File file) throws IOException { public void importConfigFile(File file) throws IOException {
if (file == null || !file.exists()) { if (file == null || !file.exists()) {
throw new FileNotFoundException("Config file not found: " + (file != null ? file.getAbsolutePath() : "null")); throw new FileNotFoundException("Config file not found: " + (file != null ? file.getAbsolutePath() : "null"));
} }
@@ -1494,6 +1579,102 @@ public class SettingsDialog extends JDialog {
return btnExport; return btnExport;
} }
public JButton getBtnImportConnections() {
return btnImportConnections;
}
public JButton getBtnExportConnections() {
return btnExportConnections;
}
public JComboBox<SavedHost> getSavedHostBox() {
return savedHostBox;
}
// ========== Import / Export Connections ==========
public void importConnectionsFile(File file) throws IOException {
if (file == null || !file.exists()) {
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
}
HostStorage.importFromIniFile(file);
refreshSavedHostsBox();
SavedHost autoHost = HostStorage.getAutoConnectHost();
if (autoHost != null) {
Settings.setAutoConnectHost(autoHost.getHost());
Settings.setAutoConnectPort(autoHost.getPort());
Settings.setAutoConnectTls(autoHost.isUseTls());
Settings.setAutoConnectVerifyCert(autoHost.isTlsVerifyCert());
Settings.setAutoConnectTn3270e(autoHost.isTn3270e());
}
refreshFromSettings();
}
public void exportConnectionsFile(File file) throws IOException {
if (file == null) {
throw new IllegalArgumentException("Target file cannot be null");
}
HostStorage.exportToIniFile(file);
}
private void importConnections() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Import Connections");
fc.setSelectedFile(new java.io.File("connections.ini"));
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini"));
int result = fc.showOpenDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
java.io.File file = fc.getSelectedFile();
try {
importConnectionsFile(file);
int count = HostStorage.getSavedHosts().size();
JOptionPane.showMessageDialog(this,
"Connections imported successfully from:\n" + file.getAbsolutePath() + "\nTotal saved connections: " + count,
"Import Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"Import connections failed: " + ex.getMessage(),
"Import Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void exportConnections() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Export Connections");
fc.setSelectedFile(new java.io.File("connections.ini"));
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini"));
int result = fc.showSaveDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
java.io.File file = fc.getSelectedFile();
// Ensure .ini extension
if (!file.getName().toLowerCase().endsWith(".ini")) {
file = new java.io.File(file.getAbsolutePath() + ".ini");
}
// Confirm overwrite
if (file.exists()) {
int confirm = JOptionPane.showConfirmDialog(this,
"File already exists. Overwrite?", "Confirm Overwrite",
JOptionPane.YES_NO_OPTION);
if (confirm != JOptionPane.YES_OPTION) return;
}
try {
exportConnectionsFile(file);
JOptionPane.showMessageDialog(this,
"Connections exported to:\n" + file.getAbsolutePath(),
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"Export connections failed: " + ex.getMessage(),
"Export Error", JOptionPane.ERROR_MESSAGE);
}
}
}
// ========== Export Config ========== // ========== Export Config ==========
private void exportConfig() { private void exportConfig() {
@@ -357,9 +357,12 @@ public class StatusBar extends JPanel {
wordWrapStatus.setText(""); wordWrapStatus.setText("");
} }
// Field status: Numeric vs Alphanumeric // Field status: Numeric vs Alphanumeric vs DBCS
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) { if (client.getOIA().isDBCS()) {
fieldTypeStatus.setText("DBCS");
fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM"); fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme)); fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else { } else {
@@ -10,6 +10,12 @@ import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.*; import java.awt.datatransfer.*;
import java.awt.event.*; import java.awt.event.*;
import java.awt.event.InputMethodEvent;
import java.awt.event.InputMethodListener;
import java.awt.font.TextHitInfo;
import java.awt.im.InputMethodRequests;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
@@ -18,7 +24,8 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
* Supports colors, bold, underline, reverse, blink, GOCA vector graphics, * Supports colors, bold, underline, reverse, blink, GOCA vector graphics,
* programmed symbols, search highlighting, and all 3278/3279 attributes. * programmed symbols, search highlighting, and all 3278/3279 attributes.
*/ */
public class TerminalPanel extends JPanel implements java.awt.print.Printable { public class TerminalPanel extends JPanel implements java.awt.print.Printable, InputMethodListener, InputMethodRequests {
private Telnet3270Client client; private Telnet3270Client client;
@@ -29,7 +36,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int cellHeight; int cellHeight;
private int fontAscent; private int fontAscent;
private int fontDescent; private int fontDescent;
private int padding = 4; private int padding = 1;
private int currentFontSize = 16; private int currentFontSize = 16;
// Cursor state // Cursor state
@@ -96,6 +103,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private long lastGraphicsUpdateCount = -1; private long lastGraphicsUpdateCount = -1;
private java.awt.image.BufferedImage cachedGraphicsImage = null; private java.awt.image.BufferedImage cachedGraphicsImage = null;
private boolean resizeGuard = false; private boolean resizeGuard = false;
private Integer targetModelRows = null;
private Integer targetModelCols = null;
private StatusBar statusBar = null; private StatusBar statusBar = null;
// ========== Phase 25: Hotspots & Mouse Wheel ========== // ========== Phase 25: Hotspots & Mouse Wheel ==========
@@ -134,13 +143,21 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
/** /**
* Compute the horizontal render offset to center the grid within the panel. * Compute the horizontal render offset to center the grid within the panel.
* The left bound of the window is the left bound of the left cells when constrained.
* Only if the window is too big is there blank space on the left, and it is equal
* to or less than the space on the right at all times.
*/ */
int getRenderOffsetX() { int getRenderOffsetX() {
int termCols = 80; int termCols = 80;
if (client != null) termCols = client.getScreenBuffer().getDisplayCols(); if (client != null && client.getScreenBuffer() != null) {
termCols = client.getScreenBuffer().getDisplayCols();
}
int gridW = termCols * cellWidth; int gridW = termCols * cellWidth;
int extra = getWidth() - gridW; int extra = getWidth() - gridW;
return Math.max(padding, extra / 2); if (extra > 0) {
return extra / 2;
}
return 0;
} }
/** /**
@@ -148,10 +165,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
*/ */
int getRenderOffsetY() { int getRenderOffsetY() {
int termRows = 24; int termRows = 24;
if (client != null) termRows = client.getScreenBuffer().getDisplayRows(); if (client != null && client.getScreenBuffer() != null) {
termRows = client.getScreenBuffer().getDisplayRows();
}
int gridH = termRows * cellHeight; int gridH = termRows * cellHeight;
int extra = getHeight() - gridH; int extra = getHeight() - gridH;
return Math.max(padding, extra / 2); if (extra > 0) {
return extra / 2;
}
return 0;
} }
// Default Host color mapping aligned 1:1 with IBM Host On-Demand ColorRemapModel3270 // Default Host color mapping aligned 1:1 with IBM Host On-Demand ColorRemapModel3270
@@ -201,6 +223,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle(); String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK; cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
enableInputMethods(true);
addInputMethodListener(this);
// Handle mouse clicks to position cursor, selection, and grab focus // Handle mouse clicks to position cursor, selection, and grab focus
MouseAdapter mouseHandler = new MouseAdapter() { MouseAdapter mouseHandler = new MouseAdapter() {
@Override @Override
@@ -210,11 +236,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
boolean wasPanelUnfocused = !hasFocus(); boolean wasPanelUnfocused = !hasFocus();
isActivationClick = wasWindowInactive || wasPanelUnfocused; isActivationClick = wasWindowInactive || wasPanelUnfocused;
if (wasWindowInactive) { if (win != null && wasWindowInactive) {
win.toFront(); win.toFront();
win.requestFocus();
} }
requestFocus(); requestFocus();
SwingUtilities.invokeLater(() -> {
if (!hasFocus()) {
requestFocus();
}
});
// Right-click context menu // Right-click context menu
if (SwingUtilities.isRightMouseButton(e)) { if (SwingUtilities.isRightMouseButton(e)) {
@@ -407,6 +437,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (!hasFocus()) {
requestFocus();
}
if (isDragging) { if (isDragging) {
isDragging = false; isDragging = false;
if (activeDragHandle != DragHandle.NONE) { if (activeDragHandle != DragHandle.NONE) {
@@ -700,6 +733,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
repaint(); repaint();
} }
public void select(int startRow, int startCol, int endRow, int endCol) {
selectionStartRow = startRow;
selectionStartCol = startCol;
selectionEndRow = endRow;
selectionEndCol = endCol;
repaint();
}
public boolean hasSelection() { public boolean hasSelection() {
return selectionStartRow >= 0 && selectionEndRow >= 0 && return selectionStartRow >= 0 && selectionEndRow >= 0 &&
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol); !(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
@@ -725,11 +766,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
for (int c = c1; c <= c2; c++) { for (int c = c1; c <= c2; c++) {
int baddr = r * cols + c; int baddr = r * cols + c;
ExtendedAttribute ea = sb.getCell(baddr); ExtendedAttribute ea = sb.getCell(baddr);
char ch = ea.ucs4; if (ea.isFieldAttribute()) {
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) { line.append(' ');
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
if (c > c1) {
continue; // Character already added by DB_LEFT
} else {
line.append(' ');
}
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
line.append(' '); line.append(' ');
} else { } else {
line.append(ch); char ch = ea.ucs4;
if (ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
} }
} }
// Trim trailing whitespace on copied lines per HoD CutPasteOptions // Trim trailing whitespace on copied lines per HoD CutPasteOptions
@@ -767,11 +820,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
prevRow = curRow; prevRow = curRow;
} }
ExtendedAttribute ea = sb.getCell(addr); ExtendedAttribute ea = sb.getCell(addr);
char ch = ea.ucs4; if (ea.isFieldAttribute()) {
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) { line.append(' ');
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
if (addr > startAddr) {
continue; // Character already added by DB_LEFT
} else {
line.append(' ');
}
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
line.append(' '); line.append(' ');
} else { } else {
line.append(ch); char ch = ea.ucs4;
if (ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
} }
} }
int end = line.length(); int end = line.length();
@@ -1017,6 +1082,33 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
public void packForModel(int modelRows, int modelCols) {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> packForModel(modelRows, modelCols));
return;
}
resizeGuard = true;
updateCellSize();
this.targetModelRows = modelRows;
this.targetModelCols = modelCols;
revalidate();
Container top = getTopLevelAncestor();
if (top instanceof java.awt.Window) {
java.awt.Window win = (java.awt.Window) top;
if (win instanceof java.awt.Frame) {
if ((((java.awt.Frame) win).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) {
win.pack();
}
} else {
win.pack();
}
}
this.targetModelRows = null;
this.targetModelCols = null;
repaint();
SwingUtilities.invokeLater(() -> resizeGuard = false);
}
public void guardedPack() { public void guardedPack() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::guardedPack); SwingUtilities.invokeLater(this::guardedPack);
@@ -1025,12 +1117,35 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
resizeGuard = true; resizeGuard = true;
revalidate(); revalidate();
Container top = getTopLevelAncestor(); Container top = getTopLevelAncestor();
if (top instanceof java.awt.Frame) { if (top instanceof java.awt.Window) {
if ((((java.awt.Frame) top).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) { java.awt.Window win = (java.awt.Window) top;
((java.awt.Window) top).pack(); if (win.isShowing()) {
int termCols = 80;
int termRows = 24;
if (client != null && client.getScreenBuffer() != null) {
termCols = client.getScreenBuffer().getDisplayCols();
termRows = client.getScreenBuffer().getDisplayRows();
}
int gridW = termCols * cellWidth;
int gridH = termRows * cellHeight;
Dimension winSize = win.getSize();
Dimension winPref = win.getPreferredSize();
boolean tooSmall = winSize.width < winPref.width || winSize.height < winPref.height
|| (getWidth() > 0 && getWidth() < gridW)
|| (getHeight() > 0 && getHeight() < gridH);
if (tooSmall) {
repaint();
SwingUtilities.invokeLater(() -> resizeGuard = false);
return;
}
}
if (win instanceof java.awt.Frame) {
if ((((java.awt.Frame) win).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) {
win.pack();
}
} else {
win.pack();
} }
} else if (top instanceof java.awt.Window) {
((java.awt.Window) top).pack();
} }
SwingUtilities.invokeLater(() -> resizeGuard = false); SwingUtilities.invokeLater(() -> resizeGuard = false);
} }
@@ -1873,7 +1988,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
@Override @Override
public Dimension getPreferredSize() { public Dimension getPreferredSize() {
if (client != null) { if (targetModelRows != null && targetModelCols != null) {
return new Dimension(targetModelCols * cellWidth + padding * 2,
targetModelRows * cellHeight + padding * 2);
}
if (client != null && client.getScreenBuffer() != null) {
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
int displayCols = sb.getDisplayCols(); int displayCols = sb.getDisplayCols();
int displayRows = sb.getDisplayRows(); int displayRows = sb.getDisplayRows();
@@ -2030,13 +2149,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
continue; continue;
} }
byte tr = ea.tr != 0 ? ea.tr : (currentFieldEa != null ? currentFieldEa.tr : 0);
boolean isOpaque = ((tr & 0xFF) == XAT_OPAQUE);
if (reverse) { if (reverse) {
Color tmp = fgColor; Color tmp = fgColor;
fgColor = (rawBg == 0) ? this.bgColor : bgColor; fgColor = (rawBg == 0) ? this.bgColor : bgColor;
bgColor = tmp; bgColor = tmp;
g2.setColor(bgColor); g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight); g2.fillRect(x, y, cellWidth, cellHeight);
} else if (bgIsExplicit || !bgColor.equals(this.bgColor)) { } else if (isOpaque || bgIsExplicit || !bgColor.equals(this.bgColor)) {
g2.setColor(bgColor); g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight); g2.fillRect(x, y, cellWidth, cellHeight);
} }
@@ -2051,7 +2173,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (cs >= 0x40 && client.getProgramSymbolManager() != null && !suppressGlyph) { if (cs >= 0x40 && client.getProgramSymbolManager() != null && !suppressGlyph) {
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) { if (slot != null) {
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0; int symBg = (bgIsExplicit || reverse || isOpaque) ? bgColor.getRGB() : 0;
haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg); haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg);
java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb); java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb);
if (img != null) { if (img != null) {
@@ -2061,7 +2183,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
if (!drawnAsPs && !suppressGlyph) { if (!drawnAsPs && !suppressGlyph && ea.db != ExtendedAttribute.DB_RIGHT
&& ea.db != ExtendedAttribute.DB_SO && ea.db != ExtendedAttribute.DB_SI) {
char ch = ea.ucs4; char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) { if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? boldTerminalFont : terminalFont; Font f = bold ? boldTerminalFont : terminalFont;
@@ -2200,11 +2323,17 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
boolean isUnderline = (cursorStyle == CursorStyle.UNDERLINE) || boolean isUnderline = (cursorStyle == CursorStyle.UNDERLINE) ||
(client.getInputProcessor() != null && client.getInputProcessor().isInsertMode()); (client.getInputProcessor() != null && client.getInputProcessor().isInsertMode());
g2.setColor(CURSOR_COLOR); g2.setColor(CURSOR_COLOR);
ExtendedAttribute curEa = (curAddr >= 0 && curAddr < sb.getSize()) ? sb.getDisplayCell(curAddr) : null;
boolean isDbcs = curEa != null && (curEa.db == ExtendedAttribute.DB_LEFT || curEa.db == ExtendedAttribute.DB_RIGHT);
int cWidth = isDbcs ? (cellWidth * 2) : cellWidth;
if (isDbcs && curEa.db == ExtendedAttribute.DB_RIGHT && curCol > 0) {
cx -= cellWidth;
}
if (isUnderline) { if (isUnderline) {
int ulH = Math.max(2, cellHeight / 6); int ulH = Math.max(2, cellHeight / 6);
g2.fillRect(cx, cy + cellHeight - ulH, cellWidth, ulH); g2.fillRect(cx, cy + cellHeight - ulH, cWidth, ulH);
} else { } else {
g2.fillRect(cx, cy, cellWidth, cellHeight); g2.fillRect(cx, cy, cWidth, cellHeight);
} }
} }
} }
@@ -2393,4 +2522,90 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
paint(g2d); paint(g2d);
return PAGE_EXISTS; return PAGE_EXISTS;
} }
// =========================================================================
// Java Input Method Framework (IMF / IME) Support
// =========================================================================
@Override
public InputMethodRequests getInputMethodRequests() {
return this;
}
@Override
public void inputMethodTextChanged(InputMethodEvent event) {
if (client == null || !client.getConnectionState().isFullSession()) {
event.consume();
return;
}
AttributedCharacterIterator text = event.getText();
if (text != null) {
int committedCount = event.getCommittedCharacterCount();
char c = text.first();
for (int i = 0; i < committedCount && c != CharacterIterator.DONE; i++) {
client.typeCharacter(c);
c = text.next();
}
refreshScreen();
}
event.consume();
}
@Override
public void caretPositionChanged(InputMethodEvent event) {
// No-op
}
@Override
public Rectangle getTextLocation(TextHitInfo offset) {
Point p;
try {
p = getLocationOnScreen();
} catch (IllegalComponentStateException e) {
p = new Point(0, 0);
}
int curRow = 0, curCol = 0;
if (client != null && client.getScreenBuffer() != null) {
int cols = client.getScreenBuffer().getCols();
int curAddr = client.getScreenBuffer().getDisplayCursorAddress();
if (cols > 0) {
curRow = curAddr / cols;
curCol = curAddr % cols;
}
}
int cx = p.x + getRenderOffsetX() + curCol * cellWidth;
int cy = p.y + getRenderOffsetY() + curRow * cellHeight;
return new Rectangle(cx, cy, cellWidth, cellHeight);
}
@Override
public TextHitInfo getLocationOffset(int x, int y) {
return TextHitInfo.leading(0);
}
@Override
public int getInsertPositionOffset() {
return 0;
}
@Override
public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public int getCommittedTextLength() {
return 0;
}
@Override
public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
} }
@@ -0,0 +1,297 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.ConnectChoiceDialog;
import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionConflictTest {
private final List<J3270App> createdApps = new ArrayList<>();
@BeforeEach
public void setUp() {
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
try {
app.dispose();
} catch (Exception ignored) {}
}
}
@AfterEach
public void tearDown() {
for (J3270App app : createdApps) {
try {
app.dispose();
} catch (Exception ignored) {}
}
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
try {
app.dispose();
} catch (Exception ignored) {}
}
}
private J3270App createApp() {
try {
J3270App app = new J3270App();
createdApps.add(app);
return app;
} catch (HeadlessException e) {
return null;
}
}
@Test
@DisplayName("ConnectChoiceDialog components, styling, and action results")
public void testConnectChoiceDialogComponents() {
try {
ConnectChoiceDialog dialog = new ConnectChoiceDialog(null, "mvs.corp.local:23");
assertEquals("Active Connection", dialog.getTitle());
assertNotNull(dialog.getNewWindowButton());
assertNotNull(dialog.getAbortButton());
assertNotNull(dialog.getCancelButton());
assertEquals("New Window", dialog.getNewWindowButton().getText());
assertEquals("Abort Current Connection", dialog.getAbortButton().getText());
assertEquals("Cancel", dialog.getCancelButton().getText());
assertTrue(dialog.getLine1().getText().contains("mvs.corp.local:23"));
assertTrue(dialog.getLine2().getText().contains("new window"));
assertTrue(dialog.getLine2().getText().contains("abort"));
// Test button click choices
dialog.getNewWindowButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.NEW_WINDOW, dialog.getChoice());
ConnectChoiceDialog dialog2 = new ConnectChoiceDialog(null, "cms.test:992");
dialog2.getAbortButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.ABORT_CURRENT, dialog2.getChoice());
ConnectChoiceDialog dialog3 = new ConnectChoiceDialog(null, "cics.test:23");
dialog3.getCancelButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.CANCEL, dialog3.getChoice());
} catch (HeadlessException ignored) {
// Expected in headless CI environments without X11
}
}
@Test
@DisplayName("When connected, Connect with NEW_WINDOW choice opens in a new window and preserves current connection")
public void testConnectChoiceNewWindow() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
config1.setSavedProfileName("Host 1");
app.connect(config1, false);
assertTrue(app.getConnectedTarget().contains("host1.test") || app.getConnectedTarget().contains("Host 1"));
// Configure handler to return NEW_WINDOW
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
assertTrue(target.contains("host1.test") || target.contains("Host 1"));
return ConnectChoiceDialog.Choice.NEW_WINDOW;
});
int initialWindows = J3270App.getOpenWindows().size();
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
config2.setSavedProfileName("Host 2");
app.connect(config2, true);
// Handler was invoked
assertTrue(handlerCalled[0], "Connect choice handler must be called when already connected");
// A new window was opened
assertEquals(initialWindows + 1, J3270App.getOpenWindows().size(),
"A new window should have been added to openWindows");
// Original app remains on config1
assertEquals("host1.test", app.getClient().getConfig().getHost());
// Find the new window
J3270App newWindow = null;
for (J3270App w : J3270App.getOpenWindows()) {
if (w != app) {
newWindow = w;
createdApps.add(w);
break;
}
}
assertNotNull(newWindow, "New window must exist");
assertNotNull(newWindow.getClient(), "New window should have a client");
assertEquals("host2.test", newWindow.getClient().getConfig().getHost(),
"New window must be connected to host2.test");
}
@Test
@DisplayName("When connected, Connect with ABORT_CURRENT choice aborts existing connection and connects in same window")
public void testConnectChoiceAbortCurrent() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
int initialWindows = J3270App.getOpenWindows().size();
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
});
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
assertTrue(handlerCalled[0], "Connect choice handler must be called");
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
"No new window should be created when choosing ABORT_CURRENT");
assertEquals("host2.test", app.getClient().getConfig().getHost(),
"Same window should now be connected to host2.test");
}
@Test
@DisplayName("When connected, Connect with CANCEL choice keeps existing connection and creates no new window")
public void testConnectChoiceCancel() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
int initialWindows = J3270App.getOpenWindows().size();
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
return ConnectChoiceDialog.Choice.CANCEL;
});
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
assertTrue(handlerCalled[0], "Connect choice handler must be called");
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
"No new window should be created when choosing CANCEL");
assertEquals("host1.test", app.getClient().getConfig().getHost(),
"Existing window must remain connected to host1.test");
}
@Test
@DisplayName("File menu contains Close Window item with VK_W accelerator")
public void testCloseWindowMenuItem() {
J3270App app = createApp();
if (app == null) return;
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
JMenu fileMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
if ("File".equals(mb.getMenu(i).getText())) {
fileMenu = mb.getMenu(i);
break;
}
}
assertNotNull(fileMenu, "File menu must exist");
JMenuItem closeWindowItem = null;
for (int i = 0; i < fileMenu.getItemCount(); i++) {
JMenuItem item = fileMenu.getItem(i);
if (item != null && "Close Window".equals(item.getText())) {
closeWindowItem = item;
break;
}
}
assertNotNull(closeWindowItem, "File menu must contain Close Window item");
KeyStroke ks = closeWindowItem.getAccelerator();
assertNotNull(ks, "Close Window item must have an accelerator");
assertEquals(KeyEvent.VK_W, ks.getKeyCode());
}
@Test
@DisplayName("Closing one window when multiple windows are open leaves remaining window open")
public void testMultiWindowCloseIndependent() {
J3270App app1 = createApp();
if (app1 == null) return;
J3270App app2 = createApp();
if (app2 == null) return;
assertTrue(J3270App.getOpenWindows().contains(app1));
assertTrue(J3270App.getOpenWindows().contains(app2));
assertEquals(2, J3270App.getOpenWindows().size());
// Close app1
app1.closeWindow();
assertFalse(J3270App.getOpenWindows().contains(app1), "app1 should be removed from open windows");
assertTrue(J3270App.getOpenWindows().contains(app2), "app2 should remain open");
assertEquals(1, J3270App.getOpenWindows().size());
}
@Test
@DisplayName("Abort current connection cleanly tears down old client and connects new client in same window")
public void testAbortCurrentConnectionGracefullyDisconnects() throws Exception {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
Telnet3270Client oldClient = app.getClient();
assertNotNull(oldClient);
app.setConnectChoiceHandler((owner, target) -> ConnectChoiceDialog.Choice.ABORT_CURRENT);
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
// Client on app is immediately replaced with new client
Telnet3270Client newClient = app.getClient();
assertNotNull(newClient);
assertNotSame(oldClient, newClient);
assertEquals("host2.test", newClient.getConfig().getHost());
// Old client should be disconnected
Thread.sleep(100);
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, oldClient.getConnectionState());
assertFalse(oldClient.isConnected());
}
@Test
@DisplayName("Explicit app.disconnect() tears down client and clears UI state")
public void testExplicitDisconnectResetsUI() throws Exception {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config, false);
Telnet3270Client client = app.getClient();
assertNotNull(client);
app.disconnect();
assertNull(app.getClient());
assertFalse(app.isConnected());
Thread.sleep(100);
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, client.getConnectionState());
assertFalse(client.isConnected());
}
}
@@ -85,7 +85,8 @@ public class LoggingConfigurationTest {
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel()); assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
File logFile = new File("j3270.log"); File logFile = new File("j3270.log");
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled"); File logFile1 = new File("j3270.log.1");
assertTrue(logFile.exists() || logFile1.exists(), "j3270.log should be created when debug is enabled");
// Clean up // Clean up
J3270App.configureLogging(false); J3270App.configureLogging(false);
@@ -0,0 +1,271 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.Frame;
import java.awt.HeadlessException;
import java.io.File;
import java.io.PrintWriter;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionHostTypeConfigTest {
private List<SavedHost> originalSavedHosts;
private String originalDefaultHostType;
@BeforeEach
public void setUp() {
FileTransferDialog.resetSessionState();
originalSavedHosts = HostStorage.getSavedHosts();
originalDefaultHostType = Settings.getDefaultHostType();
HostStorage.clearAll();
}
@AfterEach
public void tearDown() {
FileTransferDialog.resetSessionState();
HostStorage.clearAll();
if (originalSavedHosts != null) {
HostStorage.saveAll(originalSavedHosts);
}
Settings.setDefaultHostType(originalDefaultHostType);
}
@Test
@DisplayName("SavedHost converts to ConnectionConfig preserving hostType and saved connection metadata")
public void testSavedHostToConnectionConfigPreservesHostType() {
SavedHost cmsHost = new SavedHost(
"cms-uuid-1", "VM/CMS System", "vm370.corp.local", 23, 4, 62, 160, "",
false, "CMS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgCms = cmsHost.toConnectionConfig();
assertTrue(cfgCms.isSavedConnection(), "Config from SavedHost must be marked as saved connection");
assertEquals("cms-uuid-1", cfgCms.getSavedProfileId());
assertEquals("VM/CMS System", cfgCms.getSavedProfileName());
assertEquals("CMS", cfgCms.getHostType());
assertEquals(FTConfig.HostType.CMS, cfgCms.getFtHostType());
SavedHost cicsHost = new SavedHost(
"cics-uuid-2", "CICS Gateway", "cics.corp.local", 23, 2, 24, 80, "LU1",
false, "CICS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgCics = cicsHost.toConnectionConfig();
assertTrue(cfgCics.isSavedConnection());
assertEquals("CICS", cfgCics.getHostType());
assertEquals(FTConfig.HostType.CICS, cfgCics.getFtHostType());
SavedHost tsoHost = new SavedHost(
"tso-uuid-3", "MVS TSO", "tso.corp.local", 23, 4, 43, 80, "",
false, "TSO", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgTso = tsoHost.toConnectionConfig();
assertTrue(cfgTso.isSavedConnection());
assertEquals("TSO", cfgTso.getHostType());
assertEquals(FTConfig.HostType.TSO, cfgTso.getFtHostType());
}
@Test
@DisplayName("FileTransferDialog preselects CMS when using a saved connection configured as CMS")
public void testFileTransferDialogPrefillsFromSavedConnectionCms() {
ConnectionConfig config = new ConnectionConfig("vm.example.com", 23);
config.setSavedConnection(true);
config.setSavedProfileId("vm-1");
config.setSavedProfileName("VM370");
config.setHostType("CMS");
Telnet3270Client client = new Telnet3270Client(config);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must preselect CMS when saved connection is configured as CMS");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("FileTransferDialog preselects CICS when using a saved connection configured as CICS")
public void testFileTransferDialogPrefillsFromSavedConnectionCics() {
ConnectionConfig config = new ConnectionConfig("cics.example.com", 23);
config.setSavedConnection(true);
config.setSavedProfileId("cics-1");
config.setSavedProfileName("CICS Region");
config.setHostType("CICS");
Telnet3270Client client = new Telnet3270Client(config);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CICS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must preselect CICS when saved connection is configured as CICS");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("FileTransferDialog overrides stale prior transfer state with configured host type from saved connection")
public void testFileTransferDialogOverridesOldTransferStateWhenSavedConnectionUsed() {
// Simulate a prior transfer performed on a TSO system
FileTransferDialog.TransferSessionState oldState = new FileTransferDialog.TransferSessionState();
oldState.hostType = FTConfig.HostType.TSO;
oldState.localFile = "test.txt";
oldState.hostFile = "USER.DATA";
FileTransferDialog.setLastTransferState(oldState);
// Now connect to a CMS saved connection
ConnectionConfig cmsConfig = new ConnectionConfig("vm.corp.internal", 23);
cmsConfig.setSavedConnection(true);
cmsConfig.setSavedProfileName("VM Prod");
cmsConfig.setHostType("CMS");
Telnet3270Client client = new Telnet3270Client(cmsConfig);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must use the configured host type (CMS) of the saved connection rather than prior session's TSO");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("ConnectDialog allows host type configuration and saves it to profile")
public void testConnectDialogHostTypeConfigurationAndProfile() {
try {
ConnectDialog dialog = new ConnectDialog((Frame) null);
assertNotNull(dialog.getHostTypeCombo(), "ConnectDialog must provide a Host Type combo box");
assertEquals("TSO", dialog.getSelectedHostType());
// Switch to CMS
dialog.setInitialHostType("CMS");
assertEquals("CMS", dialog.getSelectedHostType());
assertEquals(FTConfig.HostType.CMS, dialog.getSelectedFtHostType());
// Switch to CICS
dialog.setInitialHostType("CICS");
assertEquals("CICS", dialog.getSelectedHostType());
assertEquals(FTConfig.HostType.CICS, dialog.getSelectedFtHostType());
// Loading a SavedHost profile updates host type in ConnectDialog
SavedHost cmsProfile = new SavedHost(
"cms-prof-1", "CMS Dev", "cms.dev.local", 23, 4, 62, 160, "",
false, "CMS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
dialog.loadProfile(cmsProfile);
assertEquals("CMS", dialog.getSelectedHostType());
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("Settings persists and exports defaultHostType")
public void testSettingsDefaultHostTypePersistenceAndIni() throws Exception {
Settings.setDefaultHostType("CMS");
assertEquals("CMS", Settings.getDefaultHostType());
File tmpIni = File.createTempFile("j3270_test_cfg", ".ini");
tmpIni.deleteOnExit();
try (PrintWriter writer = new PrintWriter(tmpIni)) {
writer.println("[connection]");
writer.println("hostType = CICS");
}
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
assertEquals("CICS", Settings.getDefaultHostType());
// Test export
File exportFile = File.createTempFile("j3270_export", ".ini");
exportFile.deleteOnExit();
Settings.exportToIniFile(exportFile.getAbsolutePath());
String content = new String(java.nio.file.Files.readAllBytes(exportFile.toPath()));
assertTrue(content.contains("hostType = CICS"), "Exported INI must contain hostType = CICS");
}
@Test
@DisplayName("J3270App setConnectionHostType updates connection config and syncs with profile in HostStorage")
public void testSetConnectionHostTypeUpdatesActiveConnectionAndStorage() {
SavedHost saved = new SavedHost(
"saved-prof-1", "Prod CMS", "vm.prod", 23, 4, 62, 160, "",
false, "TSO", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
HostStorage.saveHost(saved);
ConnectionConfig config = saved.toConnectionConfig();
assertEquals("TSO", config.getHostType());
try {
J3270App app = new J3270App();
try {
// Verify Connection menu exists in JMenuBar
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
boolean hasConnectionMenu = false;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "Connection".equals(m.getText())) {
hasConnectionMenu = true;
break;
}
}
assertTrue(hasConnectionMenu, "JMenuBar must include a Connection menu");
// Simulate connecting with this config
app.connect(config);
// Change host type via setConnectionHostType
app.setConnectionHostType(FTConfig.HostType.CMS);
assertEquals(FTConfig.HostType.CMS, app.getClient().getConfig().getFtHostType());
// Profile in storage should also be updated
SavedHost reloaded = HostStorage.findById("saved-prof-1");
assertNotNull(reloaded);
assertEquals("CMS", reloaded.getHostType());
} finally {
app.dispose();
}
} catch (HeadlessException ignored) {
// In headless environments where JFrame cannot be initialized, test model updates directly
Telnet3270Client client = new Telnet3270Client(config);
client.getConfig().setFtHostType(FTConfig.HostType.CMS);
saved.setHostType(FTConfig.HostType.CMS.name());
HostStorage.saveHost(saved);
SavedHost reloaded = HostStorage.findById("saved-prof-1");
assertNotNull(reloaded);
assertEquals("CMS", reloaded.getHostType());
}
}
}
@@ -1,6 +1,9 @@
package haus.nightmare.j3270.ft; 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.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -99,4 +102,114 @@ public class FileTransferSessionAndCmsTest {
// Headless environment; verified through state retention // Headless environment; verified through state retention
} }
} }
@Test
public void testPdsSessionStatePersistence() {
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.TSO;
state.isSend = true;
state.hostFile = "'USER.PDS.DATA'";
state.units = FTConfig.AllocationUnit.CYLINDERS;
state.space = "10,5";
state.dirBlocks = "20";
FileTransferDialog.setLastTransferState(state);
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
assertNotNull(retrieved);
assertEquals(FTConfig.AllocationUnit.CYLINDERS, retrieved.units);
assertEquals("10,5", retrieved.space);
assertEquals("20", retrieved.dirBlocks);
try {
FileTransferDialog dialog = new FileTransferDialog(null, null);
assertEquals(FTConfig.AllocationUnit.CYLINDERS, dialog.getUnitsCombo().getSelectedItem());
assertEquals("10,5", dialog.getSpaceField().getText());
assertEquals("20", dialog.getDirField().getText());
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
public void testHostDirectoryDialogStructureAndFilter() {
// Test default query resolution
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, null));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, ""));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, " "));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "*"));
assertEquals("TEST EXEC", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "TEST EXEC"));
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, null));
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, ""));
assertEquals("'MY.DATASET'", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, "'MY.DATASET'"));
// Test CMS filter formatting
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(null));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(""));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("* * *"));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("*"));
assertEquals("TEST * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST"));
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC"));
assertEquals("TEST EXEC B", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC B"));
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST.EXEC"));
try {
haus.nightmare.j3270.ui.HostDirectoryDialog dialog =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "");
assertNotNull(dialog);
assertFalse(dialog.isConfirmed());
assertNull(dialog.getSelectedHostFile());
assertEquals("* * A", dialog.getQueryField().getText());
dialog.dispose();
haus.nightmare.j3270.ui.HostDirectoryDialog dialog2 =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "* * *");
assertEquals("* * A", dialog2.getQueryField().getText());
dialog2.dispose();
haus.nightmare.j3270.ui.HostDirectoryDialog dialog3 =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "TEST EXEC");
assertEquals("TEST EXEC", dialog3.getQueryField().getText());
dialog3.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
public void testMatchesFilterWildcards() {
CMSDirectoryEntry cmsA = new CMSDirectoryEntry("PROFILE", "EXEC", "A1");
CMSDirectoryEntry cmsB = new CMSDirectoryEntry("PROFILE", "EXEC", "B1");
CMSDirectoryEntry cmsSys = new CMSDirectoryEntry("SYSTEM", "CONFIG", "S2");
// * * A matches A disk files (A1, A0, etc.), excludes B and S
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* * A"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsB, "* * A"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* * A"));
// Blank, *, * * * matches all
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, ""));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsB, "* * *"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsSys, "* * *"));
// Two tokens (fn ft) and dot tokens
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* EXEC"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*.EXEC"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROFILE *"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROF*.*"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* EXEC"));
// TSO Wildcards
TSODirectoryEntry tso = new TSODirectoryEntry("TSOUSER.TEST.DATA");
assertTrue(HostDirectoryDialog.matchesFilter(tso, ""));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "* * *"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*.DATA"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TEST"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TSOUSER.*"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "'TSOUSER.TEST.*'"));
assertFalse(HostDirectoryDialog.matchesFilter(tso, "*.CNTL"));
}
} }
@@ -0,0 +1,342 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.ui.SettingsDialog;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionIniStorageTest {
private List<SavedHost> originalSavedHosts;
private String origHost;
private int origPort;
@BeforeEach
public void setUp() {
originalSavedHosts = HostStorage.getSavedHosts();
origHost = Settings.getAutoConnectHost();
origPort = Settings.getAutoConnectPort();
}
@AfterEach
public void tearDown() {
if (originalSavedHosts != null) {
HostStorage.saveAll(originalSavedHosts);
} else {
HostStorage.clearAll();
}
Settings.setAutoConnectHost(origHost);
Settings.setAutoConnectPort(origPort);
}
@Test
@DisplayName("HostStorage exports to and loads from connections.ini with full fidelity")
public void testExportAndLoadConnectionsIniRoundtrip() throws IOException {
List<SavedHost> hosts = new ArrayList<>();
SavedHost h1 = new SavedHost("id-mainframe-1", "Production TSO", "mainframe.corp.com", 23, 4, 62, 160,
"LU01", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
SavedHost h2 = new SavedHost("id-vm-2", "VM CMS Test", "vmcms.lab.net", 992, 2, 24, 80,
"LUCMS", false, "CMS", true, false, true, "GDDM", "1047", "SOCKS5", "10.0.0.1", 1080, "proxyuser", "proxypass");
hosts.add(h1);
hosts.add(h2);
File tmpIni = File.createTempFile("connections_test", ".ini");
tmpIni.deleteOnExit();
try {
HostStorage.exportToIniFile(tmpIni, hosts);
assertTrue(tmpIni.exists());
String content = new String(Files.readAllBytes(tmpIni.toPath()));
assertTrue(content.contains("[Production TSO]"), "Must contain section header [Production TSO]");
assertTrue(content.contains("[VM CMS Test]"), "Must contain section header [VM CMS Test]");
assertTrue(content.contains("host = mainframe.corp.com"));
assertTrue(content.contains("proxyHost = 10.0.0.1"));
assertTrue(content.contains("proxyType = SOCKS5"));
List<SavedHost> loaded = HostStorage.loadFromIniFile(tmpIni);
assertEquals(2, loaded.size(), "Should load 2 connections from INI");
SavedHost loaded1 = loaded.get(0);
assertEquals("id-mainframe-1", loaded1.getId());
assertEquals("Production TSO", loaded1.getName());
assertEquals("mainframe.corp.com", loaded1.getHost());
assertEquals(23, loaded1.getPort());
assertEquals(4, loaded1.getModel());
assertEquals(62, loaded1.getDynamicRows());
assertEquals(160, loaded1.getDynamicCols());
assertEquals("LU01", loaded1.getLuName());
assertTrue(loaded1.isAutoConnect());
assertEquals("TSO", loaded1.getHostType());
assertFalse(loaded1.isUseTls());
assertTrue(loaded1.isTlsVerifyCert());
SavedHost loaded2 = loaded.get(1);
assertEquals("id-vm-2", loaded2.getId());
assertEquals("VM CMS Test", loaded2.getName());
assertEquals("vmcms.lab.net", loaded2.getHost());
assertEquals(992, loaded2.getPort());
assertEquals(2, loaded2.getModel());
assertEquals(24, loaded2.getDynamicRows());
assertEquals(80, loaded2.getDynamicCols());
assertEquals("LUCMS", loaded2.getLuName());
assertFalse(loaded2.isAutoConnect());
assertEquals("CMS", loaded2.getHostType());
assertTrue(loaded2.isUseTls());
assertFalse(loaded2.isTlsVerifyCert());
assertEquals("SOCKS5", loaded2.getProxyType());
assertEquals("10.0.0.1", loaded2.getProxyHost());
assertEquals(1080, loaded2.getProxyPort());
assertEquals("proxyuser", loaded2.getProxyUsername());
assertEquals("proxypass", loaded2.getProxyPassword());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("HostStorage importFromIniFile merges and updates existing hosts by name or id")
public void testImportFromIniFileMergesAndUpdates() throws IOException {
SavedHost initial = new SavedHost("id-100", "Mainframe Prod", "old.host.org", 23, 4, 62, 160,
"", false, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
List<SavedHost> initList = new ArrayList<>();
initList.add(initial);
HostStorage.saveAll(initList);
File tmpIni = File.createTempFile("connections_merge", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
// Update Mainframe Prod with new host address
w.write("[Mainframe Prod]\n");
w.write("id = id-100\n");
w.write("host = new.host.org\n");
w.write("port = 2323\n");
w.write("\n");
// Add a second connection
w.write("[Development CICS]\n");
w.write("host = cics.dev.org\n");
w.write("port = 992\n");
w.write("hostType = CICS\n");
w.write("useTls = true\n");
}
List<SavedHost> result = HostStorage.importFromIniFile(tmpIni);
assertEquals(2, result.size(), "Import should result in 2 saved hosts (1 updated, 1 added)");
SavedHost updated = HostStorage.findById("id-100");
assertNotNull(updated);
assertEquals("new.host.org", updated.getHost());
assertEquals(2323, updated.getPort());
SavedHost added = HostStorage.findByName("Development CICS");
assertNotNull(added);
assertEquals("cics.dev.org", added.getHost());
assertEquals(992, added.getPort());
assertEquals("CICS", added.getHostType());
assertTrue(added.isUseTls());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("HostStorage parses INI section prefixes and aliases properly")
public void testImportHandlesMissingFieldsAndAliases() throws IOException {
File tmpIni = File.createTempFile("connections_alias", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
w.write("[connection:Aliased Host]\n");
w.write("hostname = test.alias.net\n");
w.write("port = 1023\n");
w.write("terminalModel = 2\n");
w.write("type = CMS\n");
w.write("tls = true\n");
w.write("verifyCert = false\n");
w.write("charset = 1047\n");
w.write("rows = 24\n");
w.write("cols = 80\n");
w.write("lu = LUVM\n");
}
List<SavedHost> parsed = HostStorage.loadFromIniFile(tmpIni);
assertEquals(1, parsed.size());
SavedHost h = parsed.get(0);
assertEquals("Aliased Host", h.getName());
assertEquals("test.alias.net", h.getHost());
assertEquals(1023, h.getPort());
assertEquals(2, h.getModel());
assertEquals("CMS", h.getHostType());
assertTrue(h.isUseTls());
assertFalse(h.isTlsVerifyCert());
assertEquals("1047", h.getCodePage());
assertEquals(24, h.getDynamicRows());
assertEquals(80, h.getDynamicCols());
assertEquals("LUVM", h.getLuName());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("SettingsDialog contains Import Connections and Export Connections buttons and is taller")
public void testSettingsDialogConnectionButtonsAndHeight() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JButton btnImportConn = dialog.getBtnImportConnections();
JButton btnExportConn = dialog.getBtnExportConnections();
assertNotNull(btnImportConn, "Import Connections button must exist in SettingsDialog");
assertNotNull(btnExportConn, "Export Connections button must exist in SettingsDialog");
assertEquals("Import Connections...", btnImportConn.getText());
assertEquals("Export Connections...", btnExportConn.getText());
// Check window height: must be taller than previous 540 to accommodate extra buttons
assertTrue(dialog.getHeight() > 540,
"Settings window height (" + dialog.getHeight() + ") must be taller than 540 to accommodate the extra buttons");
// Verify config buttons share container row1, and connection buttons share container row2
JButton btnImport = dialog.getBtnImport();
JButton btnExport = dialog.getBtnExport();
assertNotNull(btnImport);
assertNotNull(btnExport);
Container configRow = btnImport.getParent();
assertNotNull(configRow);
assertSame(configRow, btnExport.getParent(), "btnImport and btnExport must share the same parent row");
Container connRow = btnImportConn.getParent();
assertNotNull(connRow);
assertSame(connRow, btnExportConn.getParent(), "btnImportConn and btnExportConn must share the same parent row");
assertNotSame(configRow, connRow, "Connection buttons must be below and NOT in line with config buttons");
// Verify connRow is positioned below configRow in buttonPanel
Container buttonPanel = configRow.getParent();
assertNotNull(buttonPanel);
assertSame(buttonPanel, connRow.getParent(), "Both rows must be in buttonPanel");
int configRowIdx = -1;
int connRowIdx = -1;
Component[] rows = buttonPanel.getComponents();
for (int i = 0; i < rows.length; i++) {
if (rows[i] == configRow) configRowIdx = i;
if (rows[i] == connRow) connRowIdx = i;
}
assertTrue(configRowIdx >= 0, "configRow must be in buttonPanel");
assertTrue(connRowIdx >= 0, "connRow must be in buttonPanel");
assertTrue(connRowIdx > configRowIdx, "connRow must be below configRow in buttonPanel (" + connRowIdx + " > " + configRowIdx + ")");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("SettingsDialog importConnectionsFile updates savedHostBox and Settings")
public void testSettingsDialogImportConnectionsFileUpdatesDropdownAndSettings() throws IOException {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
File tmpIni = File.createTempFile("settings_dialog_import_test", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
w.write("[Auto Mainframe]\n");
w.write("host = auto.mainframe.org\n");
w.write("port = 992\n");
w.write("autoConnect = true\n");
w.write("useTls = true\n");
}
SettingsDialog dialog = new SettingsDialog(app);
dialog.importConnectionsFile(tmpIni);
// Check savedHostBox updated
JComboBox<SavedHost> box = dialog.getSavedHostBox();
assertNotNull(box);
assertTrue(box.getItemCount() >= 1);
SavedHost sel = (SavedHost) box.getSelectedItem();
assertNotNull(sel);
assertEquals("auto.mainframe.org", sel.getHost());
assertTrue(sel.isAutoConnect());
// Check Settings updated for autoConnect host
assertEquals("auto.mainframe.org", Settings.getAutoConnectHost());
assertEquals(992, Settings.getAutoConnectPort());
assertTrue(Settings.getAutoConnectTls());
// Test exportConnectionsFile
File exportTarget = File.createTempFile("settings_dialog_export_test", ".ini");
exportTarget.deleteOnExit();
dialog.exportConnectionsFile(exportTarget);
assertTrue(exportTarget.exists());
String exported = new String(Files.readAllBytes(exportTarget.toPath()));
assertTrue(exported.contains("auto.mainframe.org"));
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("SettingsDialog importConnectionsFile throws FileNotFoundException for nonexistent file")
public void testImportNonExistentConnectionsFileThrows() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
File nonExistent = new File("/path/to/nonexistent/connections_" + System.currentTimeMillis() + ".ini");
assertThrows(FileNotFoundException.class, () -> {
dialog.importConnectionsFile(nonExistent);
});
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}
@@ -0,0 +1,242 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AutoConnectSettingsAndTerminalCenteringTest {
private List<SavedHost> originalSavedHosts;
private Settings.StartupBehavior originalStartupBehavior;
private String originalAutoConnectHost;
private int originalAutoConnectPort;
@BeforeEach
public void setUp() {
originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts());
originalStartupBehavior = Settings.getStartupBehavior();
originalAutoConnectHost = Settings.getAutoConnectHost();
originalAutoConnectPort = Settings.getAutoConnectPort();
}
@AfterEach
public void tearDown() {
HostStorage.saveAll(originalSavedHosts);
Settings.setStartupBehavior(originalStartupBehavior);
Settings.setAutoConnectHost(originalAutoConnectHost);
Settings.setAutoConnectPort(originalAutoConnectPort);
}
@Test
@DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big")
public void testTerminalPanelCenteringLogic() {
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
panel.setClient(client);
int cols = sb.getDisplayCols(); // 80
int rows = sb.getDisplayRows(); // 24
int gridW = cols * panel.cellWidth; // 800
int gridH = rows * panel.cellHeight; // 480
// 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0
panel.setSize(gridW - 100, gridH - 50);
assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained");
assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained");
// 2. Panel exactly matches grid: offset must be 0
panel.setSize(gridW, gridH);
assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid");
assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid");
// 3. Panel is larger with odd extra (extra = 7)
panel.setSize(gridW + 7, gridH + 11);
int ox = panel.getRenderOffsetX();
int leftSpace = ox;
int rightSpace = (gridW + 7) - (ox + gridW);
assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")");
assertEquals(3, leftSpace);
assertEquals(4, rightSpace);
int oy = panel.getRenderOffsetY();
int topSpace = oy;
int bottomSpace = (gridH + 11) - (oy + gridH);
assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space");
assertEquals(5, topSpace);
assertEquals(6, bottomSpace);
// 4. Panel is larger with even extra (extra = 6)
panel.setSize(gridW + 6, gridH + 8);
assertEquals(3, panel.getRenderOffsetX());
assertEquals(4, panel.getRenderOffsetY());
panel.dispose();
}
@Test
@DisplayName("guardedPack does not resize window when window is too small")
public void testGuardedPackDoesNotResizeWhenTooSmall() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
frame.getContentPane().add(panel);
frame.setSize(400, 300);
frame.addNotify(); // makes frame displayable
// Set size explicitly smaller than preferred size (800+ x 480+)
frame.setSize(400, 300);
Dimension sizeBefore = frame.getSize();
// Run guardedPack
panel.guardedPack();
// Window should not have expanded
Dimension sizeAfter = frame.getSize();
assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small");
assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small");
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {
// Environment without X display
}
}
@Test
@DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host")
public void testSettingsDialogSavedConnectionsDropdown() throws Exception {
SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", "");
List<SavedHost> testHosts = new ArrayList<>();
testHosts.add(host1);
testHosts.add(host2);
HostStorage.saveAll(testHosts);
Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT);
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JComboBox<SavedHost> combo = dialog.getSavedHostBox();
assertNotNull(combo, "savedHostBox should not be null");
assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts");
// Host Alpha was autoConnect=true, so it should be initially selected
SavedHost initialSel = (SavedHost) combo.getSelectedItem();
assertNotNull(initialSel);
assertEquals("id-test-1", initialSel.getId());
// Switch to Host Beta
combo.setSelectedIndex(1);
// Trigger applySettings via reflection
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
applyMethod.setAccessible(true);
applyMethod.invoke(dialog);
// Verify Host Beta is now the auto-connect host
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto, "Auto-connect host should exist");
assertEquals("id-test-2", auto.getId());
assertEquals("Host Beta", auto.getName());
assertEquals("beta.mainframe.test", auto.getHost());
assertEquals(992, auto.getPort());
SavedHost loadedAlpha = HostStorage.findById("id-test-1");
assertNotNull(loadedAlpha);
assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true");
// Verify Settings autoConnectHost/Port synchronized
assertEquals("beta.mainframe.test", Settings.getAutoConnectHost());
assertEquals(992, Settings.getAutoConnectPort());
// Switch startup behavior to SHOW_CONNECT and apply
Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox");
sbField.setAccessible(true);
@SuppressWarnings("unchecked")
JComboBox<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) sbField.get(dialog);
sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT);
applyMethod.invoke(dialog);
assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared");
dialog.dispose();
app.dispose();
} finally {
HostStorage.saveAll(originalSavedHosts);
}
}
@Test
@DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged")
public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
frame.getContentPane().add(panel);
frame.addNotify();
// Pack for Model 4 (43 rows, 80 cols)
panel.packForModel(43, 80);
Dimension model4Size = frame.getSize();
assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows");
assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols");
// Pack for Model 5 (27 rows, 132 cols)
panel.packForModel(27, 132);
Dimension model5Size = frame.getSize();
assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols");
// Set custom size and verify guardedPack leaves the window alone
frame.setSize(600, 400);
Dimension fixedSize = frame.getSize();
panel.guardedPack();
assertEquals(fixedSize.width, frame.getWidth());
assertEquals(fixedSize.height, frame.getHeight());
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {}
}
}
@@ -0,0 +1,153 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.event.InputMethodEvent;
import java.text.AttributedString;
import static org.junit.jupiter.api.Assertions.*;
public class DBCSInputAndClipboardTest {
@Test
@DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSClipboardCopyNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'A';
panel.setClient(client);
// Stream selection covering cells 0..2 (row 0, col 0..2)
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character");
}
@Test
@DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSBlockSelectionNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.setBlockSelectMode(true);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'B';
panel.setClient(client);
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character");
}
@Test
@DisplayName("InputMethodRequests returns cursor bounds and processes committed text")
public void testDBCSInputMethodRequestsAndEvents() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this");
Rectangle loc = panel.getTextLocation(null);
assertNotNull(loc, "Text location must not be null");
assertTrue(loc.width > 0, "Cursor width must be positive");
assertTrue(loc.height > 0, "Cursor height must be positive");
// Simulate committed IME text '\u3000'
AttributedString as = new AttributedString("\u3000");
InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
as.getIterator(), 1, null, null);
panel.inputMethodTextChanged(imeEvent);
ScreenBuffer sb = client.getScreenBuffer();
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals('\u3000', sb.getCell(0).ucs4);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
assertEquals('\u3000', sb.getCell(1).ucs4);
assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS");
}
@Test
@DisplayName("InputProcessor backspace over DBCS character deletes both cells")
public void testInputProcessorDBCSTypingAndBackspace() {
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
InputProcessor input = client.getInputProcessor();
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
sb.setCursorAddress(0);
// Type DBCS character
input.typeCharacter('\u3000');
assertEquals(2, sb.getCursorAddress());
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
// Backspace over DBCS character
input.backspace();
assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells");
assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)");
assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)");
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db);
}
}
@@ -98,4 +98,25 @@ public class TerminalPanelSelectionTest {
// Handled gracefully in headless environments // Handled gracefully in headless environments
} }
} }
@Test
@DisplayName("Mouse press and release on TerminalPanel requests focus")
public void testTerminalPanelMousePressRequestsFocus() {
try {
TerminalPanel panel = new TerminalPanel();
assertTrue(panel.isFocusable());
MouseListener[] mouseListeners = panel.getMouseListeners();
assertTrue(mouseListeners.length > 0);
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, 50, 50, 1, false, MouseEvent.BUTTON1);
mouseListeners[0].mousePressed(pressEvent);
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, 50, 50, 1, false, MouseEvent.BUTTON1);
mouseListeners[0].mouseReleased(releaseEvent);
} catch (HeadlessException ignored) {
}
}
} }
@@ -131,7 +131,18 @@ public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements EC
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0; return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
} }
public short getAttribute() { return GetAttribute(); } public boolean IsDBCS() {
return delegate != null && delegate.isDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsDBCSOnly() {
return delegate != null && delegate.isDBCSOnly();
}
public boolean isDBCSOnly() { return IsDBCSOnly(); }
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; } public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
} }
@@ -511,4 +511,61 @@ public class ECLPS implements ECLConstants, Serializable, Cloneable {
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) { public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
return delegate.GetScreen(buffer, len, row, col, length, plane); return delegate.GetScreen(buffer, len, row, col, length, plane);
} }
// ========== DBCS Character Status ==========
public boolean IsDBCSChar(int pos) {
return delegate != null && delegate.IsDBCSChar(pos);
}
public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); }
public boolean IsDBCSChar(int row, int col) {
return delegate != null && delegate.IsDBCSChar(row, col);
}
public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); }
public boolean IsDBCS1stChar(int pos) {
return delegate != null && delegate.IsDBCS1stChar(pos);
}
public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); }
public boolean IsDBCS1stChar(int row, int col) {
return delegate != null && delegate.IsDBCS1stChar(row, col);
}
public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); }
public boolean IsDBCS2ndChar(int pos) {
return delegate != null && delegate.IsDBCS2ndChar(pos);
}
public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); }
public boolean IsDBCS2ndChar(int row, int col) {
return delegate != null && delegate.IsDBCS2ndChar(row, col);
}
public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); }
public boolean IsSOChar(int pos) {
return delegate != null && delegate.IsSOChar(pos);
}
public boolean isSOChar(int pos) { return IsSOChar(pos); }
public boolean IsSIChar(int pos) {
return delegate != null && delegate.IsSIChar(pos);
}
public boolean isSIChar(int pos) { return IsSIChar(pos); }
public boolean IsSOSIChar(int pos) {
return delegate != null && delegate.IsSOSIChar(pos);
}
public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); }
} }
@@ -69,6 +69,12 @@ public class ConnectionConfig {
private int reconnectMaxRetries = 5; private int reconnectMaxRetries = 5;
private int tcpUserTimeoutMs = 0; private int tcpUserTimeoutMs = 0;
// Host operating system type (TSO, CMS, CICS)
private String hostType = "TSO";
private boolean savedConnection = false;
private String savedProfileId = null;
private String savedProfileName = null;
public ConnectionConfig() {} public ConnectionConfig() {}
public ConnectionConfig(String host, int port) { public ConnectionConfig(String host, int port) {
@@ -312,6 +318,51 @@ public class ConnectionConfig {
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; } public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); } public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
public String getHostType() {
return hostType != null ? hostType : "TSO";
}
public void setHostType(String hostType) {
this.hostType = (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO";
}
public haus.nightmare.lib3270j.ft.FTConfig.HostType getFtHostType() {
if (hostType != null) {
try {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(hostType.trim().toUpperCase());
} catch (Exception ignored) {}
}
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
}
public void setFtHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ftHostType) {
this.hostType = (ftHostType != null) ? ftHostType.name() : "TSO";
}
public boolean isSavedConnection() {
return savedConnection;
}
public void setSavedConnection(boolean savedConnection) {
this.savedConnection = savedConnection;
}
public String getSavedProfileId() {
return savedProfileId;
}
public void setSavedProfileId(String savedProfileId) {
this.savedProfileId = savedProfileId;
}
public String getSavedProfileName() {
return savedProfileName;
}
public void setSavedProfileName(String savedProfileName) {
this.savedProfileName = savedProfileName;
}
/** /**
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"), * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"), * plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
@@ -11,6 +11,7 @@ import haus.nightmare.lib3270j.telnet.TelnetFSM;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.logging.Level;
/** /**
* Main API entry point for lib3270j. * Main API entry point for lib3270j.
@@ -46,6 +47,7 @@ public class Telnet3270Client {
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false); private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
private volatile Thread reconnectThread; private volatile Thread reconnectThread;
private volatile boolean intentionallyDisconnected = false;
public Telnet3270Client(ConnectionConfig config) { public Telnet3270Client(ConnectionConfig config) {
this.config = config; this.config = config;
@@ -130,6 +132,7 @@ public class Telnet3270Client {
* then returns while telnet/TN3270E negotiation continues asynchronously. * then returns while telnet/TN3270E negotiation continues asynchronously.
*/ */
public void connect() throws IOException { public void connect() throws IOException {
intentionallyDisconnected = false;
log.info("Connecting to " + config.getHost() + ":" + config.getPort() + log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
" model=" + config.getModel()); " model=" + config.getModel());
@@ -193,8 +196,16 @@ public class Telnet3270Client {
* Disconnect from the host. * Disconnect from the host.
*/ */
public void disconnect() { public void disconnect() {
intentionallyDisconnected = true;
cancelAutoReconnect(); cancelAutoReconnect();
if (connection != null) { if (connection != null) {
try {
if (fsm != null) {
fsm.sendTelnetDisconnect();
}
} catch (Exception e) {
log.log(Level.FINE, "Error during Telnet disconnect negotiation", e);
}
connection.disconnect(); connection.disconnect();
connection = null; connection = null;
} }
@@ -203,14 +214,20 @@ public class Telnet3270Client {
public void cancelAutoReconnect() { public void cancelAutoReconnect() {
reconnecting.set(false); reconnecting.set(false);
if (reconnectThread != null) { Thread t = reconnectThread;
reconnectThread.interrupt(); if (t != null) {
t.interrupt();
try {
t.join(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
reconnectThread = null; reconnectThread = null;
} }
} }
public void initiateAutoReconnect() { public void initiateAutoReconnect() {
if (config == null || !config.isAutoReconnect()) { if (intentionallyDisconnected || config == null || !config.isAutoReconnect()) {
return; return;
} }
if (!reconnecting.compareAndSet(false, true)) { if (!reconnecting.compareAndSet(false, true)) {
@@ -27,11 +27,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int[] sbcsMapping, String nioCharsetName) { int[] sbcsMapping, String nioCharsetName) {
super(id, description, cpgid, cgcsgid, sbcsMapping); super(id, description, cpgid, cgcsgid, sbcsMapping);
if (nioCharsetName != null) { if (nioCharsetName != null) {
try { for (String candidate : nioCharsetName.split(",")) {
if (Charset.isSupported(nioCharsetName)) { candidate = candidate.trim();
this.nioCharset = Charset.forName(nioCharsetName); try {
} if (Charset.isSupported(candidate)) {
} catch (Exception ignored) {} this.nioCharset = Charset.forName(candidate);
break;
}
} catch (Exception ignored) {}
}
} }
// Initialize ideographic space (0x4040 <-> \u3000) // Initialize ideographic space (0x4040 <-> \u3000)
registerDbcsPair(0x4040, '\u3000'); registerDbcsPair(0x4040, '\u3000');
@@ -250,4 +254,78 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
public byte[] utf8WithSOSIToEbcdic(String s) { public byte[] utf8WithSOSIToEbcdic(String s) {
return stringToEbcdic(s, true); return stringToEbcdic(s, true);
} }
/**
* Double-byte character set Graphic Character Set Global ID (DBGID/CGCSGID) matching HoD DBGIDTable.
*/
public int getDbCgcsgid() {
switch (getCpgid()) {
case 930:
case 939:
case 290:
return 370; // 0x0172
case 933:
return 934; // 0x03A6
case 935:
return 937; // 0x03A9
case 937:
return 935; // 0x03A7
case 1390:
case 1399:
case 1364:
case 1371:
case 1388:
return 65535; // 0xFFFF
default:
return 370;
}
}
/**
* Double-byte character set Code Page Global ID (DBCS CPGID) matching HoD DBGIDTable.
*/
public int getDbCpgid() {
switch (getCpgid()) {
case 930:
case 939:
case 290:
case 1390:
case 1399:
return 300; // 0x012C
case 933:
case 1364:
return 834; // 0x0342
case 935:
case 1388:
return 837; // 0x0345
case 937:
case 1371:
return 835; // 0x0343
default:
return 300;
}
}
/**
* Double-byte character set section start byte matching HoD DBCSSectTable.
*/
public byte getDbcsSectionStart() {
return 0x41;
}
/**
* Double-byte character set section end byte matching HoD DBCSSectTable.
*/
public byte getDbcsSectionEnd() {
switch (getCpgid()) {
case 290:
case 930:
case 935:
case 939:
return 0x7F;
default:
return (byte) 0xFE;
}
}
} }
@@ -311,6 +311,12 @@ public class CodePageRegistry {
register(new Cp1388()); register(new Cp1388());
register(new Cp1371()); register(new Cp1371());
// Japanese Katakana & Latin SBCS / DBCS Euro
register(new Cp290());
register(new Cp1027());
register(new Cp1390());
register(new Cp1399());
// Setup Extended Aliases // Setup Extended Aliases
addAlias("ar", "420"); addAlias("ar", "420");
addAlias("arabic", "420"); addAlias("arabic", "420");
@@ -360,10 +366,12 @@ public class CodePageRegistry {
addAlias("zh-tw-ext", "1371"); addAlias("zh-tw-ext", "1371");
// HoD Converter aliases mapping all HoD converter names to CodePage / Charset // HoD Converter aliases mapping all HoD converter names to CodePage / Charset
addAlias("1390", "930"); addAlias("1390", "1390");
addAlias("1390jis2004", "930"); addAlias("1390jis2004", "1390");
addAlias("1399", "939"); addAlias("1399", "1399");
addAlias("1399jis2004", "939"); addAlias("1399jis2004", "1399");
addAlias("290", "290");
addAlias("1027", "1027");
addAlias("937macau", "937"); addAlias("937macau", "937");
addAlias("1364", "933"); addAlias("1364", "933");
addAlias("1379", "937"); addAlias("1379", "937");
@@ -0,0 +1,43 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1027 (Japanese Latin Extended SBCS).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp939.singleByteToChar.
* CCSID / CPGID: 1027, GCSGID: 1172.
*/
public class Cp1027 extends AbstractCodePage {
public static final String ID = "1027";
public static final String DESCRIPTION = "Japanese Latin Extended - EBCDIC";
public static final int CPGID = 1027;
public static final int GCSGID = 1172;
private static final String SBCS_STR =
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
" \ufffd\uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\u00a2.<(+|" +
"&\uff69\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\uff70\uff71!$*);\u00ac" +
"-/\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\ufffd,%_>?" +
"\uff7a\uff7b\uff7c\uff7d\uff7e\uff7f\uff80\uff81\uff82`:#@'=\"" +
"\ufffdabcdefghi\uff83\uff84\uff85\uff86\uff87\uff88" +
"\ufffdjklmnopqr\uff89\uff8a\uff8b\uff8c\uff8d\uff8e" +
"\u203e~stuvwxyz\uff8f\uff90\uff91[\uff92\uff93^" +
"\u00a3\u00a5\uff94\uff95\uff96\uff97\uff98\uff99\uff9a\uff9b\uff9c\uff9d]\uff9e\uff9f" +
"{ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"}JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"\\\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
public static final int[] MAPPING = new int[256];
static {
for (int i = 0; i < 256; i++) {
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
}
}
public Cp1027() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
}
}
@@ -0,0 +1,23 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1390 (Japanese Katakana Extended Mixed DBCS with Euro support: host SBCS CP290+Euro / DBCS CP16684).
* CCSID / CPGID: 1390, GCSGID: 1172.
*/
public class Cp1390 extends AbstractDBCSCodePage {
public static final String ID = "1390";
public static final String DESCRIPTION = "Japanese Katakana Extended Mixed DBCS (with Euro)";
public static final int CPGID = 1390;
public static final int GCSGID = 1172;
public static final int[] MAPPING = Cp290.MAPPING.clone();
static {
// Euro symbol at 0xE1
MAPPING[0xE1] = 0x20AC;
}
public Cp1390() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1390,IBM1390,Cp1390,x-IBM930,IBM930,Cp930");
}
}
@@ -0,0 +1,23 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1399 (Japanese Latin Extended Mixed DBCS with Euro support: host SBCS CP1027+Euro / DBCS CP16684).
* CCSID / CPGID: 1399, GCSGID: 1172.
*/
public class Cp1399 extends AbstractDBCSCodePage {
public static final String ID = "1399";
public static final String DESCRIPTION = "Japanese Latin Extended Mixed DBCS (with Euro)";
public static final int CPGID = 1399;
public static final int GCSGID = 1172;
public static final int[] MAPPING = Cp1027.MAPPING.clone();
static {
// Euro symbol at 0xE1
MAPPING[0xE1] = 0x20AC;
}
public Cp1399() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1399,IBM1399,Cp1399,x-IBM939,IBM939,Cp939");
}
}
@@ -0,0 +1,43 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 290 (Japanese Katakana Extended SBCS).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp290.
* CCSID / CPGID: 290, GCSGID: 1172.
*/
public class Cp290 extends AbstractCodePage {
public static final String ID = "290";
public static final String DESCRIPTION = "Japanese Katakana Extended - EBCDIC";
public static final int CPGID = 290;
public static final int GCSGID = 1172;
private static final String SBCS_STR =
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
" \uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\uff69\u00a3.<(+|" +
"&\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\ufffd\uff70\ufffd!\u00a5*);\u00ac" +
"-/\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ufffd,%_>?" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000:#@'=\"" +
"\u0000\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\uff7a\u0000\uff7b\uff7c\uff7d\uff7e" +
"\uff7f\uff80\uff81\uff82\uff83\uff84\uff85\uff86\uff87\uff88\uff89\u0000\ufffd\uff8a\uff8b\uff8c" +
"\u0000\u203e\uff8d\uff8e\uff8f\uff90\uff91\uff92\uff93\uff94\uff95\u0000\uff96\uff97\uff98\uff99" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\uff9a\uff9b\uff9c\uff9d\uff9e\uff9f" +
"\u0000ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"\u0000JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"$\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
public static final int[] MAPPING = new int[256];
static {
for (int i = 0; i < 256; i++) {
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
}
}
public Cp290() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
}
}
@@ -12,6 +12,6 @@ public class Cp930 extends AbstractDBCSCodePage {
public static final int GCSGID = 1172; public static final int GCSGID = 1172;
public Cp930() { public Cp930() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM930"); super(ID, DESCRIPTION, CPGID, GCSGID, Cp290.MAPPING, "x-IBM930,IBM930,Cp930");
} }
} }
@@ -12,6 +12,6 @@ public class Cp939 extends AbstractDBCSCodePage {
public static final int GCSGID = 1172; public static final int GCSGID = 1172;
public Cp939() { public Cp939() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM939"); super(ID, DESCRIPTION, CPGID, GCSGID, Cp1027.MAPPING, "x-IBM939,IBM939,Cp939");
} }
} }
@@ -90,6 +90,35 @@ public class EbcdicTranslator {
return isDBCSCodePage(); return isDBCSCodePage();
} }
public synchronized int getDbCgcsgid() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbCgcsgid();
}
return 370;
}
public synchronized int getDbCpgid() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbCpgid();
}
return 300;
}
public synchronized byte getDbcsSectionStart() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionStart();
}
return 0x41;
}
public synchronized byte getDbcsSectionEnd() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionEnd();
}
return 0x7F;
}
// ========================================================================= // =========================================================================
// Custom Character Translation Overrides // Custom Character Translation Overrides
// ========================================================================= // =========================================================================
@@ -53,6 +53,7 @@ public class DataStreamProcessor {
private boolean unlockPending = false; private boolean unlockPending = false;
private boolean unlockSysPending = false; private boolean unlockSysPending = false;
private boolean rcvdRead = false; private boolean rcvdRead = false;
private boolean qreplyPending = false;
// Modal SA (set attribute) character attributes // Modal SA (set attribute) character attributes
private byte currentFg = 0; private byte currentFg = 0;
@@ -60,6 +61,8 @@ public class DataStreamProcessor {
private byte currentGr = 0; private byte currentGr = 0;
private byte currentCs = 0; private byte currentCs = 0;
private byte currentOl = 0; private byte currentOl = 0;
private byte currentTr = 0;
private boolean inDBCS = false;
/** Functional interface for sending output back through the telnet stack. */ /** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface @FunctionalInterface
@@ -181,11 +184,13 @@ public class DataStreamProcessor {
switch (cmd) { switch (cmd) {
case CMD_W: case CMD_W:
case SNA_CMD_W: case SNA_CMD_W:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
processWrite(data, offset, length, false); processWrite(data, offset, length, false);
break; break;
case CMD_EW: case CMD_EW:
case SNA_CMD_EW: case SNA_CMD_EW:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) { if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE: clearing screen (default size)"); log.fine(">>> ERASE/WRITE: clearing screen (default size)");
@@ -206,6 +211,7 @@ public class DataStreamProcessor {
break; break;
case CMD_EWA: case CMD_EWA:
case SNA_CMD_EWA: case SNA_CMD_EWA:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) { if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
@@ -234,7 +240,12 @@ public class DataStreamProcessor {
case SNA_CMD_RM: case SNA_CMD_RM:
rcvdRead = true; rcvdRead = true;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
processReadModified(false); if (qreplyPending) {
qreplyPending = false;
sendAllQueryReplies();
} else {
processReadModified(false);
}
break; break;
case CMD_RMA: case CMD_RMA:
case SNA_CMD_RMA: case SNA_CMD_RMA:
@@ -244,7 +255,11 @@ public class DataStreamProcessor {
break; break;
case CMD_EAU: case CMD_EAU:
case SNA_CMD_EAU: case SNA_CMD_EAU:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
log.info(">>> EAU: erasing all unprotected fields"); log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected(); screen.eraseAllUnprotected();
currentFg = 0; currentFg = 0;
@@ -252,6 +267,7 @@ public class DataStreamProcessor {
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0; currentOl = 0;
currentTr = 0;
break; break;
case CMD_WSF: case CMD_WSF:
case SNA_CMD_WSF: case SNA_CMD_WSF:
@@ -277,6 +293,7 @@ public class DataStreamProcessor {
cmd == CMD_EWA || cmd == SNA_CMD_EWA); cmd == CMD_EWA || cmd == SNA_CMD_EWA);
if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) { if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) {
inputProcessor.setKeyboardLocked(false); inputProcessor.setKeyboardLocked(false);
inputProcessor.setLastAid(AID_NO);
} }
// Debug: dump non-empty screen lines // Debug: dump non-empty screen lines
@@ -376,10 +393,14 @@ public class DataStreamProcessor {
if (kbdRestore) { if (kbdRestore) {
unlockPending = true; unlockPending = true;
unlockSysPending = true; unlockSysPending = true;
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
} }
if (!contentionResolution && kbdRestore && inputProcessor != null) { if (!contentionResolution && kbdRestore && inputProcessor != null) {
inputProcessor.setKeyboardLocked(false); inputProcessor.setKeyboardLocked(false);
inputProcessor.setLastAid(AID_NO);
} }
if (resetMdt) { if (resetMdt) {
@@ -395,6 +416,8 @@ public class DataStreamProcessor {
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0; currentOl = 0;
currentTr = 0;
inDBCS = false;
} }
if (eraseFirst) { if (eraseFirst) {
@@ -405,6 +428,8 @@ public class DataStreamProcessor {
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0; currentOl = 0;
currentTr = 0;
inDBCS = false;
} }
// Process orders and data starting at byte 2 // Process orders and data starting at byte 2
@@ -435,6 +460,7 @@ public class DataStreamProcessor {
} }
case ORDER_SF: { // Start Field case ORDER_SF: { // Start Field
inDBCS = false;
if (pos + 1 >= end) { if (pos + 1 >= end) {
pos = end; pos = end;
break; break;
@@ -456,6 +482,7 @@ public class DataStreamProcessor {
} }
case ORDER_SFE: { // Start Field Extended case ORDER_SFE: { // Start Field Extended
inDBCS = false;
if (pos + 1 >= end) { if (pos + 1 >= end) {
pos = end; pos = end;
break; break;
@@ -502,6 +529,7 @@ public class DataStreamProcessor {
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0; currentOl = 0;
currentTr = 0;
break; break;
case XA_FOREGROUND: case XA_FOREGROUND:
currentFg = (byte) attrValue; currentFg = (byte) attrValue;
@@ -530,6 +558,9 @@ public class DataStreamProcessor {
case 0x84: case 0x84:
currentOl = (byte) attrValue; currentOl = (byte) attrValue;
break; break;
case XA_TRANSPARENCY:
currentTr = (byte) attrValue;
break;
} }
pos += 3; pos += 3;
lastWasOrder = true; lastWasOrder = true;
@@ -591,6 +622,44 @@ public class DataStreamProcessor {
if (toAddr >= size) if (toAddr >= size)
toAddr = toAddr % size; toAddr = toAddr % size;
boolean isDbcsSession = (translator != null && translator.isDBCS());
if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS) && pos + 4 < end) {
int fillChar2 = data[pos + 4] & 0xFF;
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(fillChar, fillChar2) : '?';
do {
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) fillChar;
ea1.ucs4 = uni;
ea1.fg = currentFg;
ea1.bg = currentBg;
ea1.gr = (byte) currentGr;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ea1.ol = currentOl;
ea1.tr = currentTr;
baddr = (baddr + 1) % size;
if (baddr == toAddr) break;
ExtendedAttribute ea2 = screen.getCell(baddr);
ea2.fa = 0;
ea2.ec = (byte) fillChar2;
ea2.ucs4 = uni;
ea2.fg = currentFg;
ea2.bg = currentBg;
ea2.gr = (byte) currentGr;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
ea2.ol = currentOl;
ea2.tr = currentTr;
baddr = (baddr + 1) % size;
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
pos += 5;
lastWasOrder = true;
break;
}
// Handle GE (graphic escape) prefix // Handle GE (graphic escape) prefix
byte fillCs = currentCs; byte fillCs = currentCs;
char fillUcs4 = 0; char fillUcs4 = 0;
@@ -614,7 +683,9 @@ public class DataStreamProcessor {
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.cs = fillCs; ea.cs = fillCs;
ea.db = ExtendedAttribute.DB_NONE;
ea.ol = currentOl; ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
} while (baddr != toAddr); } while (baddr != toAddr);
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -644,6 +715,7 @@ public class DataStreamProcessor {
ea.gr = 0; ea.gr = 0;
ea.cs = 0; ea.cs = 0;
ea.ol = 0; ea.ol = 0;
ea.tr = 0;
} }
} }
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
@@ -669,6 +741,7 @@ public class DataStreamProcessor {
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.ol = currentOl; ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
pos += 2; pos += 2;
@@ -677,6 +750,98 @@ public class DataStreamProcessor {
} }
default: { default: {
boolean isDbcsSession = (translator != null && translator.isDBCS());
if (isDbcsSession && b == 0x0E) { // Shift-Out (enter DBCS mode)
inDBCS = true;
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) 0x0E;
ea.ucs4 = ' ';
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_SO;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
} else if (isDbcsSession && b == 0x0F) { // Shift-In (exit DBCS mode)
inDBCS = false;
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) 0x0F;
ea.ucs4 = ' ';
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_SI;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
} else if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS)) {
if (pos + 1 < end) {
int b2 = data[pos + 1] & 0xFF;
if (b2 == 0x0F) { // Orphaned single byte before SI
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) b;
ea.ucs4 = translator.ebcdicToUnicode(b);
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = currentCs;
ea.ol = currentOl;
ea.tr = currentTr;
ea.db = ExtendedAttribute.DB_NONE;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
}
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(b, b2) : '?';
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) b;
ea1.ucs4 = uni;
ea1.fg = currentFg;
ea1.bg = currentBg;
ea1.gr = (byte) currentGr;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ea1.ol = currentOl;
ea1.tr = currentTr;
int nextAddr = (baddr + 1) % size;
ExtendedAttribute ea2 = screen.getCell(nextAddr);
ea2.fa = 0;
ea2.ec = (byte) b2;
ea2.ucs4 = uni;
ea2.fg = currentFg;
ea2.bg = currentBg;
ea2.gr = (byte) currentGr;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
ea2.ol = currentOl;
ea2.tr = currentTr;
baddr = (baddr + 2) % size;
screen.setBufferAddress(baddr);
pos += 2;
lastWasOrder = false;
break;
}
}
// Regular data byte (EBCDIC character) // Regular data byte (EBCDIC character)
// Format control codes display as nulls/blanks per 3270 spec // Format control codes display as nulls/blanks per 3270 spec
boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF || boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF ||
@@ -688,14 +853,18 @@ public class DataStreamProcessor {
if (isFCOrder) { if (isFCOrder) {
ea.ec = 0; // Display as null/blank ea.ec = 0; // Display as null/blank
ea.ucs4 = 0;
} else { } else {
ea.ec = (byte) b; ea.ec = (byte) b;
ea.ucs4 = (translator != null) ? translator.ebcdicToUnicode(b) : (char) b;
} }
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.cs = currentCs; ea.cs = currentCs;
ea.db = ExtendedAttribute.DB_NONE;
ea.ol = currentOl; ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -773,13 +942,26 @@ public class DataStreamProcessor {
// ========== Read Buffer ========== // ========== Read Buffer ==========
public void processReadBuffer() { public void processReadBuffer() {
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
processReadBuffer(aid);
}
public void processReadBuffer(int aid) {
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
byte[] data = inputProcessor.buildReadBufferInboundData(aid);
if (outputSender != null) {
outputSender.send3270Data(data);
}
return;
}
outputPos = 0; outputPos = 0;
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) { if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO); inputProcessor.setLastAid(AID_NO);
} }
@@ -796,7 +978,7 @@ public class DataStreamProcessor {
// Buffer contents depending on Reply Mode // Buffer contents depending on Reply Mode
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) { if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
// Extended Field or Character Mode // Extended Field or Character Mode
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0; byte curFg = 0, curBg = 0, curGr = 0, curCs = 0, curTr = 0;
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i); ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) { if (ea.isFieldAttribute()) {
@@ -807,6 +989,7 @@ public class DataStreamProcessor {
if (ea.gr != 0) count++; if (ea.gr != 0) count++;
if (ea.cs != 0) count++; if (ea.cs != 0) count++;
if (ea.ol != 0) count++; if (ea.ol != 0) count++;
if (ea.tr != 0) count++;
outputWrite(ORDER_SFE); outputWrite(ORDER_SFE);
outputWrite(count); outputWrite(count);
@@ -825,6 +1008,7 @@ public class DataStreamProcessor {
} }
if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); } if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); }
if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); } if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); }
if (ea.tr != 0) { outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF); }
} else { } else {
if (mode == SF_SRM_CHAR) { if (mode == SF_SRM_CHAR) {
// In character mode, output SA if character attributes differ // In character mode, output SA if character attributes differ
@@ -850,6 +1034,10 @@ public class DataStreamProcessor {
outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF);
curCs = ea.cs; curCs = ea.cs;
} }
if (ea.tr != curTr) {
outputWrite(ORDER_SA); outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF);
curTr = ea.tr;
}
} }
outputWrite(ea.ec & 0xFF); outputWrite(ea.ec & 0xFF);
} }
@@ -873,12 +1061,18 @@ public class DataStreamProcessor {
// ========== Read Modified ========== // ========== Read Modified ==========
public void processReadModified(boolean all) { public void processReadModified(boolean all) {
int aid = (inputProcessor != null && inputProcessor.isKeyboardLocked() && inputProcessor.getLastAid() != 0 && inputProcessor.getLastAid() != AID_NO)
? inputProcessor.getLastAid()
: AID_NO;
processReadModified(aid, all);
}
public void processReadModified(int aid, boolean all) {
if (ftDft != null && ftDft.readModified()) { if (ftDft != null && ftDft.readModified()) {
return; return;
} }
if (inputProcessor != null) { if (inputProcessor != null) {
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
inputProcessor.setLastAid(AID_NO); inputProcessor.setLastAid(AID_NO);
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all); byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
if (outputSender != null) { if (outputSender != null) {
@@ -889,9 +1083,6 @@ public class DataStreamProcessor {
outputPos = 0; outputPos = 0;
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) { if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO); inputProcessor.setLastAid(AID_NO);
} }
@@ -1054,9 +1245,11 @@ public class DataStreamProcessor {
log.info("ReadPartition Query — sending base query replies"); log.info("ReadPartition Query — sending base query replies");
graphicsPlane.clear(); graphicsPlane.clear();
gocaDecoder.resetDefaults(); gocaDecoder.resetDefaults();
qreplyPending = true;
sendAllQueryReplies(); sendAllQueryReplies();
break; break;
case SF_RP_QLIST: case SF_RP_QLIST:
qreplyPending = true;
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));
@@ -1079,14 +1272,16 @@ public class DataStreamProcessor {
sendAllQueryReplies(); sendAllQueryReplies();
} }
break; break;
case CMD_RMA:
case SNA_CMD_RMA: case SNA_CMD_RMA:
processReadModified(true); processReadModified(AID_QREPLY, true);
break; break;
case SNA_CMD_RB: case SNA_CMD_RB:
processReadBuffer(); processReadBuffer(AID_QREPLY);
break; break;
case CMD_RM:
case SNA_CMD_RM: case SNA_CMD_RM:
processReadModified(false); processReadModified(AID_QREPLY, false);
break; break;
default: default:
log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type)); log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type));
@@ -1517,6 +1712,14 @@ public class DataStreamProcessor {
processReadModified(true); processReadModified(true);
} }
public boolean isQreplyPending() {
return qreplyPending;
}
public void setQreplyPending(boolean qreplyPending) {
this.qreplyPending = qreplyPending;
}
public void processWriteStructuredField(byte[] data) { public void processWriteStructuredField(byte[] data) {
if (data != null && data.length > 0) { if (data != null && data.length > 0) {
processWriteStructuredField(data, 0, data.length); processWriteStructuredField(data, 0, data.length);
@@ -129,6 +129,7 @@ public class QueryReplyBuilder {
QR_DDM, // 0x95 - Distributed Data Management (file transfer) QR_DDM, // 0x95 - Distributed Data Management (file transfer)
QR_AUXDA, // 0x99 - Auxiliary Devices QR_AUXDA, // 0x99 - Auxiliary Devices
QR_IMP_PART, // 0xa6 - Implicit Partition Sizes QR_IMP_PART, // 0xa6 - Implicit Partition Sizes
QR_TRANSPARENCY, // 0xa8 - Background Transparency
}; };
// Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING // Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING
@@ -218,6 +219,9 @@ public class QueryReplyBuilder {
// Implicit Partition (0xA6) // Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
// Background Transparency (0xA8)
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")"); log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray(); return out.toByteArray();
} }
@@ -245,9 +249,9 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0 appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1 appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2 appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
@@ -317,11 +321,7 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break; break;
case QR_TRANSPARENCY: // 0xA8 case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break; break;
case QR_SEGMENT: // 0xB0 case QR_SEGMENT: // 0xB0
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
@@ -390,10 +390,17 @@ public class QueryReplyBuilder {
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()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
boolean wroteDbcs = false;
for (int code : codes) { for (int code : codes) {
if (isDbcs && !wroteDbcs && code == QR_DDM) {
out.write(QR_OUTLINING);
out.write(QR_DBCS_ASIA);
wroteDbcs = true;
}
out.write(code); out.write(code);
if (isDbcs && code == QR_OUTLINING) { if (isDbcs && code == QR_OUTLINING && !wroteDbcs) {
out.write(QR_DBCS_ASIA); // 0x91 out.write(QR_DBCS_ASIA); // 0x91
wroteDbcs = true;
} }
} }
return out.toByteArray(); return out.toByteArray();
@@ -441,7 +448,7 @@ public class QueryReplyBuilder {
public int getCharWidth(int rows) { public int getCharWidth(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12; return 12; // 0x0C
} }
return SW_3279_2; // 9 return SW_3279_2; // 9
} }
@@ -452,6 +459,9 @@ public class QueryReplyBuilder {
} }
public int getCharHeight(int rows) { public int getCharHeight(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 24; // 0x18
}
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
@@ -477,9 +487,54 @@ public class QueryReplyBuilder {
int cgcsgid = 0x02B9; int cgcsgid = 0x02B9;
int cpgid = 0x0025; int cpgid = 0x0025;
boolean isDbcs = false;
if (screen != null && screen.getTranslator() != null) { if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid(); cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid(); cpgid = screen.getTranslator().getCpgid();
isDbcs = screen.getTranslator().isDBCS();
}
if (isDbcs) {
int dbCgcsgid = screen.getTranslator().getDbCgcsgid();
int dbCpgid = screen.getTranslator().getDbCpgid();
byte secStart = screen.getTranslator().getDbcsSectionStart();
byte secEnd = screen.getTranslator().getDbcsSectionEnd();
// 42-byte DBCS Character Sets QR payload (total 46 bytes structured field, matching HoD DS3270.java xxxqrcs)
ByteArrayOutputStream out = new ByteArrayOutputStream(42);
out.write(0x8E); // flags: GE | DBCS / 2-byte charsets (0x8E)
out.write(0x00); // flags 2
out.write(charW); // SDW (12)
out.write(charH); // SDH (24)
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); // 4 reserved bytes
out.write(0x0B); // DL = 11 bytes per descriptor
// Descriptor 1: Base SBCS (11 bytes)
out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
// Descriptor 2: DBCS Set 1 (11 bytes)
out.write(0x01); // Set ID = 1
out.write(0x00); // Flags
out.write(0x80); // Local ID = 0x80 (DBCS)
out.write((charW * 2) & 0xFF); // Double cell width (24)
out.write(charH & 0xFF); // Cell height (24)
out.write(secStart); // Section start (e.g. 0x41)
out.write(secEnd); // Section end (e.g. 0xFE or 0x7F)
out.write((dbCgcsgid >> 8) & 0xFF); out.write(dbCgcsgid & 0xFF);
out.write((dbCpgid >> 8) & 0xFF); out.write(dbCpgid & 0xFF);
// Descriptor 3: APL / Text (11 bytes)
out.write(0x01); out.write(0x00); out.write(0xF1);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x03); out.write(0xC3); // CGCSGID 963
out.write(0x01); out.write(0x36); // CPGID 310
return out.toByteArray();
} }
if (graphicsMode.isProgrammedSymbolsEnabled()) { if (graphicsMode.isProgrammedSymbolsEnabled()) {
@@ -531,6 +586,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
public byte[] buildColor() { public byte[] buildColor() {
return STATIC_QR_COLOR.clone(); return STATIC_QR_COLOR.clone();
} }
@@ -5,13 +5,27 @@ package haus.nightmare.lib3270j.ecl;
*/ */
public interface ECLConstants { public interface ECLConstants {
// Presentation space logical planes // Presentation space logical planes (Bitmask & ordinal definitions matching IBM Host On-Demand)
int PLANE_TEXT = 1; // Unicode/EBCDIC characters int PLANE_TEXT = 1; // Unicode/EBCDIC characters
int PLANE_COLOR = 2; // Color attributes int PLANE_COLOR = 2; // Color attributes
int PLANE_HILITE = 3; // Extended highlighting int PLANE_HILITE = 3; // Extended highlighting
int PLANE_EXTENDED = 4; // Extended character sets / outlining int PLANE_EXTENDED = 4; // Extended character sets / outlining
int PLANE_FIELD = 5; // Field attribute definition bytes int PLANE_FIELD = 5; // Field attribute definition bytes
int PLANE_DBCS = 6; // Double-byte character plane int PLANE_DBCS = 6; // Double-byte character plane (lib3270j convention)
int PLANE_GRID = 8; // Grid lines / outlining plane
// Canonical IBM Host On-Demand ECLConstants plane bitmasks
int TEXT_PLANE = 1;
int COLOR_PLANE = 2;
int DBCS_PLANE = 4;
int GRID_PLANE = 8;
int FIELD_PLANE = 16;
int EXFIELD_PLANE = 32;
int NLS_PLANE = 64;
int HOST_PLANE = 128;
int RAWTEXT_PLANE = 256;
int COLOR_ATTRIBUTES_PLANE = 512;
int ALL_PLANES = 319;
// Color definitions (IBM 3279 standard 16-color palette) // Color definitions (IBM 3279 standard 16-color palette)
char COLOR_NEUTRAL_BLACK = 0; char COLOR_NEUTRAL_BLACK = 0;
@@ -295,6 +295,54 @@ public class ECLField {
} }
public void Erase() { erase(); } public void Erase() { erase(); }
/**
* Check whether this field contains any DBCS characters or SO/SI delimiters.
*/
public boolean isDBCS() {
if (ps == null || length <= 0) return false;
ScreenBuffer sb = ps.getScreenBuffer();
if (sb == null) return false;
int size = sb.getRows() * sb.getCols();
if (size <= 0) return false;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
if (ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI)) {
return true;
}
}
return false;
}
public boolean IsDBCS() { return isDBCS(); }
/**
* Check whether this field contains only DBCS characters (excluding nulls / spaces).
*/
public boolean isDBCSOnly() {
if (ps == null || length <= 0) return false;
ScreenBuffer sb = ps.getScreenBuffer();
if (sb == null) return false;
int size = sb.getRows() * sb.getCols();
if (size <= 0) return false;
boolean hasDBCS = false;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
if (ea != null) {
if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
hasDBCS = true;
} else if (ea.ec != 0 && ea.ec != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
return false;
}
}
}
return hasDBCS;
}
public boolean IsDBCSOnly() { return isDBCSOnly(); }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -25,9 +25,22 @@ public class ECLFieldList extends Vector<ECLField> {
refresh(); refresh();
} }
public ECLFieldList(ECLPS ps) {
this(ps, ps != null ? ps.getScreenBuffer() : null);
}
public ECLField getField(int index) {
return (index >= 0 && index < size()) ? get(index) : null;
}
public ECLField GetField(int index) {
return getField(index);
}
public ECLPS getPS() { return ps; } public ECLPS getPS() { return ps; }
public ScreenBuffer getScreenBuffer() { return screen; } public ScreenBuffer getScreenBuffer() { return screen; }
/** /**
* Refresh the field list by scanning the presentation space buffer. * Refresh the field list by scanning the presentation space buffer.
*/ */
@@ -264,10 +264,12 @@ public class ECLPS implements ECLConstants {
case PLANE_TEXT: case PLANE_TEXT:
if (ea.isFieldAttribute()) { if (ea.isFieldAttribute()) {
destBuffer[i] = ' '; destBuffer[i] = ' ';
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = ' ';
} else if (ea.ucs4 != 0) { } else if (ea.ucs4 != 0) {
destBuffer[i] = (char) ea.ucs4; destBuffer[i] = (char) ea.ucs4;
} else if (ea.ec != 0) { } else if (ea.ec != 0) {
destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF); destBuffer[i] = (translator != null) ? translator.ebcdicToUnicode(ea.ec & 0xFF) : (char) (ea.ec & 0xFF);
} else { } else {
destBuffer[i] = ' '; destBuffer[i] = ' ';
} }
@@ -278,12 +280,43 @@ public class ECLPS implements ECLConstants {
case PLANE_HILITE: case PLANE_HILITE:
destBuffer[i] = (char) (ea.gr & 0xFF); destBuffer[i] = (char) (ea.gr & 0xFF);
break; break;
case PLANE_EXTENDED: case DBCS_PLANE: // 4 (HoD DBCS_PLANE) or PLANE_EXTENDED for SBCS
destBuffer[i] = (char) (ea.cs & 0xFF); if (translator != null && translator.isDBCS()) {
if (ea.db == ExtendedAttribute.DB_LEFT) {
destBuffer[i] = (char) 0xC0; // DB_DBHIGH (first half of DBCS)
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
destBuffer[i] = (char) 0x80; // DB_DBCS (second half of DBCS)
} else if (ea.db == ExtendedAttribute.DB_SO) {
destBuffer[i] = (char) 0x20; // DB_SO
} else if (ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = (char) 0x10; // DB_SI
} else {
destBuffer[i] = 0;
}
} else {
destBuffer[i] = (char) (ea.cs & 0xFF);
}
break; break;
case PLANE_FIELD: case PLANE_FIELD:
case FIELD_PLANE: // 16
destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0; destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0;
break; break;
case PLANE_DBCS: // 6
if (ea.db == ExtendedAttribute.DB_LEFT) {
destBuffer[i] = (char) 0xC0;
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
destBuffer[i] = (char) 0x80;
} else if (ea.db == ExtendedAttribute.DB_SO) {
destBuffer[i] = (char) 0x20;
} else if (ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = (char) 0x10;
} else {
destBuffer[i] = 0;
}
break;
case GRID_PLANE: // 8 (HoD GRID_PLANE / Outlining)
destBuffer[i] = (char) (ea.ol & 0xFF);
break;
default: default:
destBuffer[i] = ' '; destBuffer[i] = ' ';
break; break;
@@ -292,6 +325,107 @@ public class ECLPS implements ECLConstants {
return copyLen; return copyLen;
} }
public synchronized boolean IsDBCSChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT);
}
public synchronized boolean isDBCSChar(int pos) {
return IsDBCSChar(pos);
}
public synchronized boolean IsDBCSChar(int row, int col) {
if (screen == null) return false;
return IsDBCSChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCSChar(int row, int col) {
return IsDBCSChar(row, col);
}
public synchronized boolean IsDBCS1stChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && ea.db == ExtendedAttribute.DB_LEFT;
}
public synchronized boolean isDBCS1stChar(int pos) {
return IsDBCS1stChar(pos);
}
public synchronized boolean IsDBCS1stChar(int row, int col) {
if (screen == null) return false;
return IsDBCS1stChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCS1stChar(int row, int col) {
return IsDBCS1stChar(row, col);
}
public synchronized boolean IsDBCS2ndChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && ea.db == ExtendedAttribute.DB_RIGHT;
}
public synchronized boolean isDBCS2ndChar(int pos) {
return IsDBCS2ndChar(pos);
}
public synchronized boolean IsDBCS2ndChar(int row, int col) {
if (screen == null) return false;
return IsDBCS2ndChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCS2ndChar(int row, int col) {
return IsDBCS2ndChar(row, col);
}
public synchronized boolean IsSOChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_SO || (ea.ec & 0xFF) == 0x0E);
}
public synchronized boolean isSOChar(int pos) {
return IsSOChar(pos);
}
public synchronized boolean IsSIChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_SI || (ea.ec & 0xFF) == 0x0F);
}
public synchronized boolean isSIChar(int pos) {
return IsSIChar(pos);
}
public synchronized boolean IsSOSIChar(int pos) {
return IsSOChar(pos) || IsSIChar(pos);
}
public synchronized boolean isSOSIChar(int pos) {
return IsSOSIChar(pos);
}
public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) { public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) {
if (cArray == null || len <= 0 || screen == null) return 0; if (cArray == null || len <= 0 || screen == null) return 0;
int minR = Math.max(1, Math.min(sRow, eRow)); int minR = Math.max(1, Math.min(sRow, eRow));
@@ -9,10 +9,11 @@ import java.util.regex.Pattern;
*/ */
public class FTConfig { public class FTConfig {
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?"); private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FB|VB|VBS|FBS|FIXED|VARIABLE|UNDEFINED)\\)?");
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?"); private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?"); private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?"); private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?(?:[\\s,]+(\\d+))?\\)?");
private static final Pattern DIR_PATTERN = Pattern.compile("(?i)(?:DIR|DIRECTORY)[\\s\\(]+(\\d+)\\)?");
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?"); private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?"); private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?"); private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
@@ -68,6 +69,7 @@ public class FTConfig {
private int blksize = 0; private int blksize = 0;
private int primarySpace = 0; private int primarySpace = 0;
private int secondarySpace = 0; private int secondarySpace = 0;
private int directoryBlocks = 0;
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;
@@ -172,6 +174,20 @@ public class FTConfig {
public int getSecondarySpace() { return secondarySpace; } public int getSecondarySpace() { return secondarySpace; }
public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; } public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; }
public int getDirectoryBlocks() { return directoryBlocks; }
public void setDirectoryBlocks(int directoryBlocks) { this.directoryBlocks = Math.max(0, directoryBlocks); }
public void setDirectoryBlocks(String dir) {
if (dir == null || dir.trim().isEmpty()) {
this.directoryBlocks = 0;
return;
}
try {
this.directoryBlocks = Math.max(0, Integer.parseInt(dir.trim()));
} catch (NumberFormatException e) {
this.directoryBlocks = 0;
}
}
public int getAvblock() { return avblock; } public int getAvblock() { return avblock; }
public void setAvblock(int avblock) { this.avblock = avblock; } public void setAvblock(int avblock) { this.avblock = avblock; }
@@ -217,9 +233,9 @@ public class FTConfig {
this.recfm = RecordFormat.DEFAULT; this.recfm = RecordFormat.DEFAULT;
} else { } else {
String u = r.trim().toUpperCase(); String u = r.trim().toUpperCase();
if (u.equals("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED; if (u.startsWith("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED;
else if (u.equals("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE; else if (u.startsWith("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE;
else if (u.equals("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED; else if (u.startsWith("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED;
else this.recfm = RecordFormat.DEFAULT; else this.recfm = RecordFormat.DEFAULT;
} }
} }
@@ -233,14 +249,23 @@ public class FTConfig {
} }
public void setSpace(String s) { public void setSpace(String s) {
// Space string like "10,5" for primary/secondary // Space string like "10,5" or "10,5,10" for primary/secondary/directory
if (s == null || s.trim().isEmpty()) return; if (s == null || s.trim().isEmpty()) return;
String[] parts = s.split(","); String[] parts = s.split(",");
try { try {
if (parts.length > 0) this.primarySpace = Integer.parseInt(parts[0].trim()); if (parts.length > 0 && !parts[0].trim().isEmpty()) {
if (parts.length > 1) this.secondarySpace = Integer.parseInt(parts[1].trim()); this.primarySpace = Integer.parseInt(parts[0].trim());
} catch (NumberFormatException e) { if (this.units == AllocationUnit.DEFAULT) {
// ignore this.units = AllocationUnit.TRACKS;
}
}
if (parts.length > 1 && !parts[1].trim().isEmpty()) {
this.secondarySpace = Integer.parseInt(parts[1].trim());
}
if (parts.length > 2 && !parts[2].trim().isEmpty()) {
this.directoryBlocks = Math.max(0, Integer.parseInt(parts[2].trim()));
}
} catch (NumberFormatException ignored) {
} }
} }
@@ -279,6 +304,22 @@ public class FTConfig {
if (spaceMatcher.group(2) != null) { if (spaceMatcher.group(2) != null) {
this.secondarySpace = Integer.parseInt(spaceMatcher.group(2)); this.secondarySpace = Integer.parseInt(spaceMatcher.group(2));
} }
if (spaceMatcher.group(3) != null) {
this.directoryBlocks = Math.max(0, Integer.parseInt(spaceMatcher.group(3)));
}
if (this.units == AllocationUnit.DEFAULT) {
this.units = AllocationUnit.TRACKS;
}
} catch (NumberFormatException ignored) {}
}
Matcher dirMatcher = DIR_PATTERN.matcher(trimmed);
if (dirMatcher.find()) {
try {
this.directoryBlocks = Math.max(0, Integer.parseInt(dirMatcher.group(1)));
if (this.units == AllocationUnit.DEFAULT) {
this.units = AllocationUnit.TRACKS;
}
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
@@ -501,7 +542,12 @@ public class FTConfig {
StringBuilder cmd = new StringBuilder(); StringBuilder cmd = new StringBuilder();
// Host program selection: IND$FILE or APVUFILE // Host program selection: IND$FILE or APVUFILE
cmd.append(getProgramName()).append(" "); // For TSO, prefix with "TSO " so it can be called from any ISPF command line or native prompt
String prog = getProgramName();
if (hostType == HostType.TSO && !prog.toUpperCase().startsWith("TSO ")) {
cmd.append("TSO ");
}
cmd.append(prog).append(" ");
cmd.append(isReceive() ? "GET " : "PUT "); cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename != null ? hostFilename.trim() : ""); cmd.append(hostFilename != null ? hostFilename.trim() : "");
@@ -568,14 +614,18 @@ public class FTConfig {
opts.append(" BLKSIZE(").append(blksize).append(")"); opts.append(" BLKSIZE(").append(blksize).append(")");
} }
} }
if (units != AllocationUnit.DEFAULT) { if (units != AllocationUnit.DEFAULT || primarySpace > 0 || directoryBlocks > 0) {
AllocationUnit effectiveUnits = (units != AllocationUnit.DEFAULT) ? units : AllocationUnit.TRACKS;
if (opts.length() > 0) opts.append(" "); if (opts.length() > 0) opts.append(" ");
opts.append("SPACE(").append(primarySpace); opts.append("SPACE(").append(primarySpace);
if (secondarySpace > 0) { if (secondarySpace > 0 || directoryBlocks > 0) {
opts.append(",").append(secondarySpace); opts.append(",").append(secondarySpace);
} }
if (directoryBlocks > 0) {
opts.append(",").append(directoryBlocks);
}
opts.append(")"); opts.append(")");
switch (units) { switch (effectiveUnits) {
case TRACKS: opts.append(" TRACKS"); break; case TRACKS: opts.append(" TRACKS"); break;
case CYLINDERS: opts.append(" CYLINDERS"); break; case CYLINDERS: opts.append(" CYLINDERS"); break;
case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break; case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break;
@@ -816,15 +816,14 @@ public class FTDft {
for (int i = 0; i < lineStr.length(); i++) { for (int i = 0; i < lineStr.length(); i++) {
char ch = lineStr.charAt(i); char ch = lineStr.charAt(i);
int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1; int dbcsVal = (isDbcs && translator != null) ? translator.unicodeToDbcs(ch) : -1;
boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF); if (dbcsVal != -1) {
if (charIsDbcs) {
if (!inDbcsState && !noSo) { if (!inDbcsState && !noSo) {
converted.write(0x0E); // Shift-Out converted.write(0x0E); // Shift-Out
inDbcsState = true; inDbcsState = true;
} }
converted.write((ebc >> 8) & 0xFF); converted.write((dbcsVal >> 8) & 0xFF);
converted.write(ebc & 0xFF); converted.write(dbcsVal & 0xFF);
} else { } else {
if (inDbcsState && !noSo) { if (inDbcsState && !noSo) {
converted.write(0x0F); // Shift-In converted.write(0x0F); // Shift-In
@@ -17,7 +17,7 @@ public class CMSDirectoryParser {
return entries; return entries;
} }
String[] lines = text.split("\r?\n"); String[] lines = splitIntoLines(text);
for (String line : lines) { for (String line : lines) {
String trimmed = line.trim(); String trimmed = line.trim();
@@ -26,6 +26,16 @@ public class CMSDirectoryParser {
String upper = trimmed.toUpperCase(); String upper = trimmed.toUpperCase();
if (upper.startsWith("FILENAME") || upper.startsWith("DIRECTORY") || upper.startsWith("FILELIST")) continue; if (upper.startsWith("FILENAME") || upper.startsWith("DIRECTORY") || upper.startsWith("FILELIST")) continue;
if (upper.startsWith("READY;") || upper.startsWith("READY(") || upper.equals("READY")
|| upper.startsWith("MORE...") || upper.startsWith("HOLDING")
|| upper.startsWith("RUNNING") || upper.equals("RUNNING")
|| upper.startsWith("DMSLST") || upper.startsWith("DMS")) continue;
if (upper.endsWith("MORE...") || upper.endsWith("HOLDING") || upper.endsWith("RUNNING")) {
int idx = Math.max(upper.lastIndexOf("MORE..."), Math.max(upper.lastIndexOf("HOLDING"), upper.lastIndexOf("RUNNING")));
trimmed = trimmed.substring(0, idx).trim();
if (trimmed.isEmpty()) continue;
}
CMSDirectoryEntry entry = parseCmsLine(trimmed); CMSDirectoryEntry entry = parseCmsLine(trimmed);
if (entry != null) { if (entry != null) {
@@ -36,6 +46,24 @@ public class CMSDirectoryParser {
return entries; return entries;
} }
private static String[] splitIntoLines(String text) {
if (text.contains("\n") || text.contains("\r")) {
return text.split("\r?\n");
}
if (text.length() >= 80) {
int lineLen = (text.length() % 132 == 0 && text.length() % 80 != 0) ? 132 : 80;
int numLines = (text.length() + lineLen - 1) / lineLen;
String[] lines = new String[numLines];
for (int i = 0; i < numLines; i++) {
int start = i * lineLen;
int end = Math.min(start + lineLen, text.length());
lines[i] = text.substring(start, end);
}
return lines;
}
return new String[]{text};
}
private static CMSDirectoryEntry parseCmsLine(String line) { private static CMSDirectoryEntry parseCmsLine(String line) {
String[] tokens = line.split("\\s+"); String[] tokens = line.split("\\s+");
if (tokens.length < 2) return null; if (tokens.length < 2) return null;
@@ -11,12 +11,16 @@ import java.util.regex.Pattern;
public class TSODirectoryParser { public class TSODirectoryParser {
private static final Pattern DSN_PATTERN = Pattern.compile("([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)+)"); 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 NONVSAM_PATTERN = Pattern.compile("(?:NONVSAM|CLUSTER|GDG|ALIAS|AIX|PATH)\\s*[-:=]+\\s*([A-Z0-9@#$]+(?:\\.[A-Z0-9@#$]+)*)", Pattern.CASE_INSENSITIVE);
private static final Pattern VOLSER_PATTERN = Pattern.compile("VOLSER-+([A-Z0-9]+)", Pattern.CASE_INSENSITIVE); private static final Pattern VOLSER_PATTERN = Pattern.compile("VOLSER\\s*[-:=]+\\s*([A-Z0-9*@#$&]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern RECFM_PATTERN = Pattern.compile("RECFM-+([A-Z]+)", Pattern.CASE_INSENSITIVE); private static final Pattern RECFM_PATTERN = Pattern.compile("RECFM\\s*[-:=]+\\s*([A-Z0-9,]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern LRECL_PATTERN = Pattern.compile("LRECL-+([0-9]+)", Pattern.CASE_INSENSITIVE); private static final Pattern LRECL_PATTERN = Pattern.compile("(?:AVG|MAX)?LRECL\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern BLKSIZE_PATTERN = Pattern.compile("BLKSIZE-+([0-9]+)", Pattern.CASE_INSENSITIVE); private static final Pattern BLKSIZE_PATTERN = Pattern.compile("(?:CISIZE|BLKSIZE)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern DSORG_PATTERN = Pattern.compile("DSORG-+([A-Z]+)", Pattern.CASE_INSENSITIVE); private static final Pattern DSORG_PATTERN = Pattern.compile("DSORG\\s*[-:=]+\\s*([A-Z0-9\\-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern EXTENTS_PATTERN = Pattern.compile("(?:VOL-)?EXTENTS\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern SPACE_PRI_PATTERN = Pattern.compile("(?:SPACE-PRI|ALLOC-PRM|ALLOC_PRM)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern SPACE_TYPE_PATTERN = Pattern.compile("SPACE-TYPE\\s*[-:=]+\\s*([A-Z]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern TRACKS_PATTERN = Pattern.compile("(?:TRACKS|TRKS)\\s*[-:=]+\\s*([0-9]+)", Pattern.CASE_INSENSITIVE);
/** /**
* Parse raw text containing TSO dataset listings into a structured list of TSODirectoryEntry. * Parse raw text containing TSO dataset listings into a structured list of TSODirectoryEntry.
@@ -27,19 +31,27 @@ public class TSODirectoryParser {
return entries; return entries;
} }
String[] lines = text.split("\r?\n"); String[] lines = splitIntoLines(text);
// Check if text is LISTCAT format // Check if text is LISTCAT format
if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT")) { if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT") || text.toUpperCase().contains("IDCAMS")
|| text.toUpperCase().contains("CLUSTER") || text.toUpperCase().contains("GDG")) {
parseListcat(lines, entries); parseListcat(lines, entries);
if (!entries.isEmpty()) return entries; if (!entries.isEmpty()) return entries;
} }
// Check if text is LISTDS format
if (text.toUpperCase().contains("--RECFM-") || text.toUpperCase().contains("--VOLUMES--")) {
parseListds(lines, entries);
if (!entries.isEmpty()) return entries;
}
// Check for ISPF DSLIST or general tabular format // Check for ISPF DSLIST or general tabular format
for (String line : lines) { for (String line : lines) {
String trimmed = line.trim(); String trimmed = line.trim();
if (trimmed.isEmpty()) continue; if (trimmed.isEmpty()) continue;
if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**")) continue; if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**") || trimmed.startsWith("***")) continue;
if (trimmed.equalsIgnoreCase("READY") || trimmed.toUpperCase().startsWith("READY;")) continue;
if (trimmed.toUpperCase().startsWith("COMMAND") || trimmed.toUpperCase().startsWith("DSLIST") || trimmed.toUpperCase().startsWith("DATA SETS")) continue; if (trimmed.toUpperCase().startsWith("COMMAND") || trimmed.toUpperCase().startsWith("DSLIST") || trimmed.toUpperCase().startsWith("DATA SETS")) continue;
TSODirectoryEntry entry = parseTabularLine(trimmed); TSODirectoryEntry entry = parseTabularLine(trimmed);
@@ -51,38 +63,196 @@ public class TSODirectoryParser {
return entries; return entries;
} }
private static String[] splitIntoLines(String text) {
if (text.contains("\n") || text.contains("\r")) {
return text.split("\r?\n");
}
if (text.length() >= 80) {
int lineLen = (text.length() % 132 == 0 && text.length() % 80 != 0) ? 132 : 80;
int numLines = (text.length() + lineLen - 1) / lineLen;
String[] lines = new String[numLines];
for (int i = 0; i < numLines; i++) {
int start = i * lineLen;
int end = Math.min(start + lineLen, text.length());
lines[i] = text.substring(start, end);
}
return lines;
}
return new String[]{text};
}
private static void parseListcat(String[] lines, List<TSODirectoryEntry> entries) { private static void parseListcat(String[] lines, List<TSODirectoryEntry> entries) {
TSODirectoryEntry current = null; TSODirectoryEntry current = null;
String currentSpaceType = null;
for (String line : lines) { for (String line : lines) {
String upper = line.toUpperCase(); String upper = line.toUpperCase();
Matcher nonvsamMat = NONVSAM_PATTERN.matcher(upper); Matcher nonvsamMat = NONVSAM_PATTERN.matcher(upper);
if (nonvsamMat.find()) { if (nonvsamMat.find()) {
if (current != null && current.getName() != null) { if (current != null && current.getName() != null) {
if (current.getTracksAllocated() > 0 && current.getExtents() == 0) {
current.setExtents(1);
}
entries.add(current); entries.add(current);
} }
current = new TSODirectoryEntry(nonvsamMat.group(1)); current = new TSODirectoryEntry(nonvsamMat.group(1));
if (upper.contains("CLUSTER") || upper.contains("AIX") || upper.contains("PATH")) {
current.setDsorg("VSAM");
}
currentSpaceType = null;
continue; continue;
} }
if (current != null) { if (current != null) {
Matcher volMat = VOLSER_PATTERN.matcher(upper); Matcher volMat = VOLSER_PATTERN.matcher(upper);
if (volMat.find()) current.setVolume(volMat.group(1)); if (volMat.find() && current.getVolume() == null) current.setVolume(volMat.group(1));
Matcher recMat = RECFM_PATTERN.matcher(upper); Matcher recMat = RECFM_PATTERN.matcher(upper);
if (recMat.find()) current.setRecfm(recMat.group(1)); if (recMat.find()) {
String raw = recMat.group(1).toUpperCase();
if (raw.contains("VAR") && raw.contains("BLOCK")) current.setRecfm("VB");
else if (raw.contains("FIX") && raw.contains("BLOCK")) current.setRecfm("FB");
else if (raw.contains("VAR")) current.setRecfm("V");
else if (raw.contains("FIX")) current.setRecfm("F");
else if (raw.contains("UNDEF")) current.setRecfm("U");
else current.setRecfm(raw);
}
Matcher lreclMat = LRECL_PATTERN.matcher(upper); Matcher lreclMat = LRECL_PATTERN.matcher(upper);
if (lreclMat.find()) { if (lreclMat.find() && current.getLrecl() == 0) {
try { current.setLrecl(Integer.parseInt(lreclMat.group(1))); } catch (NumberFormatException ignored) {} try { current.setLrecl(Integer.parseInt(lreclMat.group(1))); } catch (NumberFormatException ignored) {}
} }
Matcher blkMat = BLKSIZE_PATTERN.matcher(upper); Matcher blkMat = BLKSIZE_PATTERN.matcher(upper);
if (blkMat.find()) { if (blkMat.find() && current.getBlksize() == 0) {
try { current.setBlksize(Integer.parseInt(blkMat.group(1))); } catch (NumberFormatException ignored) {} try { current.setBlksize(Integer.parseInt(blkMat.group(1))); } catch (NumberFormatException ignored) {}
} }
Matcher dsorgMat = DSORG_PATTERN.matcher(upper); Matcher dsorgMat = DSORG_PATTERN.matcher(upper);
if (dsorgMat.find()) current.setDsorg(dsorgMat.group(1)); if (dsorgMat.find() && current.getDsorg() == null) current.setDsorg(dsorgMat.group(1));
Matcher extMat = EXTENTS_PATTERN.matcher(upper);
if (extMat.find() && current.getExtents() == 0) {
try { current.setExtents(Integer.parseInt(extMat.group(1))); } catch (NumberFormatException ignored) {}
}
Matcher typeMat = SPACE_TYPE_PATTERN.matcher(upper);
if (typeMat.find()) {
currentSpaceType = typeMat.group(1);
}
Matcher trkMat = TRACKS_PATTERN.matcher(upper);
if (trkMat.find() && current.getTracksAllocated() == 0) {
try { current.setTracksAllocated(Integer.parseInt(trkMat.group(1))); } catch (NumberFormatException ignored) {}
} else {
Matcher priMat = SPACE_PRI_PATTERN.matcher(upper);
if (priMat.find() && current.getTracksAllocated() == 0) {
try {
int pri = Integer.parseInt(priMat.group(1));
if (currentSpaceType != null && currentSpaceType.startsWith("CYL")) {
pri = pri * 15;
}
current.setTracksAllocated(pri);
} catch (NumberFormatException ignored) {}
}
}
}
}
if (current != null && current.getName() != null) {
if (current.getTracksAllocated() > 0 && current.getExtents() == 0) {
current.setExtents(1);
}
entries.add(current);
}
}
private static void parseListds(String[] lines, List<TSODirectoryEntry> entries) {
TSODirectoryEntry current = null;
boolean inRecfm = false;
boolean inVol = false;
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.isEmpty()) continue;
String upper = trimmed.toUpperCase();
if (upper.startsWith("--RECFM-")) {
inRecfm = true;
inVol = false;
continue;
}
if (upper.startsWith("--VOLUMES--")) {
inVol = true;
inRecfm = false;
continue;
}
if (upper.startsWith("--ALLOCATION") || upper.startsWith("--TRACKS-")) {
inRecfm = false;
inVol = false;
continue;
}
if (inRecfm && current != null) {
String[] toks = trimmed.split("\\s+");
if (toks.length >= 1 && toks[0].matches("^[A-Z]+$")) current.setRecfm(toks[0]);
if (toks.length >= 2 && toks[1].matches("^\\d+$")) {
try { current.setLrecl(Integer.parseInt(toks[1])); } catch (NumberFormatException ignored) {}
}
if (toks.length >= 3 && toks[2].matches("^\\d+$")) {
try { current.setBlksize(Integer.parseInt(toks[2])); } catch (NumberFormatException ignored) {}
}
if (toks.length >= 4 && toks[3].matches("^[A-Z]+$")) current.setDsorg(toks[3]);
inRecfm = false;
continue;
}
if (inVol && current != null) {
String[] toks = trimmed.split("\\s+");
if (toks.length >= 1 && toks[0].matches("^[A-Z0-9*@#$&]{1,6}$")) {
current.setVolume(toks[0]);
}
inVol = false;
continue;
}
if (current != null) {
Matcher trkMat = TRACKS_PATTERN.matcher(upper);
if (trkMat.find() && current.getTracksAllocated() == 0) {
try { current.setTracksAllocated(Integer.parseInt(trkMat.group(1))); } catch (NumberFormatException ignored) {}
} else {
Matcher priMat = SPACE_PRI_PATTERN.matcher(upper);
if (priMat.find() && current.getTracksAllocated() == 0) {
try { current.setTracksAllocated(Integer.parseInt(priMat.group(1))); } catch (NumberFormatException ignored) {}
}
}
Matcher extMat = EXTENTS_PATTERN.matcher(upper);
if (extMat.find() && current.getExtents() == 0) {
try { current.setExtents(Integer.parseInt(extMat.group(1))); } catch (NumberFormatException ignored) {}
}
// Check for tabular numbers row under --TRACKS-ALLOCATED-EXTENTS-
String[] numToks = trimmed.split("\\s+");
if (numToks.length >= 2 && numToks[0].matches("^\\d+$") && numToks[1].matches("^\\d+$")) {
if (current.getTracksAllocated() == 0) {
try { current.setTracksAllocated(Integer.parseInt(numToks[0])); } catch (NumberFormatException ignored) {}
}
if (current.getExtents() == 0) {
try { current.setExtents(Integer.parseInt(numToks[1])); } catch (NumberFormatException ignored) {}
}
}
}
String[] toks = trimmed.split("\\s+");
if (toks.length > 0) {
String cleanTok = toks[0].replace("'", "").replace("\"", "");
if (DSN_PATTERN.matcher(cleanTok.toUpperCase()).matches()) {
if (current != null && current.getName() != null) {
entries.add(current);
}
current = new TSODirectoryEntry(cleanTok.toUpperCase());
inRecfm = false;
inVol = false;
}
} }
} }
if (current != null && current.getName() != null) { if (current != null && current.getName() != null) {
@@ -108,6 +278,8 @@ public class TSODirectoryParser {
TSODirectoryEntry entry = new TSODirectoryEntry(tokens[dsnIdx].toUpperCase()); TSODirectoryEntry entry = new TSODirectoryEntry(tokens[dsnIdx].toUpperCase());
boolean seenFormat = false; boolean seenFormat = false;
int numBeforeFormat = 0;
int numAfterFormat = 0;
// Parse remaining tokens // Parse remaining tokens
for (int i = 0; i < tokens.length; i++) { for (int i = 0; i < tokens.length; i++) {
@@ -117,7 +289,7 @@ public class TSODirectoryParser {
if (tok.matches("^(PS|PO|PO-E|VSAM|DA|IS)$")) { if (tok.matches("^(PS|PO|PO-E|VSAM|DA|IS)$")) {
entry.setDsorg(tok); entry.setDsorg(tok);
seenFormat = true; seenFormat = true;
} else if (tok.matches("^(F|FB|V|VB|U|VBS|FBS)$")) { } else if (tok.matches("^(F|FB|V|VB|U|VBS|FBS|FBA|VBA|FA|VA|FBM|VBM)$")) {
entry.setRecfm(tok); entry.setRecfm(tok);
seenFormat = true; seenFormat = true;
} else if (tok.matches("^(3390|3380|TAPE|VIO)$")) { } else if (tok.matches("^(3390|3380|TAPE|VIO)$")) {
@@ -129,23 +301,29 @@ public class TSODirectoryParser {
} else { } else {
entry.setReferencedDate(tok); entry.setReferencedDate(tok);
} }
} else if (tok.matches("^[A-Z0-9]{6}$") && entry.getVolume() == null && !tok.matches("^\\d+$")) { } else if (tok.matches("^[A-Z0-9*@#$&]{1,6}$") && entry.getVolume() == null && !tok.matches("^\\d+$")
&& !tok.matches("^(PAGE|TOTAL|TRACKS|TRKS|EXTENTS|VOLUME|DSORG|RECFM|DATASET|NAME|DSN)$")) {
entry.setVolume(tok); entry.setVolume(tok);
} else if (tok.matches("^\\d+$")) { } else if (tok.matches("^\\d+$")) {
int val = Integer.parseInt(tok); int val = Integer.parseInt(tok);
if (!seenFormat) { if (!seenFormat) {
if (entry.getTracksAllocated() == 0) { if (numBeforeFormat == 0) {
entry.setTracksAllocated(val); entry.setTracksAllocated(val);
} else if (entry.getPercentUsed() == 0 && val <= 100) { numBeforeFormat++;
} else if (numBeforeFormat == 1 && val <= 100) {
entry.setPercentUsed(val); entry.setPercentUsed(val);
} else if (entry.getExtents() == 0 && val <= 128) { numBeforeFormat++;
} else if (numBeforeFormat <= 2 && val <= 128) {
entry.setExtents(val); entry.setExtents(val);
numBeforeFormat++;
} }
} else { } else {
if (entry.getLrecl() == 0 && val <= 32760) { if (numAfterFormat == 0 && val <= 32760) {
entry.setLrecl(val); entry.setLrecl(val);
} else if (entry.getBlksize() == 0 && val <= 32760) { numAfterFormat++;
} else if (numAfterFormat == 1 && val <= 32760) {
entry.setBlksize(val); entry.setBlksize(val);
numAfterFormat++;
} else if (entry.getTracksAllocated() == 0) { } else if (entry.getTracksAllocated() == 0) {
entry.setTracksAllocated(val); entry.setTracksAllocated(val);
} }
@@ -153,6 +331,10 @@ public class TSODirectoryParser {
} }
} }
if (entry.getTracksAllocated() > 0 && entry.getExtents() == 0) {
entry.setExtents(1);
}
return entry; return entry;
} }
} }
@@ -129,6 +129,9 @@ public class InputProcessor {
lockStateListener.onLockStateChanged(locked); lockStateListener.onLockStateChanged(locked);
} }
} }
if (!locked) {
this.lastAid = AID_NO;
}
} }
public boolean isInsertMode() { return insertMode; } public boolean isInsertMode() { return insertMode; }
public void setInsertMode(boolean insert) { public void setInsertMode(boolean insert) {
@@ -292,9 +295,10 @@ public class InputProcessor {
} }
} }
// Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode) // Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode, or DBCS)
boolean isAplChar = false; boolean isAplChar = false;
int ebc = -1; int ebc = -1;
int dbcs = -1;
if (aplKeyboardMode) { if (aplKeyboardMode) {
int aplCode = getAplCodeForChar(ch); int aplCode = getAplCodeForChar(ch);
if (aplCode >= 0) { if (aplCode >= 0) {
@@ -303,10 +307,62 @@ public class InputProcessor {
isAplChar = true; isAplChar = true;
} }
} }
if (ebc < 0) { if (ebc < 0 && translator != null && translator.isDBCS()) {
ebc = translator.unicodeToEbcdic(ch); dbcs = translator.unicodeToDbcs(ch);
}
if (ebc < 0 && dbcs < 0) {
ebc = (translator != null) ? translator.unicodeToEbcdic(ch) : (int) ch;
}
if (ebc < 0 && dbcs < 0) return;
if (dbcs >= 0) {
// DBCS character typing
if (insertMode) {
if (!screen.insertChar(baddr, ch)) {
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
}
setKeyboardLocked(true);
return;
}
} else {
int nextPos = screen.incrementAddress(baddr);
if (screen.isFormatted() && screen.getCell(nextPos).isFieldAttribute()) {
// Cannot fit 2-cell DBCS at the last position of the field
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
}
setKeyboardLocked(true);
return;
}
int b1 = (dbcs >> 8) & 0xFF;
int b2 = dbcs & 0xFF;
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) b1;
ea1.ucs4 = ch;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ExtendedAttribute ea2 = screen.getCell(nextPos);
ea2.fa = 0;
ea2.ec = (byte) b2;
ea2.ucs4 = ch;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
}
screen.setCursorAddress(screen.incrementAddress(nextPos));
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return;
} }
if (ebc < 0) return;
if (insertMode) { if (insertMode) {
// Insert mode: shift characters right from cursor to end of field // Insert mode: shift characters right from cursor to end of field
@@ -703,7 +759,7 @@ public class InputProcessor {
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields). * Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
*/ */
public byte[] buildReadModifiedInboundData() { public byte[] buildReadModifiedInboundData() {
return buildReadModifiedInboundData(lastAid != 0 ? lastAid : AID_ENTER, false); return buildReadModifiedInboundData(lastAid != 0 ? lastAid : AID_NO, false);
} }
/** /**
@@ -1476,38 +1532,12 @@ public class InputProcessor {
} }
public void deleteChar() { public void deleteChar() {
if (!screen.isFormatted()) return;
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols(); if (screen.isFormatted()) {
if (size <= 0) return; byte faVal = screen.getFieldAttributeAt(addr);
addr = ((addr % size) + size) % size; if (faIsProtected(faVal & 0xFF)) return;
// Cannot delete a field attribute
if (screen.getCell(addr).isFieldAttribute()) return;
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
// Shift characters left within the field
int shiftAddr = addr;
int count = 0;
while (count < size) {
int next = screen.incrementAddress(shiftAddr);
if (screen.getCell(next).isFieldAttribute()) {
screen.getCell(shiftAddr).ec = 0;
screen.getCell(shiftAddr).ucs4 = 0;
break;
}
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
shiftAddr = next;
count++;
}
int faAddr = screen.findFieldAttribute(addr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
} }
screen.deleteChar(addr);
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot(); screen.updateDisplaySnapshot();
} }
@@ -1525,6 +1555,9 @@ public class InputProcessor {
return; return;
} }
} }
if (screen.getCell(prev).db == ExtendedAttribute.DB_RIGHT) {
prev = screen.decrementAddress(prev);
}
screen.setCursorAddress(prev); screen.setCursorAddress(prev);
deleteChar(); deleteChar();
} }
@@ -1629,6 +1662,7 @@ public class InputProcessor {
/** Reset (unlock keyboard, cancel insert mode, clear OIA error states). */ /** Reset (unlock keyboard, cancel insert mode, clear OIA error states). */
public void reset() { public void reset() {
setKeyboardLocked(false); setKeyboardLocked(false);
lastAid = AID_NO;
insertMode = false; insertMode = false;
if (oia != null) { if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED); oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
@@ -97,6 +97,11 @@ public class ExtendedAttribute {
/** Validation attribute. */ /** Validation attribute. */
public byte vl; public byte vl;
// Transparency attribute values (0x46 / XA_TRANSPARENCY)
public static final byte TR_DEFAULT = 0x00;
public static final byte TR_OR = (byte) 0xF0;
public static final byte TR_OPAQUE = (byte) 0xFF;
/** Transparency attribute. */ /** Transparency attribute. */
public byte tr; public byte tr;
@@ -727,6 +727,14 @@ public class ScreenBuffer {
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i]; ExtendedAttribute ea = buffer[i];
if (!ea.isFieldAttribute()) { if (!ea.isFieldAttribute()) {
if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT) {
// Preserve decoded DBCS character
continue;
}
if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
ea.ucs4 = ' ';
continue;
}
if (ea.ec == 0) { if (ea.ec == 0) {
ea.ucs4 = 0; ea.ucs4 = 0;
} else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE } else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE
@@ -736,6 +744,9 @@ public class ScreenBuffer {
} }
} }
} }
if (translator != null && translator.isDBCS()) {
processSOSI();
}
} }
private char getAplGraphic(int ec) { private char getAplGraphic(int ec) {
@@ -1124,7 +1135,7 @@ public class ScreenBuffer {
int count = 0; int count = 0;
while (count < size) { while (count < size) {
int next = incrementAddress(shiftAddr); int next = incrementAddress(shiftAddr);
if (getCell(next).isFieldAttribute()) { if (getCell(next).isFieldAttribute() || count == size - 1) {
getCell(shiftAddr).clear(); getCell(shiftAddr).clear();
break; break;
} }
@@ -556,17 +556,42 @@ public class TelnetConnection {
running = false; running = false;
stopKeepAlive(); stopKeepAlive();
try { try {
if (outputStream != null) {
try {
outputStream.flush();
} catch (IOException ignored) {}
}
if (socket != null && !socket.isClosed()) { if (socket != null && !socket.isClosed()) {
socket.shutdownInput(); try {
socket.shutdownOutput(); if (!socket.isOutputShutdown()) {
socket.shutdownOutput();
}
} catch (IOException ignored) {}
// Allow host a brief drain window to receive EOF / FIN-ACK
if (readerThread != null && readerThread.isAlive() && Thread.currentThread() != readerThread) {
try {
readerThread.join(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else if (inputStream != null) {
try {
socket.setSoTimeout(100);
byte[] b = new byte[256];
while (inputStream.read(b) != -1) {}
} catch (Throwable ignored) {}
}
socket.close(); socket.close();
} }
} catch (IOException e) { } catch (IOException e) {
log.log(Level.FINE, "Error during disconnect", e); log.log(Level.FINE, "Error during disconnect", e);
} finally {
socket = null;
inputStream = null;
outputStream = null;
readerThread = null;
} }
socket = null;
inputStream = null;
outputStream = null;
log.info("Disconnected"); log.info("Disconnected");
} }
@@ -1366,6 +1366,71 @@ public class TelnetFSM {
sendRecord(resp); sendRecord(resp);
} }
/**
* Send TN3270E UNBIND request to release LU session cleanly (RFC 2355 Section 9).
* @param reason SNA UNBIND reason code (e.g. UNBIND_NORMAL = 0x01)
*/
public synchronized void sendUnbind(int reason) {
if (!tn3270eNegotiated || !tn3270eBound) {
return;
}
if (connection == null || !connection.isConnected()) {
return;
}
try {
byte[] unbindData = new byte[EH_SIZE + 1];
unbindData[0] = (byte) DT_UNBIND;
unbindData[1] = 0; // Request flags
unbindData[2] = (byte) RSF_NO_RESPONSE; // Response flags
unbindData[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
unbindData[4] = (byte) (eXmitSeq & 0xFF);
unbindData[5] = (byte) (reason & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
log.info("Sending TN3270E UNBIND reason=" + unbindReasonName(reason));
sendRecord(unbindData);
tn3270eBound = false;
} catch (Exception e) {
log.log(Level.FINE, "Failed to send TN3270E UNBIND", e);
}
}
/**
* Send graceful Telnet de-negotiation (UNBIND if bound, followed by WONT/DONT for active options).
*/
public synchronized void sendTelnetDisconnect() {
if (connection == null || !connection.isConnected()) {
return;
}
try {
if (tn3270eNegotiated && tn3270eBound) {
sendUnbind(UNBIND_NORMAL);
}
// De-negotiate TN3270E first if active
if (myOpts[TELOPT_TN3270E]) {
sendCommand(WONT, TELOPT_TN3270E);
myOpts[TELOPT_TN3270E] = false;
}
if (hisOpts[TELOPT_TN3270E]) {
sendCommand(DONT, TELOPT_TN3270E);
hisOpts[TELOPT_TN3270E] = false;
}
// De-negotiate all other active options
for (int opt = 0; opt < 256; opt++) {
if (myOpts[opt]) {
sendCommand(WONT, opt);
myOpts[opt] = false;
}
if (hisOpts[opt]) {
sendCommand(DONT, opt);
hisOpts[opt] = false;
}
}
log.fine("Sent Telnet option de-negotiation");
} catch (Exception e) {
log.log(Level.FINE, "Error sending Telnet de-negotiation", e);
}
}
// ========== Check if we should transition to 3270 mode ========== // ========== Check if we should transition to 3270 mode ==========
private void checkIn3270() { private void checkIn3270() {
@@ -1531,6 +1596,9 @@ public class TelnetFSM {
private void changeState(ConnectionState newState) { private void changeState(ConnectionState newState) {
ConnectionState old = this.connectionState; ConnectionState old = this.connectionState;
if (old == newState) {
return;
}
this.connectionState = newState; this.connectionState = newState;
log.info("State: " + old + " -> " + newState); log.info("State: " + old + " -> " + newState);
for (ConnectionListener l : connectionListeners) { for (ConnectionListener l : connectionListeners) {
@@ -37,6 +37,7 @@ public class FileTransferFileObject implements Serializable {
private int blksize = 0; private int blksize = 0;
private int primarySpace = 0; private int primarySpace = 0;
private int secondarySpace = 0; private int secondarySpace = 0;
private int directoryBlocks = 0;
private int avblock = 0; private int avblock = 0;
private String options = ""; private String options = "";
@@ -201,6 +202,14 @@ public class FileTransferFileObject implements Serializable {
this.secondarySpace = secondarySpace; this.secondarySpace = secondarySpace;
} }
public int getDirectoryBlocks() {
return directoryBlocks;
}
public void setDirectoryBlocks(int directoryBlocks) {
this.directoryBlocks = Math.max(0, directoryBlocks);
}
public int getAvblock() { public int getAvblock() {
return avblock; return avblock;
} }
@@ -241,6 +250,7 @@ public class FileTransferFileObject implements Serializable {
config.setBlksize(blksize); config.setBlksize(blksize);
config.setPrimarySpace(primarySpace); config.setPrimarySpace(primarySpace);
config.setSecondarySpace(secondarySpace); config.setSecondarySpace(secondarySpace);
config.setDirectoryBlocks(directoryBlocks);
config.setAvblock(avblock); config.setAvblock(avblock);
if (options != null && !options.isEmpty()) { if (options != null && !options.isEmpty()) {
config.parseOptions(options); config.parseOptions(options);
@@ -263,6 +273,7 @@ public class FileTransferFileObject implements Serializable {
this.blksize = config.getBlksize(); this.blksize = config.getBlksize();
this.primarySpace = config.getPrimarySpace(); this.primarySpace = config.getPrimarySpace();
this.secondarySpace = config.getSecondarySpace(); this.secondarySpace = config.getSecondarySpace();
this.directoryBlocks = config.getDirectoryBlocks();
this.avblock = config.getAvblock(); this.avblock = config.getAvblock();
} }
@@ -603,10 +603,260 @@ public class Xfer3270 implements FileTransferInterface {
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_PROCESSING); if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_PROCESSING);
Vector result = new Vector(); Vector result = new Vector();
String dataToParse = hostQuery;
if (hostQuery == null || (!hostQuery.contains("\n") && !hostQuery.contains("\r")
&& !hostQuery.toUpperCase().contains("NONVSAM") && !hostQuery.toUpperCase().contains("IN-CAT")
&& !hostQuery.toUpperCase().contains("FORMAT"))) {
if (session != null && session.getPS() != null) {
try {
ECLPS ps = session.getPS();
ScreenBuffer sb = ps.getScreenBuffer();
StringBuilder sbAll = new StringBuilder();
int maxPages = 500;
if (hostType == VM_CMS) {
ps.SendKeys("[clear]");
if (session.getOIA() != null) {
session.getOIA().waitForInput(3000);
}
String filter;
if (hostQuery == null || hostQuery.trim().isEmpty()) {
filter = "* * A";
} else {
String clean = hostQuery.replace("'", "").replace("\"", "").trim();
if (clean.equals("* * *") || clean.equals("*")) {
filter = "* * A";
} else if (clean.contains(".")) {
int dot = clean.indexOf('.');
filter = clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
} else {
String[] parts = clean.split("\\s+");
if (parts.length == 1) {
filter = parts[0] + " * A";
} else if (parts.length == 2) {
filter = parts[0] + " " + parts[1] + " A";
} else {
filter = parts[0] + " " + parts[1] + " " + parts[2];
}
}
}
ps.SendKeys("LISTFILE " + filter + " ( nohead alloc date[enter]");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
for (int page = 0; page < maxPages; page++) {
long startWait = System.currentTimeMillis();
long timeoutMs = 10000;
String screen = "";
boolean isMore = false;
while (System.currentTimeMillis() - startWait < timeoutMs) {
if (session.getOIA() != null) {
session.getOIA().waitForInput(500);
}
int rows = ps.getRows();
int cols = ps.getCols();
screen = ps.copyString(0, 0, rows - 1, cols - 1);
String upper = screen.toUpperCase();
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
isMore = true;
break;
}
if (upper.contains("READY;") || upper.contains("READY(")) {
break;
}
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
}
sbAll.append(screen).append("\n");
if (isMore) {
ps.SendKeys("[clear]");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
if (session.getOIA() != null) {
session.getOIA().waitForInput(3000);
}
} else {
break;
}
}
// Automatically clear the listing from the terminal presentation space
ps.SendKeys("[clear]");
if (session.getOIA() != null) {
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
session.getOIA().waitForInput(3000);
}
} else {
// TSO
int rows = ps.getRows();
int cols = ps.getCols();
String initialScreen = ps.copyString(0, 0, rows - 1, cols - 1);
boolean isFormatted = sb != null && sb.isFormatted();
boolean hasCmdArrow = initialScreen.contains("===>");
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
String filter = (hostQuery != null && !hostQuery.trim().isEmpty()) ? hostQuery.trim() : "";
String tsoCmd = "TSO LISTCAT ALL";
if (!filter.isEmpty()) {
if (filter.startsWith("'") && filter.endsWith("'") && filter.length() > 2) {
filter = filter.substring(1, filter.length() - 1);
}
int paren = filter.indexOf('(');
if (paren > 0) filter = filter.substring(0, paren);
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
tsoCmd = "TSO LISTCAT ALL";
} else {
String level = filter;
if (level.endsWith(".*")) {
level = level.substring(0, level.length() - 2);
} else if (level.endsWith("*")) {
level = level.substring(0, level.length() - 1);
}
if (level.isEmpty()) {
tsoCmd = "TSO LISTCAT ALL";
} else {
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
}
}
}
if (isIspf) {
int arrowPos = initialScreen.indexOf("===>");
if (arrowPos >= 0 && sb != null) {
int inputAddr = sb.findNextUnprotected(arrowPos);
if (inputAddr > 0) {
sb.setCursorAddress(inputAddr);
sb.updateDisplaySnapshot();
} else {
ps.SendKeys("[home]");
}
} else {
ps.SendKeys("[home]");
}
ps.SendKeys("[erase_eof]");
ps.SendKeys(tsoCmd + "[enter]");
} else {
ps.SendKeys("[clear]");
if (session.getOIA() != null) {
session.getOIA().waitForInput(3000);
}
ps.SendKeys(tsoCmd + "[enter]");
}
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
// Wait for output to start appearing
long startCmd = System.currentTimeMillis();
while (System.currentTimeMillis() - startCmd < 4000) {
if (session.getOIA() != null) {
session.getOIA().waitForInput(200);
}
int r = ps.getRows();
int c = ps.getCols();
String s = ps.copyString(0, 0, r - 1, c - 1);
String u = s.toUpperCase();
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|| u.contains("NO ENTRY") || s.contains(" --- ")) {
break;
}
try { Thread.sleep(80); } catch (InterruptedException ignored) {}
}
String prevScreen = "";
for (int page = 0; page < maxPages; page++) {
if (session.getOIA() != null) {
session.getOIA().waitForInput(300);
}
int r = ps.getRows();
int c = ps.getCols();
String screen = ps.copyString(0, 0, r - 1, c - 1);
String upper = screen.toUpperCase();
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
break;
}
prevScreen = screen;
sbAll.append(screen).append("\n");
if (upper.contains("***")) {
ps.SendKeys("[enter]");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
if (session.getOIA() != null) {
session.getOIA().waitForInput(2000);
}
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")) {
ps.SendKeys("[enter]");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
if (session.getOIA() != null) {
session.getOIA().waitForInput(1000);
}
int r2 = ps.getRows();
int c2 = ps.getCols();
String s2 = ps.copyString(0, 0, r2 - 1, c2 - 1);
if (s2.toUpperCase().contains("***")) {
prevScreen = s2;
sbAll.append(s2).append("\n");
ps.SendKeys("[enter]");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
if (session.getOIA() != null) {
session.getOIA().waitForInput(2000);
}
continue;
} else {
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
sbAll.append(s2).append("\n");
}
break;
}
} else {
break;
}
}
// Terminal clean-up
if (isIspf) {
ps.SendKeys("[enter]");
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
if (session.getOIA() != null) {
session.getOIA().waitForInput(1500);
}
int r = ps.getRows();
int c = ps.getCols();
String currentScr = ps.copyString(0, 0, r - 1, c - 1);
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|| currentScr.contains(" --- ") || (sb != null && !sb.isFormatted())) {
ps.SendKeys("[clear]");
if (session.getOIA() != null) {
session.getOIA().waitForInput(2000);
}
}
} else {
ps.SendKeys("[clear]");
if (session.getOIA() != null) {
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
session.getOIA().waitForInput(2000);
}
}
}
dataToParse = sbAll.toString();
} catch (Exception ignored) {
// Fall back to hostQuery
}
}
}
try { try {
if (hostType == VM_CMS) { if (hostType == VM_CMS) {
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING); if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING);
List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(hostQuery); List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(dataToParse);
for (CMSDirectoryEntry entry : cmsEntries) { for (CMSDirectoryEntry entry : cmsEntries) {
String hName = entry.getFilename() + " " + entry.getFiletype(); String hName = entry.getFilename() + " " + entry.getFiletype();
String lName = entry.getFilename() + "." + entry.getFiletype(); String lName = entry.getFilename() + "." + entry.getFiletype();
@@ -622,7 +872,7 @@ public class Xfer3270 implements FileTransferInterface {
} }
} else { } else {
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING); if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING);
List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(hostQuery); List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(dataToParse);
for (TSODirectoryEntry entry : tsoEntries) { for (TSODirectoryEntry entry : tsoEntries) {
String name = entry.getName(); String name = entry.getName();
if (hostFiles != null) hostFiles.addElement(name); if (hostFiles != null) hostFiles.addElement(name);
@@ -0,0 +1,204 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class DBCSDataStreamFullTest {
@Test
public void testDBCSDataStreamShiftOutShiftIn() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// Data stream:
// EW, WCC, SBA (0, 0), SO (0x0E), 0x40 0x40 (\u3000 ideographic space), SI (0x0F)
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // Shift-Out
(byte) 0x40, (byte) 0x40, // DBCS ideographic space
(byte) 0x0F // Shift-In
};
processor.processRecord(stream, 0, stream.length, true);
ExtendedAttribute cell0 = screen.getCell(0);
assertEquals(ExtendedAttribute.DB_SO, cell0.db, "Cell 0 must be DB_SO");
ExtendedAttribute cell1 = screen.getCell(1);
assertEquals(ExtendedAttribute.DB_LEFT, cell1.db, "Cell 1 must be DB_LEFT");
assertEquals('\u3000', cell1.ucs4, "Cell 1 ucs4 must be \\u3000");
ExtendedAttribute cell2 = screen.getCell(2);
assertEquals(ExtendedAttribute.DB_RIGHT, cell2.db, "Cell 2 must be DB_RIGHT");
assertEquals('\u3000', cell2.ucs4, "Cell 2 ucs4 must be \\u3000");
ExtendedAttribute cell3 = screen.getCell(3);
assertEquals(ExtendedAttribute.DB_SI, cell3.db, "Cell 3 must be DB_SI");
}
@Test
public void testDBCSRepeatToAddressOrder() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// In DBCS mode (after SO), ORDER_RA consumes 2 data bytes and fills pairs
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // SO at 0
(byte) ORDER_RA,
(byte) 0x40, (byte) 0x4B, // RA up to address 11 (covers cells 1..10)
(byte) 0x40, (byte) 0x40, // 2-byte repeat char (\u3000)
(byte) 0x0F // SI at 11
};
processor.processRecord(stream, 0, stream.length, true);
assertEquals(ExtendedAttribute.DB_SO, screen.getCell(0).db);
for (int i = 1; i < 11; i += 2) {
assertEquals(ExtendedAttribute.DB_LEFT, screen.getCell(i).db, "Cell " + i + " must be DB_LEFT");
assertEquals('\u3000', screen.getCell(i).ucs4);
assertEquals(ExtendedAttribute.DB_RIGHT, screen.getCell(i + 1).db, "Cell " + (i + 1) + " must be DB_RIGHT");
assertEquals('\u3000', screen.getCell(i + 1).ucs4);
}
assertEquals(ExtendedAttribute.DB_SI, screen.getCell(11).db);
}
@Test
public void testECLPlanesDBCSAndOutlining() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // SO at 0
(byte) 0x40, (byte) 0x40, // DBCS at 1, 2
(byte) 0x0F // SI at 3
};
processor.processRecord(stream, 0, stream.length, true);
// Add outlining to cell 1
screen.getCell(1).ol = (byte) (ExtendedAttribute.OL_BOX);
ECLPS eclPS = new ECLPS(screen, null, translator);
char[] dbcsPlane = new char[4];
int count = eclPS.getPlane(ECLConstants.DBCS_PLANE, dbcsPlane, 0, 4);
assertEquals(4, count);
assertEquals(0x20, dbcsPlane[0], "SO must be 0x20 in DBCS plane");
assertEquals(0xC0, dbcsPlane[1], "DB_LEFT must be 0xC0 (DB_DBHIGH)");
assertEquals(0x80, dbcsPlane[2], "DB_RIGHT must be 0x80 (DB_DBCS)");
assertEquals(0x10, dbcsPlane[3], "SI must be 0x10 in DBCS plane");
char[] gridPlane = new char[4];
eclPS.getPlane(ECLConstants.GRID_PLANE, gridPlane, 0, 4);
assertEquals((char) (ExtendedAttribute.OL_BOX & 0xFF), gridPlane[1]);
// Helper methods
assertTrue(eclPS.IsSOChar(0));
assertFalse(eclPS.IsDBCSChar(0));
assertTrue(eclPS.IsDBCSChar(1));
assertTrue(eclPS.IsDBCS1stChar(1));
assertFalse(eclPS.IsDBCS2ndChar(1));
assertTrue(eclPS.IsDBCSChar(2));
assertFalse(eclPS.IsDBCS1stChar(2));
assertTrue(eclPS.IsDBCS2ndChar(2));
assertTrue(eclPS.IsSIChar(3));
assertFalse(eclPS.IsDBCSChar(3));
}
@Test
public void testECLFieldDBCSMethods() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// Field with SO + DBCS + SI
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) ORDER_SF,
(byte) FA_PRINTABLE, // Field at 0
(byte) 0x0E, // SO at 1
(byte) 0x40, (byte) 0x40, // DBCS at 2, 3
(byte) 0x0F // SI at 4
};
processor.processRecord(stream, 0, stream.length, true);
ECLPS eclPS = new ECLPS(screen, null, translator);
ECLFieldList fieldList = new ECLFieldList(eclPS);
fieldList.Refresh();
assertEquals(1, fieldList.size());
ECLField field = fieldList.getField(0);
assertTrue(field.isDBCS(), "Field should report isDBCS = true");
assertTrue(field.isDBCSOnly(), "Field should report isDBCSOnly = true");
}
@Test
public void testQueryReplyBuilderDBCSCharsets() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
QueryReplyBuilder builder = new QueryReplyBuilder(screen);
byte[] charsets = builder.buildCharsets();
assertNotNull(charsets);
assertEquals(42, charsets.length, "DBCS Character Sets QR payload must be 42 bytes (matching HoD xxxqrcs)");
assertEquals((byte) 0x8E, charsets[0], "Flags 1 must be 0x8E");
assertEquals((byte) 0x00, charsets[1], "Flags 2 must be 0x00");
assertEquals(12, charsets[2], "SDW must be 12");
assertEquals(24, charsets[3], "SDH must be 24");
assertEquals(0x0B, charsets[8], "Descriptor length DL must be 11 (0x0B)");
// Descriptor 2 (DBCS): offset 9 + 11 = 20
assertEquals(0x01, charsets[20], "Set ID must be 1");
assertEquals((byte) 0x80, charsets[22], "Local ID must be 0x80 (DBCS)");
assertEquals(24, charsets[23], "DBCS cell width must be double (24)");
assertEquals(24, charsets[24], "DBCS cell height must be 24");
assertEquals(0x41, charsets[25], "Section start must be 0x41");
assertEquals(0x7F, charsets[26], "Section end for 930 must be 0x7F");
// DB CGCSGID = 370 (0x0172), DB CPGID = 300 (0x012C)
int cgcsgid = ((charsets[27] & 0xFF) << 8) | (charsets[28] & 0xFF);
int cpgid = ((charsets[29] & 0xFF) << 8) | (charsets[30] & 0xFF);
assertEquals(370, cgcsgid);
assertEquals(300, cpgid);
// Summary Query Reply contains Outlining (0x8C) and DBCS Asia (0x91)
byte[] summary = builder.buildSummary();
boolean hasOutlining = false;
boolean hasDbcsAsia = false;
for (byte b : summary) {
if ((b & 0xFF) == QR_OUTLINING) hasOutlining = true;
if ((b & 0xFF) == QR_DBCS_ASIA) hasDbcsAsia = true;
}
assertTrue(hasOutlining, "Summary must include QR_OUTLINING");
assertTrue(hasDbcsAsia, "Summary must include QR_DBCS_ASIA");
}
}
@@ -271,4 +271,156 @@ public class DataStreamProcessorTest {
assertEquals('8', screen.getCell(12).ucs4); assertEquals('8', screen.getCell(12).ucs4);
assertEquals('9', screen.getCell(13).ucs4); assertEquals('9', screen.getCell(13).ucs4);
} }
@Test
public void testReadPartitionReadModifiedReturnsAidQreply() {
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
processor.setOutputSender(out::writeBytes);
// Send WSF Read Partition with SNA_CMD_RM (0xF6)
byte[] sfRm = new byte[] {
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RM
};
out.reset();
processor.processRecord(sfRm, 0, sfRm.length, true);
byte[] sent = out.toByteArray();
assertTrue(sent.length >= 3, "Inbound data stream must contain at least AID and cursor address");
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified AID must be AID_QREPLY (0x61)");
// Send WSF Read Partition with CMD_RM (0x06)
byte[] sfRmNonSna = new byte[] {
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) CMD_RM
};
out.reset();
processor.processRecord(sfRmNonSna, 0, sfRmNonSna.length, true);
sent = out.toByteArray();
assertTrue(sent.length >= 3);
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified (non-SNA) AID must be AID_QREPLY (0x61)");
}
@Test
public void testReadPartitionReadModifiedAllReturnsAidQreply() {
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
processor.setOutputSender(out::writeBytes);
// Send WSF Read Partition with SNA_CMD_RMA (0x6E)
byte[] sfRma = new byte[] {
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RMA
};
out.reset();
processor.processRecord(sfRma, 0, sfRma.length, true);
byte[] sent = out.toByteArray();
assertTrue(sent.length >= 3);
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified All AID must be AID_QREPLY (0x61)");
}
@Test
public void testReadPartitionReadBufferReturnsAidQreply() {
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
processor.setOutputSender(out::writeBytes);
// Send WSF Read Partition with SNA_CMD_RB (0xF2)
byte[] sfRb = new byte[] {
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RB
};
out.reset();
processor.processRecord(sfRb, 0, sfRb.length, true);
byte[] sent = out.toByteArray();
assertTrue(sent.length >= 3);
assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Buffer AID must be AID_QREPLY (0x61)");
}
@Test
public void testReadModifiedAfterQueryRespondsWithQueryReply() {
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
processor.setOutputSender(out::writeBytes);
// 1. Send WSF Read Partition Query
byte[] sfQuery = new byte[] {
(byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, (byte) 0xFF, (byte) SF_RP_QUERY
};
out.reset();
processor.processRecord(sfQuery, 0, sfQuery.length, true);
assertTrue(processor.isQreplyPending(), "qreplyPending should be true after query");
// 2. Mainframe issues channel Read Modified command (0x06) to poll the query reply
byte[] rmCmd = new byte[] { (byte) CMD_RM };
out.reset();
processor.processRecord(rmCmd, 0, rmCmd.length, true);
byte[] sent = out.toByteArray();
assertTrue(sent.length > 0, "Should have received query reply data");
assertEquals((byte) AID_SF, sent[0], "Query reply header AID must be AID_SF (0x88)");
assertFalse(processor.isQreplyPending(), "qreplyPending should be cleared after reply is sent");
}
@Test
public void testStandaloneReadModifiedReturnsAidNo() {
haus.nightmare.lib3270j.input.InputProcessor input = new haus.nightmare.lib3270j.input.InputProcessor(screen, translator, null);
processor.setInputProcessor(input);
// Simulate operator previously pressing Enter
input.setLastAid(AID_ENTER);
input.setKeyboardLocked(true);
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
processor.setOutputSender(out::writeBytes);
// Host responds with Erase/Write Alternate (0x7E) with WCC 0xC3 (kbdRestore=true)
byte[] ewa = new byte[] { (byte) SNA_CMD_EWA, (byte) 0xC3 };
processor.processRecord(ewa, 0, ewa.length, true);
assertFalse(input.isKeyboardLocked(), "Keyboard should be restored by WCC");
assertEquals(AID_NO, input.getLastAid(), "lastAid should be reset to AID_NO on keyboard restore");
// Host subsequently polls terminal with standalone Read Modified (0xF6)
byte[] rm = new byte[] { (byte) SNA_CMD_RM };
out.reset();
processor.processRecord(rm, 0, rm.length, true);
byte[] sent = out.toByteArray();
assertTrue(sent.length >= 3, "Read Modified response must contain at least AID and 2-byte cursor address");
assertEquals((byte) AID_NO, sent[0], "Standalone Read Modified from host must respond with AID_NO (0x60)");
assertFalse(input.isKeyboardLocked(), "Keyboard must remain unlocked after host Read Modified poll");
}
@Test
public void testStandaloneReadModifiedWithModifiedFieldSendsDataAndAidNo() {
haus.nightmare.lib3270j.input.InputProcessor input = new haus.nightmare.lib3270j.input.InputProcessor(screen, translator, null);
processor.setInputProcessor(input);
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
processor.setOutputSender(out::writeBytes);
// Write a screen with an unprotected field at pos 10, WCC kbdRestore
byte[] initStream = new byte[] {
(byte) CMD_EW, (byte) 0xC3,
(byte) ORDER_SBA, 0x00, 0x0A, // SBA 10
(byte) ORDER_SF, (byte) FA_PRINTABLE, // unprotected
(byte) 0xC1, (byte) 0xC2 // EBCDIC 'A', 'B'
};
processor.processRecord(initStream, 0, initStream.length, true);
// Modify the field by setting MDT (e.g. user typed into it)
screen.getCell(10).fa = (byte) (screen.getCell(10).fa | FA_MODIFY);
// Host polls with 0xF6 (SNA_CMD_RM)
out.reset();
byte[] rm = new byte[] { (byte) SNA_CMD_RM };
processor.processRecord(rm, 0, rm.length, true);
byte[] sent = out.toByteArray();
assertTrue(sent.length > 3, "Should include SBA and field contents for modified field");
assertEquals((byte) AID_NO, sent[0], "AID must be AID_NO (0x60)");
assertEquals((byte) ORDER_SBA, sent[3], "Order must be SBA");
assertEquals((byte) 0xC1, sent[6], "First char should be 'A'");
assertEquals((byte) 0xC2, sent[7], "Second char should be 'B'");
assertTrue((screen.getCell(10).fa & FA_MODIFY) != 0, "MDT must not be cleared by Read Modified command");
}
} }
@@ -49,9 +49,24 @@ public class QueryReplyBuilderTest {
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary // In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary
int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
boolean hasB0 = false;
boolean hasA8 = false;
for (int i = 5; i < 1 + sumLen; i++) { for (int i = 5; i < 1 + sumLen; i++) {
assertNotEquals(QR_SEGMENT, replies[i] & 0xFF); if ((replies[i] & 0xFF) == QR_SEGMENT) hasB0 = true;
if ((replies[i] & 0xFF) == QR_TRANSPARENCY) hasA8 = true;
} }
assertFalse(hasB0);
assertTrue(hasA8, "QR_TRANSPARENCY (0xA8) should be advertised even in GraphicsMode.NONE");
// And QR_TRANSPARENCY structured field payload must be present in replies
boolean hasA8Payload = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_TRANSPARENCY) {
hasA8Payload = true;
break;
}
}
assertTrue(hasA8Payload, "QR_TRANSPARENCY structured field should be included in buildAllQueryReplies");
} }
@Test @Test
@@ -87,10 +87,10 @@ public class QueryReplyPhase2Test {
assertNotNull(reply); assertNotNull(reply);
List<ParsedSF> sfs = parseQueryReplies(reply); List<ParsedSF> sfs = parseQueryReplies(reply);
// Base query reply must contain exactly 10 structured fields for SBCS: // Base query reply must contain exactly 11 structured fields for SBCS:
// 0x80 (Summary), 0x81 (Usable Area), 0x84 (Alpha Partitions), 0x85 (Character Sets), // 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) // 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part), 0xA8 (Transparency)
assertEquals(10, sfs.size()); assertEquals(11, sfs.size());
assertEquals(QR_SUMMARY, sfs.get(0).qcode); assertEquals(QR_SUMMARY, sfs.get(0).qcode);
assertEquals(QR_USABLE_AREA, sfs.get(1).qcode); assertEquals(QR_USABLE_AREA, sfs.get(1).qcode);
@@ -102,10 +102,10 @@ public class QueryReplyPhase2Test {
assertEquals(QR_DDM, sfs.get(7).qcode); assertEquals(QR_DDM, sfs.get(7).qcode);
assertEquals(QR_AUXDA, sfs.get(8).qcode); assertEquals(QR_AUXDA, sfs.get(8).qcode);
assertEquals(QR_IMP_PART, sfs.get(9).qcode); assertEquals(QR_IMP_PART, sfs.get(9).qcode);
assertEquals(QR_TRANSPARENCY, sfs.get(10).qcode);
// Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply // Verify that Vector Graphics SF payloads (0xB0..0xB6) are NOT sent in base reply
for (ParsedSF sf : sfs) { for (ParsedSF sf : sfs) {
assertNotEquals(QR_TRANSPARENCY, sf.qcode);
assertNotEquals(QR_SEGMENT, sf.qcode); assertNotEquals(QR_SEGMENT, sf.qcode);
assertNotEquals(QR_PROCEDURE, sf.qcode); assertNotEquals(QR_PROCEDURE, sf.qcode);
assertNotEquals(QR_LINETYPE, sf.qcode); assertNotEquals(QR_LINETYPE, sf.qcode);
@@ -246,7 +246,7 @@ public class QueryReplyPhase2Test {
assertTrue(sent.length > 0); assertTrue(sent.length > 0);
assertEquals((byte) AID_SF, sent[0]); assertEquals((byte) AID_SF, sent[0]);
List<ParsedSF> sfs = parseQueryReplies(sent); List<ParsedSF> sfs = parseQueryReplies(sent);
assertEquals(10, sfs.size()); assertEquals(11, sfs.size());
// Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4 // Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4
byte[] queryList = new byte[] { byte[] queryList = new byte[] {
@@ -0,0 +1,143 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TransparencyDataStreamTest {
private ScreenBuffer screen;
private DataStreamProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
EbcdicTranslator translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
processor = new DataStreamProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
@Test
public void testSetAttributeTransparency() {
// Send SFE with XA_TRANSPARENCY = XAT_OPAQUE (0xFF)
byte[] sfeRecord = new byte[] {
(byte) CMD_EW, 0x00,
(byte) ORDER_SFE, 0x02,
(byte) XA_3270, (byte) FA_PRINTABLE,
(byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE
};
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
ExtendedAttribute cell0 = screen.getCell(0);
assertTrue(cell0.isFieldAttribute());
assertEquals(ExtendedAttribute.TR_OPAQUE, cell0.tr);
// Send SA with XA_TRANSPARENCY = XAT_OPAQUE followed by character 'A' (0xC1)
byte[] saRecord = new byte[] {
(byte) CMD_W, 0x00,
(byte) ORDER_SBA, 0x00, 0x01,
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
(byte) 0xC1 // 'A'
};
processor.processRecord(saRecord, 0, saRecord.length, true);
ExtendedAttribute charCell1 = screen.getCell(1);
assertFalse(charCell1.isFieldAttribute());
assertEquals((byte) 0xC1, charCell1.ec);
assertEquals(ExtendedAttribute.TR_OPAQUE, charCell1.tr);
// Send SA with XA_ALL (0x00) followed by character 'B' (0xC2) -> should reset tr to 0
byte[] resetRecord = new byte[] {
(byte) CMD_W, 0x00,
(byte) ORDER_SBA, 0x00, 0x02,
(byte) ORDER_SA, (byte) XA_ALL, 0x00,
(byte) 0xC2 // 'B'
};
processor.processRecord(resetRecord, 0, resetRecord.length, true);
ExtendedAttribute charCell2 = screen.getCell(2);
assertFalse(charCell2.isFieldAttribute());
assertEquals((byte) 0xC2, charCell2.ec);
assertEquals(ExtendedAttribute.TR_DEFAULT, charCell2.tr);
}
@Test
public void testRepeatToAddressWithTransparency() {
// Set SA Transparency = Opaque, then RA to address 5 with char 'C' (0xC3)
byte[] raRecord = new byte[] {
(byte) CMD_EW, 0x00,
(byte) ORDER_SBA, 0x00, 0x00,
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
(byte) ORDER_RA, 0x00, 0x05, (byte) 0xC3
};
processor.processRecord(raRecord, 0, raRecord.length, true);
for (int i = 0; i < 5; i++) {
ExtendedAttribute cell = screen.getCell(i);
assertEquals((byte) 0xC3, cell.ec);
assertEquals(ExtendedAttribute.TR_OPAQUE, cell.tr, "Cell at " + i + " should have TR_OPAQUE");
}
}
@Test
public void testReadBufferExtendedFieldIncludesTransparency() {
screen.setReplyMode((byte) SF_SRM_XFIELD);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
processor.processReadBuffer();
byte[] result = output.toByteArray();
assertTrue(result.length > 3);
assertEquals((byte) AID_NO, result[0]);
// Find ORDER_SFE
boolean foundTr = false;
for (int i = 3; i < result.length - 2; i++) {
if ((result[i] & 0xFF) == ORDER_SFE) {
int pairCount = result[i + 1] & 0xFF;
int pos = i + 2;
for (int p = 0; p < pairCount; p++) {
int attrType = result[pos] & 0xFF;
int attrVal = result[pos + 1] & 0xFF;
if (attrType == XA_TRANSPARENCY && attrVal == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
foundTr = true;
}
pos += 2;
}
}
}
assertTrue(foundTr, "Read Buffer in SF_SRM_XFIELD mode should report XA_TRANSPARENCY");
}
@Test
public void testReadBufferCharacterModeIncludesTransparency() {
screen.setReplyMode((byte) SF_SRM_CHAR);
screen.getCell(0).ec = (byte) 0xC1;
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
processor.processReadBuffer();
byte[] result = output.toByteArray();
assertTrue(result.length > 3);
boolean foundSaTr = false;
for (int i = 3; i < result.length - 2; i++) {
if ((result[i] & 0xFF) == ORDER_SA && (result[i + 1] & 0xFF) == XA_TRANSPARENCY && (result[i + 2] & 0xFF) == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
foundSaTr = true;
break;
}
}
assertTrue(foundSaTr, "Read Buffer in SF_SRM_CHAR mode should output ORDER_SA for XA_TRANSPARENCY");
}
}
@@ -14,7 +14,7 @@ public class FTConfigTest {
config.setTransferMode(FTConfig.TransferMode.ASCII); config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE); config.setCrAction(FTConfig.CrAction.REMOVE);
assertEquals("IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand()); assertEquals("TSO IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
} }
@Test @Test
@@ -25,7 +25,7 @@ public class FTConfigTest {
config.setHostFilename("'USER.BIN.DATA'"); config.setHostFilename("'USER.BIN.DATA'");
config.setTransferMode(FTConfig.TransferMode.BINARY); config.setTransferMode(FTConfig.TransferMode.BINARY);
assertEquals("IND$FILE GET 'USER.BIN.DATA'", config.buildCommand()); assertEquals("TSO IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
} }
@Test @Test
@@ -40,7 +40,7 @@ public class FTConfigTest {
config.setLrecl("80"); config.setLrecl("80");
config.setBlksize("3120"); config.setBlksize("3120");
assertEquals("IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand()); assertEquals("TSO IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
} }
@Test @Test
@@ -125,4 +125,66 @@ public class FTConfigTest {
String cmd = config.buildCommand(); String cmd = config.buildCommand();
assertEquals("IND$FILE PUT FILE01 (BINARY NOCRLF REPLACE", cmd); assertEquals("IND$FILE PUT FILE01 (BINARY NOCRLF REPLACE", cmd);
} }
@Test
public void testPdsAllocationCommand() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("'USER.PDS.DATA'");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRecfm("FB");
config.setLrecl("80");
config.setBlksize("3120");
config.setPrimarySpace(15);
config.setSecondarySpace(5);
config.setDirectoryBlocks(10);
config.setUnits(FTConfig.AllocationUnit.TRACKS);
String cmd = config.buildCommand();
assertEquals("TSO IND$FILE PUT 'USER.PDS.DATA' ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(15,5,10) TRACKS", cmd);
}
@Test
public void testPdsParseOptionsThreeParameters() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("SYS1.PARMLIB");
config.parseOptions("TSO ASCII CRLF SPACE(20,10,15) TRACKS");
assertEquals(20, config.getPrimarySpace());
assertEquals(10, config.getSecondarySpace());
assertEquals(15, config.getDirectoryBlocks());
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
assertEquals("TSO IND$FILE PUT SYS1.PARMLIB ASCII CRLF SPACE(20,10,15) TRACKS", config.buildCommand());
}
@Test
public void testPdsParseOptionsWithDirToken() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("SYS1.PROCLIB");
config.parseOptions("TSO ASCII CRLF SPACE(10,5) DIR(20) CYLINDERS");
assertEquals(10, config.getPrimarySpace());
assertEquals(5, config.getSecondarySpace());
assertEquals(20, config.getDirectoryBlocks());
assertEquals(FTConfig.AllocationUnit.CYLINDERS, config.getUnits());
assertEquals("TSO IND$FILE PUT SYS1.PROCLIB ASCII CRLF SPACE(10,5,20) CYLINDERS", config.buildCommand());
}
@Test
public void testPdsSetSpaceWithThreeArguments() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("MY.PDS");
config.setSpace("12,4,8");
assertEquals(12, config.getPrimarySpace());
assertEquals(4, config.getSecondarySpace());
assertEquals(8, config.getDirectoryBlocks());
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
assertTrue(config.buildCommand().contains("SPACE(12,4,8) TRACKS"));
}
} }
@@ -443,7 +443,7 @@ public class FTProtocolHardeningPhase16Test {
assertEquals("APVUFILE", optionsCfg.getProgramName()); assertEquals("APVUFILE", optionsCfg.getProgramName());
String cmd = optionsCfg.buildCommand(); String cmd = optionsCfg.buildCommand();
assertTrue(cmd.startsWith("APVUFILE PUT 'TEST.FILE'")); assertTrue(cmd.startsWith("TSO APVUFILE PUT 'TEST.FILE'"));
assertTrue(cmd.contains("NEW")); assertTrue(cmd.contains("NEW"));
assertTrue(cmd.contains("NOSO")); assertTrue(cmd.contains("NOSO"));
} }
@@ -68,6 +68,8 @@ public class DirectoryParserTest {
assertEquals(80, e0.getLrecl()); assertEquals(80, e0.getLrecl());
assertEquals(3120, e0.getBlksize()); assertEquals(3120, e0.getBlksize());
assertEquals("PS", e0.getDsorg()); assertEquals("PS", e0.getDsorg());
assertEquals(15, e0.getTracksAllocated());
assertEquals(1, e0.getExtents());
TSODirectoryEntry e1 = entries.get(1); TSODirectoryEntry e1 = entries.get(1);
assertEquals("TSOUSER.OTHER.DATA", e1.getName()); assertEquals("TSOUSER.OTHER.DATA", e1.getName());
@@ -121,4 +123,213 @@ public class DirectoryParserTest {
assertEquals("GENPASS", entries.get(1).getFilename()); assertEquals("GENPASS", entries.get(1).getFilename());
assertEquals("REXX", entries.get(1).getFiletype()); assertEquals("REXX", entries.get(1).getFiletype());
} }
@Test
public void testContinuousScreenBufferParsingCms() {
// Continuous 80-character rows without newlines (as produced by unformatted 3270 presentation space)
String row1 = String.format("%-80s", "PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01");
String row2 = String.format("%-80s", "TEST DATA A1 F 80 100 2 2026-08-01 09:15:00");
String row3 = String.format("%-80s", "Ready; T=0.01/0.01 12:00:00");
String continuousScreen = row1 + row2 + row3;
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(continuousScreen);
assertEquals(2, entries.size(), "Should parse 2 CMS entries and ignore Ready;");
assertEquals("PROFILE", entries.get(0).getFilename());
assertEquals("TEST", entries.get(1).getFilename());
}
@Test
public void testContinuousScreenBufferParsingTso() {
// Continuous 80-character rows without newlines for ISPF DSLIST
String row1 = String.format("%-80s", "Dslist - Data Sets Matching USER.*");
String row2 = String.format("%-80s", "Command - Enter \"/\" to select action Tracks Dsorg Recfm Volume");
String row3 = String.format("%-80s", " USER.TEST.CNTL 15 PO FB TSO001");
String row4 = String.format("%-80s", " USER.TEST.DATA 10 PS FB TSO001");
String row5 = String.format("%-80s", "***");
String continuousScreen = row1 + row2 + row3 + row4 + row5;
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(continuousScreen);
assertEquals(2, entries.size(), "Should parse 2 TSO entries and ignore ***");
assertEquals("USER.TEST.CNTL", entries.get(0).getName());
assertEquals("USER.TEST.DATA", entries.get(1).getName());
}
@Test
public void testCmsMoreRunningAndReadyHandling() {
String cmsOutput =
"PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01 MORE...\n" +
"MORE...\n" +
"RUNNING\n" +
"HOLDING\n" +
"TEST DATA A1 F 80 100 2 2026-08-01 09:15:00\n" +
"OTHER FILE A1 V 80 50 1 2026-08-02 10:00:00 RUNNING\n" +
"Ready(00000); T=0.01/0.01 12:00:00\n";
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(cmsOutput);
assertEquals(3, entries.size(), "Should parse 3 CMS entries and strip/ignore MORE..., RUNNING, HOLDING, Ready(00000)");
assertEquals("PROFILE", entries.get(0).getFilename());
assertEquals("TEST", entries.get(1).getFilename());
assertEquals("OTHER", entries.get(2).getFilename());
}
@Test
public void testTsoListcatWithClusterAndGdg() {
String listcatOutput =
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
"NONVSAM ------- TSOUSER.NORMAL.DATA\n" +
" VOLUMES\n" +
" VOLSER------------TSO001\n" +
"CLUSTER ------- TSOUSER.VSAM.KSDS\n" +
" IN-CAT --- CATALOG.USER\n" +
"GDG ----------- TSOUSER.GDG.BASE\n" +
"ALIAS --------- TSOUSER.MY.ALIAS\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
assertEquals(4, entries.size(), "Should parse NONVSAM, CLUSTER, GDG, and ALIAS");
assertEquals("TSOUSER.NORMAL.DATA", entries.get(0).getName());
assertEquals("TSO001", entries.get(0).getVolume());
assertEquals("TSOUSER.VSAM.KSDS", entries.get(1).getName());
assertEquals("VSAM", entries.get(1).getDsorg());
assertEquals("TSOUSER.GDG.BASE", entries.get(2).getName());
assertEquals("TSOUSER.MY.ALIAS", entries.get(3).getName());
}
@Test
public void testTsoListcatAllocDetails() {
String allocOutput =
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
"NONVSAM ------- TSOUSER.PROJECT.DATA\n" +
" IN-CAT --- CATALOG.USER\n" +
" VOLUMES\n" +
" VOLSER------------TSOP01 DEVTYPE------X'3010200F'\n" +
" ALLOCATION\n" +
" SPACE-TYPE---------TRACK SPACE-PRI----------------15 SPACE-SEC----------------5\n" +
" EXTENTS-----------------1\n" +
"CLUSTER ------- TSOUSER.VSAM.TEST\n" +
" IN-CAT --- CATALOG.USER\n" +
" DATA ------- TSOUSER.VSAM.TEST.DATA\n" +
" VOLUMES\n" +
" VOLSER------------TSOP02\n" +
" ALLOCATION\n" +
" SPACE-TYPE------CYLINDER SPACE-PRI----------------2\n" +
" EXTENTS-----------------2\n" +
" ATTRIBUTES\n" +
" AVGLRECL--------------80 MAXLRECL----------------80 CISIZE----------------4096\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(allocOutput);
assertEquals(2, entries.size());
TSODirectoryEntry nonvsam = entries.get(0);
assertEquals("TSOUSER.PROJECT.DATA", nonvsam.getName());
assertEquals("TSOP01", nonvsam.getVolume());
assertEquals(15, nonvsam.getTracksAllocated());
assertEquals(1, nonvsam.getExtents());
TSODirectoryEntry vsam = entries.get(1);
assertEquals("TSOUSER.VSAM.TEST", vsam.getName());
assertEquals("VSAM", vsam.getDsorg());
assertEquals("TSOP02", vsam.getVolume());
assertEquals(30, vsam.getTracksAllocated()); // 2 cylinders * 15 tracks
assertEquals(2, vsam.getExtents());
assertEquals(80, vsam.getLrecl());
assertEquals(4096, vsam.getBlksize());
}
@Test
public void testTsoListdsParsing() {
String listdsOutput =
"TSOUSER.MY.SOURCE\n" +
"--RECFM-LRECL-BLKSIZE-DSORG\n" +
" FB 80 3120 PO\n" +
"--VOLUMES--\n" +
" SRC001\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listdsOutput);
assertEquals(1, entries.size());
TSODirectoryEntry e = entries.get(0);
assertEquals("TSOUSER.MY.SOURCE", e.getName());
assertEquals("FB", e.getRecfm());
assertEquals(80, e.getLrecl());
assertEquals(3120, e.getBlksize());
assertEquals("PO", e.getDsorg());
assertEquals("SRC001", e.getVolume());
}
@Test
public void testTsoListdsWithStatusAllocationParsing() {
String listdsStatusOutput =
"SYS1.ACMDLIB\n" +
"--RECFM-LRECL-BLKSIZE-DSORG\n" +
" U 0 32760 PO\n" +
"--VOLUMES--\n" +
" TK5DLB\n" +
"--ALLOCATION---------------\n" +
" SPACE-TYPE TRACK\n" +
" SPACE-PRI 30\n" +
" SPACE-SEC 5\n" +
"--TRACKS-ALLOCATED-EXTENTS-\n" +
" 30 1\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listdsStatusOutput);
assertEquals(1, entries.size());
TSODirectoryEntry e = entries.get(0);
assertEquals("SYS1.ACMDLIB", e.getName());
assertEquals("U", e.getRecfm());
assertEquals(0, e.getLrecl());
assertEquals(32760, e.getBlksize());
assertEquals("PO", e.getDsorg());
assertEquals("TK5DLB", e.getVolume());
assertEquals(30, e.getTracksAllocated());
assertEquals(1, e.getExtents());
}
@Test
public void testTsoTabularDslispParsing() {
String tabular = " USER.SAMPLE.COBOL MNT01 15 50 1 3390 PO FB 80 3120\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(tabular);
assertEquals(1, entries.size());
TSODirectoryEntry e = entries.get(0);
assertEquals("USER.SAMPLE.COBOL", e.getName());
assertEquals("MNT01", e.getVolume());
assertEquals(15, e.getTracksAllocated());
assertEquals(50, e.getPercentUsed());
assertEquals(1, e.getExtents());
assertEquals("3390", e.getDevice());
assertEquals("PO", e.getDsorg());
assertEquals("FB", e.getRecfm());
assertEquals(80, e.getLrecl());
assertEquals(3120, e.getBlksize());
}
@Test
public void testTsoListcatWithSysresAsterisksVolume() {
String listcatOutput =
"IDCAMS SYSTEM SERVICES TIME: 02:32:00\n" +
"NONVSAM ------- SYS1.ACMDLIB\n" +
" IN-CAT --- SYS1.MCAT.TK5\n" +
" HISTORY\n" +
" OWNER-IDENT-------(NULL) CREATION----------23.146\n" +
" RELEASE----------------2 EXPIRATION--------00.000\n" +
" VOLUMES\n" +
" VOLSER------------****** DEVTYPE------X'3010200F' FSEQN------------------0\n" +
" ASSOCIATIONS--------(NULL)\n" +
"NONVSAM ------- SYS1.SYSTEM.CMDSBSYS.CSCDATA\n" +
" IN-CAT --- SYS1.MCAT.TK5\n" +
" HISTORY\n" +
" OWNER-IDENT-------(NULL) CREATION----------23.146\n" +
" VOLUMES\n" +
" VOLSER------------TK5002 DEVTYPE------X'3010200F' FSEQN------------------0\n" +
" ASSOCIATIONS--------(NULL)\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
assertEquals(2, entries.size());
TSODirectoryEntry sysresEntry = entries.get(0);
assertEquals("SYS1.ACMDLIB", sysresEntry.getName());
assertEquals("******", sysresEntry.getVolume(), "SYSRES volume with asterisks should be preserved");
TSODirectoryEntry explicitEntry = entries.get(1);
assertEquals("SYS1.SYSTEM.CMDSBSYS.CSCDATA", explicitEntry.getName());
assertEquals("TK5002", explicitEntry.getVolume());
}
} }
@@ -54,6 +54,7 @@ public class Mock3270Server implements AutoCloseable {
private int sequenceNumber = 0; private int sequenceNumber = 0;
private final List<byte[]> receivedRecords = new CopyOnWriteArrayList<>(); private final List<byte[]> receivedRecords = new CopyOnWriteArrayList<>();
private final List<int[]> receivedCommands = new CopyOnWriteArrayList<>();
private final AtomicInteger receivedNops = new AtomicInteger(0); private final AtomicInteger receivedNops = new AtomicInteger(0);
private final AtomicInteger receivedTimingMarks = new AtomicInteger(0); private final AtomicInteger receivedTimingMarks = new AtomicInteger(0);
@@ -66,7 +67,9 @@ public class Mock3270Server implements AutoCloseable {
} }
public Mock3270Server(int requestedPort) throws IOException { public Mock3270Server(int requestedPort) throws IOException {
this.serverSocket = new ServerSocket(requestedPort, 10, InetAddress.getByName("127.0.0.1")); this.serverSocket = new ServerSocket();
this.serverSocket.setReuseAddress(true);
this.serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), requestedPort), 10);
this.port = serverSocket.getLocalPort(); this.port = serverSocket.getLocalPort();
this.translator = new EbcdicTranslator(); this.translator = new EbcdicTranslator();
executor.submit(this::acceptLoop); executor.submit(this::acceptLoop);
@@ -126,6 +129,10 @@ public class Mock3270Server implements AutoCloseable {
return Collections.unmodifiableList(receivedRecords); return Collections.unmodifiableList(receivedRecords);
} }
public List<int[]> getReceivedCommands() {
return Collections.unmodifiableList(receivedCommands);
}
public byte[] getLastReceivedRecord() { public byte[] getLastReceivedRecord() {
if (receivedRecords.isEmpty()) return null; if (receivedRecords.isEmpty()) return null;
return receivedRecords.get(receivedRecords.size() - 1); return receivedRecords.get(receivedRecords.size() - 1);
@@ -285,6 +292,7 @@ public class Mock3270Server implements AutoCloseable {
private void handleOptionCommand(int cmd, int opt) throws IOException { private void handleOptionCommand(int cmd, int opt) throws IOException {
log.fine("Mock3270Server: RCVD IAC " + cmd + " opt=" + opt); log.fine("Mock3270Server: RCVD IAC " + cmd + " opt=" + opt);
receivedCommands.add(new int[] { cmd, opt });
if (cmd == WILL && opt == TELOPT_TN3270E) { if (cmd == WILL && opt == TELOPT_TN3270E) {
// Client accepted TN3270E -> Send SB TN3270E SEND DEVICE-TYPE IAC SE // Client accepted TN3270E -> Send SB TN3270E SEND DEVICE-TYPE IAC SE
sendDeviceTypeSend(); sendDeviceTypeSend();
@@ -565,6 +573,11 @@ public class Mock3270Server implements AutoCloseable {
} catch (Exception ignored) {} } catch (Exception ignored) {}
disconnectClient(); disconnectClient();
executor.shutdownNow(); executor.shutdownNow();
try {
executor.awaitTermination(200, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Mock3270Server stopped"); log.info("Mock3270Server stopped");
} }
} }
@@ -76,8 +76,8 @@ public class Mock3270ServerIntegrationTest {
public void testSendScreenAndVerifyPresentationSpace() throws Exception { public void testSendScreenAndVerifyPresentationSpace() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2); ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config); client = new Telnet3270Client(config);
assertTrue(client.connect(8000)); assertTrue(client.connect(8000), "Client should connect to mock server");
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS)); assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS), "Server handshake should complete within 5s");
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000); waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
// Server sends formatted greeting screen at row 2, col 10 // Server sends formatted greeting screen at row 2, col 10
@@ -243,7 +243,10 @@ public class Mock3270ServerIntegrationTest {
// Disconnect active client connection abruptly // Disconnect active client connection abruptly
server.disconnectClient(); server.disconnectClient();
// Wait for client to detect disconnect and reconnect automatically // Wait for client to detect disconnect and enter RECONNECTING
waitForState(client, ConnectionState.RECONNECTING, 5000);
// Wait for client to reconnect automatically
waitForState(client, ConnectionState.CONNECTED_TN3270E, 8000); waitForState(client, ConnectionState.CONNECTED_TN3270E, 8000);
assertEquals(ConnectionState.CONNECTED_TN3270E, client.getConnectionState()); assertEquals(ConnectionState.CONNECTED_TN3270E, client.getConnectionState());
} }
@@ -290,4 +293,46 @@ public class Mock3270ServerIntegrationTest {
assertTrue(matched, "Fragmented stream must be reassembled and displayed correctly"); assertTrue(matched, "Fragmented stream must be reassembled and displayed correctly");
} }
@Test
public void testGracefulDisconnectSendsUnbindAndDeNegotiation() throws Exception {
ConnectionConfig config = new ConnectionConfig("127.0.0.1", server.getPort(), TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config);
assertTrue(client.connect(8000));
assertTrue(server.waitForHandshake(5, TimeUnit.SECONDS));
waitForState(client, ConnectionState.CONNECTED_TN3270E, 5000);
assertTrue(client.getTelnetFSM().isTn3270eBound());
// Perform graceful disconnect
client.disconnect();
// Verify client transitioned to NOT_CONNECTED
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
assertFalse(client.isConnected());
// Wait briefly for server to process queued records
Thread.sleep(150);
// Verify server received TN3270E UNBIND
boolean receivedUnbind = false;
for (byte[] rec : server.getReceivedRecords()) {
if (rec != null && rec.length > 5 && (rec[0] & 0xFF) == DT_UNBIND) {
int reason = rec[5] & 0xFF;
assertEquals(UNBIND_NORMAL, reason, "UNBIND reason should be UNBIND_NORMAL (0x01)");
receivedUnbind = true;
break;
}
}
assertTrue(receivedUnbind, "Server should have received TN3270E UNBIND record");
// Verify server received Telnet option de-negotiation (WONT TN3270E or DONT TN3270E)
boolean receivedWontOrDontTn3270e = false;
for (int[] cmd : server.getReceivedCommands()) {
if ((cmd[0] == WONT || cmd[0] == DONT) && cmd[1] == TELOPT_TN3270E) {
receivedWontOrDontTn3270e = true;
break;
}
}
assertTrue(receivedWontOrDontTn3270e, "Server should have received Telnet de-negotiation for TN3270E");
}
} }