Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0201c7c277
|
|||
|
b61702a57c
|
|||
|
bd4c1f3040
|
|||
|
0d6ab31f0a
|
|||
|
1f0e36c561
|
|||
|
8286af19e6
|
|||
|
3d3a939ece
|
|||
|
d05f1a4c45
|
@@ -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,12 +1099,16 @@ 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();
|
||||||
|
|
||||||
|
if (resetUi) {
|
||||||
Runnable uiReset = () -> {
|
Runnable uiReset = () -> {
|
||||||
terminalPanel.setClient(null);
|
terminalPanel.setClient(null);
|
||||||
statusBar.setClient(null, terminalPanel);
|
statusBar.setClient(null, terminalPanel);
|
||||||
@@ -855,6 +1123,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void adjustFontSize(int delta) {
|
private void adjustFontSize(int delta) {
|
||||||
int current = terminalPanel.getFontSize();
|
int current = terminalPanel.getFontSize();
|
||||||
@@ -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]);
|
||||||
|
if (i == 0) {
|
||||||
|
table.getColumnModel().getColumn(0).setMinWidth(250);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runQuery() {
|
private void onFilterOrParse() {
|
||||||
setupTableColumns();
|
|
||||||
currentEntries.clear();
|
|
||||||
String query = queryField.getText().trim();
|
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();
|
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 (client != null && client.getXfer() != null) {
|
|
||||||
if (hostType == FTConfig.HostType.CMS) {
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
|
// CMS Query:
|
||||||
currentEntries.addAll(cmsEntries);
|
// 1. Clear presentation space first to ensure clean state
|
||||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
client.getPS().SendKeys("[clear]");
|
||||||
tableModel.addRow(new Object[]{
|
waitForKeyboardUnlock(client, 3000);
|
||||||
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
|
||||||
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
// 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 {
|
} else {
|
||||||
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
|
client.getPS().SendKeys("[home]");
|
||||||
currentEntries.addAll(tsoEntries);
|
}
|
||||||
for (TSODirectoryEntry entry : tsoEntries) {
|
client.getPS().SendKeys("[erase_eof]");
|
||||||
tableModel.addRow(new Object[]{
|
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||||
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
} else {
|
||||||
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusLabel.setText(currentEntries.size() + " entries found.");
|
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,13 +766,25 @@ 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);
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
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(' ');
|
||||||
|
} else {
|
||||||
char ch = ea.ucs4;
|
char ch = ea.ucs4;
|
||||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
if (ch <= 0x20 || ch == 0xFF) {
|
||||||
line.append(' ');
|
line.append(' ');
|
||||||
} else {
|
} else {
|
||||||
line.append(ch);
|
line.append(ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
|
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
|
||||||
int end = line.length();
|
int end = line.length();
|
||||||
while (end > 0 && line.charAt(end - 1) == ' ') {
|
while (end > 0 && line.charAt(end - 1) == ' ') {
|
||||||
@@ -767,13 +820,25 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
prevRow = curRow;
|
prevRow = curRow;
|
||||||
}
|
}
|
||||||
ExtendedAttribute ea = sb.getCell(addr);
|
ExtendedAttribute ea = sb.getCell(addr);
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
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(' ');
|
||||||
|
} else {
|
||||||
char ch = ea.ucs4;
|
char ch = ea.ucs4;
|
||||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
if (ch <= 0x20 || ch == 0xFF) {
|
||||||
line.append(' ');
|
line.append(' ');
|
||||||
} else {
|
} else {
|
||||||
line.append(ch);
|
line.append(ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
int end = line.length();
|
int end = line.length();
|
||||||
while (end > 0 && line.charAt(end - 1) == ' ') {
|
while (end > 0 && line.charAt(end - 1) == ' ') {
|
||||||
end--;
|
end--;
|
||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+242
@@ -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,12 +27,16 @@ 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) {
|
||||||
|
for (String candidate : nioCharsetName.split(",")) {
|
||||||
|
candidate = candidate.trim();
|
||||||
try {
|
try {
|
||||||
if (Charset.isSupported(nioCharsetName)) {
|
if (Charset.isSupported(candidate)) {
|
||||||
this.nioCharset = Charset.forName(nioCharsetName);
|
this.nioCharset = Charset.forName(candidate);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} 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
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
+214
-11
@@ -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();
|
||||||
|
if (qreplyPending) {
|
||||||
|
qreplyPending = false;
|
||||||
|
sendAllQueryReplies();
|
||||||
|
} else {
|
||||||
processReadModified(false);
|
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
|
||||||
|
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);
|
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;
|
|
||||||
addr = ((addr % size) + size) % size;
|
|
||||||
|
|
||||||
// Cannot delete a field attribute
|
|
||||||
if (screen.getCell(addr).isFieldAttribute()) return;
|
|
||||||
|
|
||||||
byte faVal = screen.getFieldAttributeAt(addr);
|
byte faVal = screen.getFieldAttributeAt(addr);
|
||||||
if (faIsProtected(faVal & 0xFF)) return;
|
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 {
|
||||||
|
if (!socket.isOutputShutdown()) {
|
||||||
socket.shutdownOutput();
|
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;
|
socket = null;
|
||||||
inputStream = null;
|
inputStream = null;
|
||||||
outputStream = null;
|
outputStream = null;
|
||||||
|
readerThread = 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);
|
||||||
|
|||||||
+204
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -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[] {
|
||||||
|
|||||||
+143
@@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-3
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user