Connection storage
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 50s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s

This commit is contained in:
2026-09-24 12:44:50 +00:00
parent 1f0e36c561
commit 0d6ab31f0a
3 changed files with 736 additions and 9 deletions
@@ -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) {
@@ -86,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];
@@ -97,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);
} }
@@ -110,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);
@@ -134,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) {
@@ -149,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);
@@ -1510,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"));
} }
@@ -1548,10 +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() { public JComboBox<SavedHost> getSavedHostBox() {
return savedHostBox; 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() {
@@ -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) {
}
}
}