Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
30b1a77b61
|
|||
|
9bd1676ec1
|
@@ -1070,8 +1070,47 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
private static void applyProxyUrl(ConnectionConfig config, String proxyUrl) {
|
||||
if (config == null || proxyUrl == null || proxyUrl.trim().isEmpty()) return;
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(proxyUrl.trim());
|
||||
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
|
||||
ConnectionConfig.ProxyType pType;
|
||||
int pPort;
|
||||
if (scheme.equals("http") || scheme.equals("https")) {
|
||||
pType = ConnectionConfig.ProxyType.HTTP;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
|
||||
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
|
||||
pType = ConnectionConfig.ProxyType.SOCKS4;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
|
||||
pType = ConnectionConfig.ProxyType.SOCKS5;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
String pHost = uri.getHost();
|
||||
String pUser = null;
|
||||
String pPass = null;
|
||||
String userInfo = uri.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
int colon = userInfo.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
pUser = userInfo.substring(0, colon);
|
||||
pPass = userInfo.substring(colon + 1);
|
||||
} else {
|
||||
pUser = userInfo;
|
||||
}
|
||||
}
|
||||
if (pHost != null && !pHost.isEmpty()) {
|
||||
config.setProxy(pType, pHost, pPort, pUser, pPass);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warning("Failed to parse CLI proxy URL: " + proxyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = false;
|
||||
@@ -1081,6 +1120,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
Boolean cliAutoSysUnlock = null;
|
||||
GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
String cliProxyUrl = null;
|
||||
String cliProfileName = null;
|
||||
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
@@ -1107,6 +1148,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
cliGraphicsMode = GraphicsMode.NONE;
|
||||
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
|
||||
configFile = args[++i];
|
||||
} else if (arg.startsWith("--proxy=") || arg.startsWith("-proxy=")) {
|
||||
cliProxyUrl = arg.substring(arg.indexOf('=') + 1).trim();
|
||||
} else if (("--proxy".equals(arg) || "-proxy".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
cliProxyUrl = args[++i].trim();
|
||||
} else if (arg.startsWith("--profile=")) {
|
||||
cliProfileName = arg.substring(arg.indexOf('=') + 1).trim();
|
||||
} else if ("--profile".equals(arg) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
cliProfileName = args[++i].trim();
|
||||
} else if (arg.startsWith("-")) {
|
||||
System.err.println("Unknown option: " + arg);
|
||||
} else {
|
||||
@@ -1143,11 +1192,33 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
final Boolean finalTn3270e = cliTn3270e;
|
||||
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
|
||||
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
final String finalProxyUrl = cliProxyUrl;
|
||||
final String finalProfileName = cliProfileName;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
J3270App app = new J3270App();
|
||||
app.setVisible(true);
|
||||
|
||||
if (finalProfileName != null) {
|
||||
haus.nightmare.j3270.storage.SavedHost profile = haus.nightmare.j3270.storage.HostStorage.findByName(finalProfileName);
|
||||
if (profile == null) {
|
||||
profile = haus.nightmare.j3270.storage.HostStorage.findById(finalProfileName);
|
||||
}
|
||||
if (profile != null) {
|
||||
ConnectionConfig config = profile.toConnectionConfig();
|
||||
if (finalTls) config.setUseTls(true);
|
||||
if (finalNoVerify) config.setTlsVerifyCert(false);
|
||||
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
|
||||
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
|
||||
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
|
||||
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
|
||||
app.connect(config);
|
||||
return;
|
||||
} else {
|
||||
System.err.println("Profile not found: " + finalProfileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!remainingArgs.isEmpty()) {
|
||||
String hostArg = remainingArgs.get(0);
|
||||
int port = finalTls ? 992 : 23;
|
||||
@@ -1188,6 +1259,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
if (finalProxyUrl != null) {
|
||||
applyProxyUrl(config, finalProxyUrl);
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
|
||||
@@ -1197,6 +1271,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
break;
|
||||
case AUTO_CONNECT:
|
||||
haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
|
||||
if (autoHost != null) {
|
||||
ConnectionConfig config = autoHost.toConnectionConfig();
|
||||
if (finalTls) config.setUseTls(true);
|
||||
if (finalNoVerify) config.setTlsVerifyCert(false);
|
||||
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
|
||||
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
|
||||
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
|
||||
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
|
||||
app.connect(config);
|
||||
} else {
|
||||
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
|
||||
@@ -1214,10 +1299,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
if (finalProxyUrl != null) {
|
||||
applyProxyUrl(config, finalProxyUrl);
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DO_NOTHING:
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Storage manager for saved host profiles, maintaining 1:1 format compatibility with a3270.
|
||||
* Persists profiles in Java user preferences with JSON serialization.
|
||||
*/
|
||||
public class HostStorage {
|
||||
|
||||
private static final Logger log = Logger.getLogger(HostStorage.class.getName());
|
||||
private static final Preferences prefs = Preferences.userNodeForPackage(HostStorage.class);
|
||||
public static final String KEY_SAVED_HOSTS = "saved_hosts";
|
||||
|
||||
public static synchronized List<SavedHost> getSavedHosts() {
|
||||
String jsonStr = prefs.get(KEY_SAVED_HOSTS, null);
|
||||
if (jsonStr == null || jsonStr.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return parseHostsJson(jsonStr);
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to parse saved hosts JSON", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void saveHost(SavedHost host) {
|
||||
if (host == null) return;
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
int index = -1;
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
if (Objects.equals(current.get(i).getId(), host.getId())) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If this host is set to autoConnect, clear autoConnect on all others
|
||||
SavedHost hostToSave = host.copy();
|
||||
if (hostToSave.isAutoConnect()) {
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
current.get(i).setAutoConnect(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
current.set(index, hostToSave);
|
||||
} else {
|
||||
current.add(hostToSave);
|
||||
}
|
||||
saveAll(current);
|
||||
}
|
||||
|
||||
public static synchronized void deleteHost(String hostId) {
|
||||
if (hostId == null) return;
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
current.removeIf(h -> Objects.equals(h.getId(), hostId));
|
||||
saveAll(current);
|
||||
}
|
||||
|
||||
public static synchronized SavedHost getAutoConnectHost() {
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (h.isAutoConnect()) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized SavedHost findById(String id) {
|
||||
if (id == null) return null;
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (id.equals(h.getId())) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized SavedHost findByName(String name) {
|
||||
if (name == null) return null;
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (name.equalsIgnoreCase(h.getName())) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized void saveAll(List<SavedHost> hosts) {
|
||||
if (hosts == null) return;
|
||||
String json = serializeHostsJson(hosts);
|
||||
prefs.put(KEY_SAVED_HOSTS, json);
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void clearAll() {
|
||||
prefs.remove(KEY_SAVED_HOSTS);
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ================= JSON Serialization =================
|
||||
|
||||
public static String serializeHostsJson(List<SavedHost> hosts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[\n");
|
||||
for (int i = 0; i < hosts.size(); i++) {
|
||||
SavedHost h = hosts.get(i);
|
||||
sb.append(" ").append(serializeHostJson(h));
|
||||
if (i < hosts.size() - 1) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String serializeHostJson(SavedHost h) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
appendField(sb, "id", h.getId(), true);
|
||||
appendField(sb, "name", h.getName(), true);
|
||||
appendField(sb, "host", h.getHost(), true);
|
||||
appendField(sb, "port", h.getPort(), true);
|
||||
appendField(sb, "model", h.getModel(), true);
|
||||
appendField(sb, "dynamicRows", h.getDynamicRows(), true);
|
||||
appendField(sb, "dynamicCols", h.getDynamicCols(), true);
|
||||
appendField(sb, "luName", h.getLuName(), true);
|
||||
appendField(sb, "autoConnect", h.isAutoConnect(), true);
|
||||
appendField(sb, "hostType", h.getHostType(), true);
|
||||
appendField(sb, "useTls", h.isUseTls(), true);
|
||||
appendField(sb, "tlsVerifyCert", h.isTlsVerifyCert(), true);
|
||||
appendField(sb, "tn3270e", h.isTn3270e(), true);
|
||||
appendField(sb, "graphicsMode", h.getGraphicsMode(), true);
|
||||
appendField(sb, "codePage", h.getCodePage(), true);
|
||||
appendField(sb, "proxyType", h.getProxyType(), true);
|
||||
appendField(sb, "proxyHost", h.getProxyHost(), true);
|
||||
appendField(sb, "proxyPort", h.getProxyPort(), true);
|
||||
appendField(sb, "proxyUsername", h.getProxyUsername(), true);
|
||||
appendField(sb, "proxyPassword", h.getProxyPassword(), false);
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, String val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":\"").append(escapeJson(val != null ? val : "")).append("\"");
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, int val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":").append(val);
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, boolean val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":").append(val);
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '"': sb.append("\\\""); break;
|
||||
case '\\': sb.append("\\\\"); break;
|
||||
case '\b': sb.append("\\b"); break;
|
||||
case '\f': sb.append("\\f"); break;
|
||||
case '\n': sb.append("\\n"); break;
|
||||
case '\r': sb.append("\\r"); break;
|
||||
case '\t': sb.append("\\t"); break;
|
||||
default:
|
||||
if (c < 32) {
|
||||
sb.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ================= JSON Deserialization =================
|
||||
|
||||
public static List<SavedHost> parseHostsJson(String json) {
|
||||
List<SavedHost> list = new ArrayList<>();
|
||||
if (json == null || json.trim().isEmpty()) return list;
|
||||
|
||||
SimpleJsonParser parser = new SimpleJsonParser(json);
|
||||
Object root = parser.parseValue();
|
||||
if (root instanceof List) {
|
||||
for (Object item : (List<?>) root) {
|
||||
if (item instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) item;
|
||||
list.add(fromMap(map));
|
||||
}
|
||||
}
|
||||
} else if (root instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) root;
|
||||
list.add(fromMap(map));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static SavedHost fromMap(Map<String, Object> map) {
|
||||
SavedHost h = new SavedHost();
|
||||
if (map.containsKey("id")) h.setId(String.valueOf(map.get("id")));
|
||||
if (map.containsKey("name")) h.setName(String.valueOf(map.get("name")));
|
||||
if (map.containsKey("host")) h.setHost(String.valueOf(map.get("host")));
|
||||
if (map.containsKey("port")) h.setPort(parseInt(map.get("port"), 23));
|
||||
if (map.containsKey("model")) h.setModel(parseInt(map.get("model"), 4));
|
||||
if (map.containsKey("dynamicRows")) h.setDynamicRows(parseInt(map.get("dynamicRows"), 62));
|
||||
if (map.containsKey("dynamicCols")) h.setDynamicCols(parseInt(map.get("dynamicCols"), 160));
|
||||
if (map.containsKey("luName")) h.setLuName(String.valueOf(map.get("luName")));
|
||||
if (map.containsKey("autoConnect")) h.setAutoConnect(parseBool(map.get("autoConnect"), false));
|
||||
if (map.containsKey("hostType")) h.setHostType(String.valueOf(map.get("hostType")));
|
||||
if (map.containsKey("useTls")) h.setUseTls(parseBool(map.get("useTls"), false));
|
||||
if (map.containsKey("tlsVerifyCert")) h.setTlsVerifyCert(parseBool(map.get("tlsVerifyCert"), true));
|
||||
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
|
||||
if (map.containsKey("graphicsMode")) {
|
||||
String gm = String.valueOf(map.get("graphicsMode"));
|
||||
if ("PROGRAMMED_SYMBOLS".equalsIgnoreCase(gm)) gm = "BOTH";
|
||||
h.setGraphicsMode(gm);
|
||||
}
|
||||
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
|
||||
if (map.containsKey("proxyType")) h.setProxyType(String.valueOf(map.get("proxyType")));
|
||||
if (map.containsKey("proxyHost")) h.setProxyHost(String.valueOf(map.get("proxyHost")));
|
||||
if (map.containsKey("proxyPort")) h.setProxyPort(parseInt(map.get("proxyPort"), 0));
|
||||
if (map.containsKey("proxyUsername")) h.setProxyUsername(String.valueOf(map.get("proxyUsername")));
|
||||
if (map.containsKey("proxyPassword")) h.setProxyPassword(String.valueOf(map.get("proxyPassword")));
|
||||
return h;
|
||||
}
|
||||
|
||||
private static int parseInt(Object obj, int def) {
|
||||
if (obj == null) return def;
|
||||
if (obj instanceof Number) return ((Number) obj).intValue();
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(obj).trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean parseBool(Object obj, boolean def) {
|
||||
if (obj == null) return def;
|
||||
if (obj instanceof Boolean) return (Boolean) obj;
|
||||
return Boolean.parseBoolean(String.valueOf(obj).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal pure-Java JSON parser for nested Maps, Lists, Strings, Numbers, and Booleans.
|
||||
*/
|
||||
static class SimpleJsonParser {
|
||||
private final String src;
|
||||
private int pos = 0;
|
||||
|
||||
SimpleJsonParser(String src) {
|
||||
this.src = src;
|
||||
}
|
||||
|
||||
Object parseValue() {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length()) return null;
|
||||
char c = src.charAt(pos);
|
||||
if (c == '{') return parseObject();
|
||||
if (c == '[') return parseArray();
|
||||
if (c == '"') return parseString();
|
||||
if (c == 't' || c == 'f') return parseBoolean();
|
||||
if (c == 'n') return parseNull();
|
||||
if (c == '-' || Character.isDigit(c)) return parseNumber();
|
||||
throw new IllegalArgumentException("Unexpected char '" + c + "' at position " + pos);
|
||||
}
|
||||
|
||||
private Map<String, Object> parseObject() {
|
||||
match('{');
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
skipWhitespace();
|
||||
if (peek() == '}') {
|
||||
pos++;
|
||||
return map;
|
||||
}
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
String key = parseString();
|
||||
skipWhitespace();
|
||||
match(':');
|
||||
Object val = parseValue();
|
||||
map.put(key, val);
|
||||
skipWhitespace();
|
||||
char c = peek();
|
||||
if (c == ',') {
|
||||
pos++;
|
||||
} else if (c == '}') {
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<Object> parseArray() {
|
||||
match('[');
|
||||
List<Object> list = new ArrayList<>();
|
||||
skipWhitespace();
|
||||
if (peek() == ']') {
|
||||
pos++;
|
||||
return list;
|
||||
}
|
||||
while (true) {
|
||||
Object val = parseValue();
|
||||
list.add(val);
|
||||
skipWhitespace();
|
||||
char c = peek();
|
||||
if (c == ',') {
|
||||
pos++;
|
||||
} else if (c == ']') {
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
match('"');
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (pos < src.length()) {
|
||||
char c = src.charAt(pos++);
|
||||
if (c == '"') {
|
||||
return sb.toString();
|
||||
} else if (c == '\\') {
|
||||
if (pos >= src.length()) break;
|
||||
char esc = src.charAt(pos++);
|
||||
switch (esc) {
|
||||
case '"': sb.append('"'); break;
|
||||
case '\\': sb.append('\\'); break;
|
||||
case '/': sb.append('/'); break;
|
||||
case 'b': sb.append('\b'); break;
|
||||
case 'f': sb.append('\f'); break;
|
||||
case 'n': sb.append('\n'); break;
|
||||
case 'r': sb.append('\r'); break;
|
||||
case 't': sb.append('\t'); break;
|
||||
case 'u':
|
||||
if (pos + 4 <= src.length()) {
|
||||
String hex = src.substring(pos, pos + 4);
|
||||
pos += 4;
|
||||
try {
|
||||
sb.append((char) Integer.parseInt(hex, 16));
|
||||
} catch (NumberFormatException e) {
|
||||
sb.append("?");
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sb.append(esc);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private Boolean parseBoolean() {
|
||||
if (src.startsWith("true", pos)) {
|
||||
pos += 4;
|
||||
return true;
|
||||
} else if (src.startsWith("false", pos)) {
|
||||
pos += 5;
|
||||
return false;
|
||||
}
|
||||
throw new IllegalArgumentException("Expected boolean at " + pos);
|
||||
}
|
||||
|
||||
private Object parseNull() {
|
||||
if (src.startsWith("null", pos)) {
|
||||
pos += 4;
|
||||
return null;
|
||||
}
|
||||
throw new IllegalArgumentException("Expected null at " + pos);
|
||||
}
|
||||
|
||||
private Number parseNumber() {
|
||||
int start = pos;
|
||||
if (src.charAt(pos) == '-') pos++;
|
||||
while (pos < src.length() && (Character.isDigit(src.charAt(pos)) || src.charAt(pos) == '.' || src.charAt(pos) == 'e' || src.charAt(pos) == 'E' || src.charAt(pos) == '+' || src.charAt(pos) == '-')) {
|
||||
pos++;
|
||||
}
|
||||
String numStr = src.substring(start, pos);
|
||||
if (numStr.contains(".")) {
|
||||
return Double.parseDouble(numStr);
|
||||
}
|
||||
return Long.parseLong(numStr);
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
private char peek() {
|
||||
return pos < src.length() ? src.charAt(pos) : '\0';
|
||||
}
|
||||
|
||||
private void match(char expected) {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length() || src.charAt(pos) != expected) {
|
||||
throw new IllegalArgumentException("Expected '" + expected + "' at " + pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Data model for a saved 3270 connection profile, matching the schema of a3270 SavedHost.
|
||||
* Includes per-connection host, model, protocol, and proxy configuration.
|
||||
*/
|
||||
public class SavedHost {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String host;
|
||||
private int port;
|
||||
private int model;
|
||||
private int dynamicRows;
|
||||
private int dynamicCols;
|
||||
private String luName;
|
||||
private boolean autoConnect;
|
||||
private String hostType;
|
||||
private boolean useTls;
|
||||
private boolean tlsVerifyCert;
|
||||
private boolean tn3270e;
|
||||
private String graphicsMode;
|
||||
private String codePage;
|
||||
|
||||
// Per-connection proxy configuration
|
||||
private String proxyType;
|
||||
private String proxyHost;
|
||||
private int proxyPort;
|
||||
private String proxyUsername;
|
||||
private String proxyPassword;
|
||||
|
||||
public SavedHost() {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.name = "Mainframe";
|
||||
this.host = "127.0.0.1";
|
||||
this.port = 23;
|
||||
this.model = 4;
|
||||
this.dynamicRows = 62;
|
||||
this.dynamicCols = 160;
|
||||
this.luName = "";
|
||||
this.autoConnect = false;
|
||||
this.hostType = "TSO";
|
||||
this.useTls = false;
|
||||
this.tlsVerifyCert = true;
|
||||
this.tn3270e = true;
|
||||
this.graphicsMode = "BOTH";
|
||||
this.codePage = "037";
|
||||
this.proxyType = "NONE";
|
||||
this.proxyHost = "";
|
||||
this.proxyPort = 0;
|
||||
this.proxyUsername = "";
|
||||
this.proxyPassword = "";
|
||||
}
|
||||
|
||||
public SavedHost(String id, String name, String host, int port, int model,
|
||||
int dynamicRows, int dynamicCols, String luName, boolean autoConnect,
|
||||
String hostType, boolean useTls, boolean tlsVerifyCert, boolean tn3270e,
|
||||
String graphicsMode, String codePage, String proxyType, String proxyHost,
|
||||
int proxyPort, String proxyUsername, String proxyPassword) {
|
||||
this.id = (id != null && !id.trim().isEmpty()) ? id : UUID.randomUUID().toString();
|
||||
this.name = name != null ? name : "";
|
||||
this.host = host != null ? host : "127.0.0.1";
|
||||
this.port = port > 0 ? port : 23;
|
||||
this.model = model;
|
||||
this.dynamicRows = dynamicRows > 0 ? dynamicRows : 62;
|
||||
this.dynamicCols = dynamicCols > 0 ? dynamicCols : 160;
|
||||
this.luName = luName != null ? luName : "";
|
||||
this.autoConnect = autoConnect;
|
||||
this.hostType = hostType != null ? hostType : "TSO";
|
||||
this.useTls = useTls;
|
||||
this.tlsVerifyCert = tlsVerifyCert;
|
||||
this.tn3270e = tn3270e;
|
||||
this.graphicsMode = graphicsMode != null ? graphicsMode : "BOTH";
|
||||
this.codePage = codePage != null ? codePage : "037";
|
||||
this.proxyType = (proxyType != null && !proxyType.trim().isEmpty()) ? proxyType.toUpperCase() : "NONE";
|
||||
this.proxyHost = proxyHost != null ? proxyHost : "";
|
||||
this.proxyPort = proxyPort;
|
||||
this.proxyUsername = proxyUsername != null ? proxyUsername : "";
|
||||
this.proxyPassword = proxyPassword != null ? proxyPassword : "";
|
||||
}
|
||||
|
||||
public SavedHost copy() {
|
||||
return new SavedHost(id, name, host, port, model, dynamicRows, dynamicCols, luName,
|
||||
autoConnect, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, codePage,
|
||||
proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this saved host profile into an active ConnectionConfig.
|
||||
*/
|
||||
public ConnectionConfig toConnectionConfig() {
|
||||
TerminalModel tm;
|
||||
if (model == 0) {
|
||||
tm = TerminalModel.IBM_DYNAMIC;
|
||||
} else {
|
||||
try {
|
||||
tm = TerminalModel.forModel(model, true);
|
||||
} catch (Exception e) {
|
||||
tm = TerminalModel.IBM_3279_4;
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
|
||||
if (model == 0 || tm.isDynamic()) {
|
||||
config.setDynamicDimensions(dynamicRows, dynamicCols);
|
||||
}
|
||||
if (luName != null && !luName.trim().isEmpty()) {
|
||||
config.setLuName(luName.trim());
|
||||
}
|
||||
config.setTlsVerifyCert(tlsVerifyCert);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
if (graphicsMode != null && !graphicsMode.isEmpty()) {
|
||||
config.setGraphicsMode(GraphicsMode.fromString(graphicsMode));
|
||||
}
|
||||
if (codePage != null && !codePage.isEmpty()) {
|
||||
config.setCodePage(codePage);
|
||||
}
|
||||
|
||||
// Apply per-connection proxy if configured
|
||||
if (proxyType != null && !proxyType.equalsIgnoreCase("NONE") && proxyHost != null && !proxyHost.trim().isEmpty()) {
|
||||
try {
|
||||
ConnectionConfig.ProxyType pt = ConnectionConfig.ProxyType.valueOf(proxyType.toUpperCase());
|
||||
int pPort = proxyPort > 0 ? proxyPort : (pt == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080);
|
||||
config.setProxy(pt, proxyHost.trim(), pPort,
|
||||
(proxyUsername != null && !proxyUsername.trim().isEmpty()) ? proxyUsername.trim() : null,
|
||||
(proxyPassword != null && !proxyPassword.isEmpty()) ? proxyPassword : null);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public int getModel() { return model; }
|
||||
public void setModel(int model) { this.model = model; }
|
||||
|
||||
public int getDynamicRows() { return dynamicRows; }
|
||||
public void setDynamicRows(int dynamicRows) { this.dynamicRows = dynamicRows; }
|
||||
|
||||
public int getDynamicCols() { return dynamicCols; }
|
||||
public void setDynamicCols(int dynamicCols) { this.dynamicCols = dynamicCols; }
|
||||
|
||||
public String getLuName() { return luName; }
|
||||
public void setLuName(String luName) { this.luName = luName; }
|
||||
|
||||
public boolean isAutoConnect() { return autoConnect; }
|
||||
public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; }
|
||||
|
||||
public String getHostType() { return hostType; }
|
||||
public void setHostType(String hostType) { this.hostType = hostType; }
|
||||
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
|
||||
|
||||
public boolean isTn3270e() { return tn3270e; }
|
||||
public void setTn3270e(boolean tn3270e) { this.tn3270e = tn3270e; }
|
||||
|
||||
public String getGraphicsMode() { return graphicsMode; }
|
||||
public void setGraphicsMode(String graphicsMode) { this.graphicsMode = graphicsMode; }
|
||||
|
||||
public String getCodePage() { return codePage; }
|
||||
public void setCodePage(String codePage) { this.codePage = codePage; }
|
||||
|
||||
public String getProxyType() { return proxyType != null ? proxyType : "NONE"; }
|
||||
public void setProxyType(String proxyType) { this.proxyType = (proxyType != null) ? proxyType.toUpperCase() : "NONE"; }
|
||||
|
||||
public String getProxyHost() { return proxyHost != null ? proxyHost : ""; }
|
||||
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
|
||||
|
||||
public int getProxyPort() { return proxyPort; }
|
||||
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
|
||||
|
||||
public String getProxyUsername() { return proxyUsername != null ? proxyUsername : ""; }
|
||||
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
|
||||
|
||||
public String getProxyPassword() { return proxyPassword != null ? proxyPassword : ""; }
|
||||
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
return host + (port > 0 ? ":" + port : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SavedHost savedHost = (SavedHost) o;
|
||||
return Objects.equals(id, savedHost.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,30 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Connection dialog for entering host, port, model, and LU name.
|
||||
* Connection dialog for entering host, port, model, LU name, and per-connection proxy configuration.
|
||||
* Supports loading and saving host profiles matching the a3270 schema.
|
||||
*/
|
||||
public class ConnectDialog extends JDialog {
|
||||
|
||||
private static final String NEW_PROFILE_LABEL = "<New / Custom Connection>";
|
||||
|
||||
// Profile selector
|
||||
private JComboBox<Object> profileCombo;
|
||||
private JButton saveProfileBtn;
|
||||
private JButton deleteProfileBtn;
|
||||
private String selectedProfileId = null;
|
||||
|
||||
// Connection parameters
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JComboBox<TerminalModel> modelCombo;
|
||||
@@ -26,6 +40,14 @@ public class ConnectDialog extends JDialog {
|
||||
private JCheckBox tn3270eCheckBox;
|
||||
private JCheckBox keepAliveCheckBox;
|
||||
private JCheckBox autoReconnectCheckBox;
|
||||
|
||||
// Per-connection proxy configuration
|
||||
private JComboBox<ConnectionConfig.ProxyType> proxyTypeCombo;
|
||||
private JTextField proxyHostField;
|
||||
private JTextField proxyPortField;
|
||||
private JTextField proxyUserField;
|
||||
private JPasswordField proxyPassField;
|
||||
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
@@ -35,64 +57,114 @@ public class ConnectDialog extends JDialog {
|
||||
buildUI();
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
setResizable(true);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new GridBagLayout());
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
JPanel contentPanel = new JPanel(new GridBagLayout());
|
||||
contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(5, 6, 5, 6);
|
||||
gbc.insets = new Insets(4, 5, 4, 5);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Host
|
||||
int gridy = 0;
|
||||
|
||||
// 0. Profile Row
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel profileLabel = new JLabel("Profile:");
|
||||
profileLabel.setFont(labelFont);
|
||||
contentPanel.add(profileLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
JPanel profileBar = new JPanel(new BorderLayout(6, 0));
|
||||
profileBar.setOpaque(false);
|
||||
|
||||
profileCombo = new JComboBox<>();
|
||||
profileCombo.setFont(ThemeManager.getUiFont());
|
||||
ThemeManager.styleComboBox(profileCombo);
|
||||
refreshProfileCombo();
|
||||
profileCombo.addActionListener(e -> onProfileSelectionChanged());
|
||||
profileBar.add(profileCombo, BorderLayout.CENTER);
|
||||
|
||||
JPanel profileBtnBar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
|
||||
profileBtnBar.setOpaque(false);
|
||||
|
||||
saveProfileBtn = new JButton("Save Profile...");
|
||||
ThemeManager.styleButton(saveProfileBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveProfileBtn.setFont(labelFont);
|
||||
saveProfileBtn.addActionListener(e -> onSaveProfile());
|
||||
|
||||
deleteProfileBtn = new JButton("Delete");
|
||||
ThemeManager.styleButton(deleteProfileBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
deleteProfileBtn.setFont(labelFont);
|
||||
deleteProfileBtn.setEnabled(false);
|
||||
deleteProfileBtn.addActionListener(e -> onDeleteProfile());
|
||||
|
||||
profileBtnBar.add(saveProfileBtn);
|
||||
profileBtnBar.add(deleteProfileBtn);
|
||||
profileBar.add(profileBtnBar, BorderLayout.EAST);
|
||||
contentPanel.add(profileBar, gbc);
|
||||
|
||||
gridy++;
|
||||
|
||||
// 1. Host
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
hostLabel.setFont(labelFont);
|
||||
mainPanel.add(hostLabel, gbc);
|
||||
contentPanel.add(hostLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
hostField = createField(20);
|
||||
mainPanel.add(hostField, gbc);
|
||||
contentPanel.add(hostField, gbc);
|
||||
|
||||
// Port
|
||||
gridy++;
|
||||
|
||||
// 2. Port
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
portLabel.setFont(labelFont);
|
||||
mainPanel.add(portLabel, gbc);
|
||||
contentPanel.add(portLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
portField = createField(6);
|
||||
portField.setText("23");
|
||||
mainPanel.add(portField, gbc);
|
||||
contentPanel.add(portField, gbc);
|
||||
|
||||
// Model
|
||||
gridy++;
|
||||
|
||||
// 3. Model
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel modelLabel = new JLabel("Model:");
|
||||
modelLabel.setFont(labelFont);
|
||||
mainPanel.add(modelLabel, gbc);
|
||||
contentPanel.add(modelLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
modelCombo = new JComboBox<>(TerminalModel.values());
|
||||
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
|
||||
modelCombo.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleComboBox(modelCombo);
|
||||
mainPanel.add(modelCombo, gbc);
|
||||
contentPanel.add(modelCombo, gbc);
|
||||
|
||||
// Dynamic Dimensions (shown when IBM-DYNAMIC is selected)
|
||||
gridy++;
|
||||
|
||||
// 4. Dynamic Dimensions
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 3;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
dynamicDimLabel = new JLabel("Screen Size:");
|
||||
dynamicDimLabel.setFont(labelFont);
|
||||
mainPanel.add(dynamicDimLabel, gbc);
|
||||
contentPanel.add(dynamicDimLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
@@ -113,7 +185,7 @@ public class ConnectDialog extends JDialog {
|
||||
dynamicDimPanel.add(dynamicRowsSpinner);
|
||||
dynamicDimPanel.add(colsLabel);
|
||||
dynamicDimPanel.add(dynamicColsSpinner);
|
||||
mainPanel.add(dynamicDimPanel, gbc);
|
||||
contentPanel.add(dynamicDimPanel, gbc);
|
||||
|
||||
Runnable updateDynamicVisibility = () -> {
|
||||
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
|
||||
@@ -125,40 +197,46 @@ public class ConnectDialog extends JDialog {
|
||||
modelCombo.addActionListener(e -> updateDynamicVisibility.run());
|
||||
updateDynamicVisibility.run();
|
||||
|
||||
// LU Name
|
||||
gridy++;
|
||||
|
||||
// 5. LU Name
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel luLabel = new JLabel("LU Name:");
|
||||
luLabel.setFont(labelFont);
|
||||
mainPanel.add(luLabel, gbc);
|
||||
contentPanel.add(luLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
luField = createField(12);
|
||||
mainPanel.add(luField, gbc);
|
||||
contentPanel.add(luField, gbc);
|
||||
|
||||
// Graphics Mode
|
||||
gridy++;
|
||||
|
||||
// 6. Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 5;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel graphicsLabel = new JLabel("Graphics:");
|
||||
graphicsLabel.setFont(labelFont);
|
||||
mainPanel.add(graphicsLabel, gbc);
|
||||
contentPanel.add(graphicsLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
graphicsCombo.setFont(ThemeManager.getUiFont());
|
||||
ThemeManager.styleComboBox(graphicsCombo);
|
||||
mainPanel.add(graphicsCombo, gbc);
|
||||
contentPanel.add(graphicsCombo, gbc);
|
||||
|
||||
// Code Page
|
||||
gridy++;
|
||||
|
||||
// 7. Code Page
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 6;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel cpLabel = new JLabel("Code Page:");
|
||||
cpLabel.setFont(labelFont);
|
||||
mainPanel.add(cpLabel, gbc);
|
||||
contentPanel.add(cpLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
String[] commonCodePages = {
|
||||
@@ -179,43 +257,44 @@ public class ConnectDialog extends JDialog {
|
||||
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
|
||||
"905 - Turkey (Turkish Latin-3)",
|
||||
"420 - Arabic Bilingual",
|
||||
"424 - Hebrew (with Lowercase)",
|
||||
"803 - Hebrew Old / Standard",
|
||||
"838 - Thai",
|
||||
"1160 - Thai (Euro \u20AC)",
|
||||
"424 - Hebrew (with Latin)",
|
||||
"803 - Hebrew Character Set",
|
||||
"838 - Thai Extended",
|
||||
"1160 - Thai Euro \u20AC",
|
||||
"1025 - Cyrillic Multilingual",
|
||||
"1123 - Cyrillic Ukraine",
|
||||
"1154 - Cyrillic Multilingual (Euro \u20AC)",
|
||||
"880 - Cyrillic Russian",
|
||||
"1154 - Cyrillic Euro \u20AC",
|
||||
"880 - Cyrillic (Russian)",
|
||||
"1140 - US / Canada (Euro \u20AC)",
|
||||
"1141 - Germany / Austria (Euro \u20AC)",
|
||||
"1142 - Denmark / Norway (Euro \u20AC)",
|
||||
"1143 - Sweden / Finland (Euro \u20AC)",
|
||||
"1144 - Italy (Euro \u20AC)",
|
||||
"1145 - Spain / Latin America (Euro \u20AC)",
|
||||
"1146 - United Kingdom (Euro \u20AC)",
|
||||
"1147 - France (Euro \u20AC)",
|
||||
"1148 - International (Euro \u20AC)",
|
||||
"1149 - Iceland (Euro \u20AC)",
|
||||
"930 - Japanese Katakana Mixed DBCS",
|
||||
"939 - Japanese Latin Mixed DBCS",
|
||||
"935 - Simplified Chinese Mixed DBCS",
|
||||
"1388 - Simplified Chinese Extended Mixed DBCS",
|
||||
"937 - Traditional Chinese Mixed DBCS",
|
||||
"1371 - Traditional Chinese Extended Mixed DBCS",
|
||||
"1388 - Simplified Chinese Extended DBCS",
|
||||
"1371 - Traditional Chinese Extended DBCS",
|
||||
"933 - Korean Mixed DBCS"
|
||||
};
|
||||
codePageCombo = new JComboBox<>(commonCodePages);
|
||||
codePageCombo.setEditable(true);
|
||||
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
|
||||
for (String item : commonCodePages) {
|
||||
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
|
||||
codePageCombo.setSelectedItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
codePageCombo.setFont(ThemeManager.getUiFont());
|
||||
codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleComboBox(codePageCombo);
|
||||
mainPanel.add(codePageCombo, gbc);
|
||||
setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
||||
contentPanel.add(codePageCombo, gbc);
|
||||
|
||||
// TLS / SSL Checkbox
|
||||
gridy++;
|
||||
|
||||
// 8. TLS Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 7;
|
||||
gbc.weightx = 1.0;
|
||||
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
||||
gbc.gridy = gridy;
|
||||
tlsCheckBox = new JCheckBox("Use TLS / SSL Encryption");
|
||||
ThemeManager.styleCheckBox(tlsCheckBox);
|
||||
tlsCheckBox.setFont(labelFont);
|
||||
tlsCheckBox.addActionListener(e -> {
|
||||
@@ -228,46 +307,169 @@ public class ConnectDialog extends JDialog {
|
||||
portField.setText("23");
|
||||
}
|
||||
});
|
||||
mainPanel.add(tlsCheckBox, gbc);
|
||||
contentPanel.add(tlsCheckBox, gbc);
|
||||
|
||||
// Verify Certificate Checkbox
|
||||
gridy++;
|
||||
|
||||
// 9. Verify Cert Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 8;
|
||||
gbc.gridy = gridy;
|
||||
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
||||
ThemeManager.styleCheckBox(verifyCertCheckBox);
|
||||
verifyCertCheckBox.setFont(ThemeManager.getUiFont());
|
||||
verifyCertCheckBox.setSelected(true);
|
||||
verifyCertCheckBox.setEnabled(false);
|
||||
mainPanel.add(verifyCertCheckBox, gbc);
|
||||
contentPanel.add(verifyCertCheckBox, gbc);
|
||||
|
||||
// TN3270E Checkbox
|
||||
gridy++;
|
||||
|
||||
// 10. TN3270E Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 9;
|
||||
gbc.gridy = gridy;
|
||||
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
|
||||
ThemeManager.styleCheckBox(tn3270eCheckBox);
|
||||
tn3270eCheckBox.setFont(ThemeManager.getUiFont());
|
||||
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||
mainPanel.add(tn3270eCheckBox, gbc);
|
||||
contentPanel.add(tn3270eCheckBox, gbc);
|
||||
|
||||
// Keep-Alive Checkbox
|
||||
gridy++;
|
||||
|
||||
// 11. Keep-Alive Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 10;
|
||||
gbc.gridy = gridy;
|
||||
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
|
||||
ThemeManager.styleCheckBox(keepAliveCheckBox);
|
||||
keepAliveCheckBox.setFont(ThemeManager.getUiFont());
|
||||
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
|
||||
mainPanel.add(keepAliveCheckBox, gbc);
|
||||
contentPanel.add(keepAliveCheckBox, gbc);
|
||||
|
||||
// Auto-Reconnect Checkbox
|
||||
gridy++;
|
||||
|
||||
// 12. Auto-Reconnect Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 11;
|
||||
gbc.gridy = gridy;
|
||||
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
|
||||
ThemeManager.styleCheckBox(autoReconnectCheckBox);
|
||||
autoReconnectCheckBox.setFont(ThemeManager.getUiFont());
|
||||
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
|
||||
mainPanel.add(autoReconnectCheckBox, gbc);
|
||||
contentPanel.add(autoReconnectCheckBox, gbc);
|
||||
|
||||
gridy++;
|
||||
|
||||
// 13. PER-CONNECTION PROXY CONFIGURATION PANEL
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = gridy;
|
||||
gbc.gridwidth = 2;
|
||||
gbc.weightx = 1.0;
|
||||
|
||||
JPanel proxyGroup = new JPanel(new GridBagLayout());
|
||||
proxyGroup.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createEtchedBorder(),
|
||||
"Proxy Configuration (This Connection)",
|
||||
TitledBorder.LEFT,
|
||||
TitledBorder.TOP,
|
||||
labelFont
|
||||
));
|
||||
GridBagConstraints pgbc = new GridBagConstraints();
|
||||
pgbc.insets = new Insets(3, 6, 3, 6);
|
||||
pgbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
// Proxy Type
|
||||
pgbc.gridx = 0;
|
||||
pgbc.gridy = 0;
|
||||
pgbc.weightx = 0;
|
||||
JLabel ptLabel = new JLabel("Proxy Type:");
|
||||
ptLabel.setFont(labelFont);
|
||||
proxyGroup.add(ptLabel, pgbc);
|
||||
|
||||
pgbc.gridx = 1;
|
||||
pgbc.gridwidth = 3;
|
||||
pgbc.weightx = 1.0;
|
||||
proxyTypeCombo = new JComboBox<>(ConnectionConfig.ProxyType.values());
|
||||
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
|
||||
proxyTypeCombo.setFont(ThemeManager.getUiFont());
|
||||
ThemeManager.styleComboBox(proxyTypeCombo);
|
||||
proxyGroup.add(proxyTypeCombo, pgbc);
|
||||
|
||||
// Proxy Host & Port
|
||||
pgbc.gridx = 0;
|
||||
pgbc.gridy = 1;
|
||||
pgbc.gridwidth = 1;
|
||||
pgbc.weightx = 0;
|
||||
JLabel phLabel = new JLabel("Proxy Host:");
|
||||
phLabel.setFont(labelFont);
|
||||
proxyGroup.add(phLabel, pgbc);
|
||||
|
||||
pgbc.gridx = 1;
|
||||
pgbc.weightx = 1.0;
|
||||
proxyHostField = createField(14);
|
||||
proxyGroup.add(proxyHostField, pgbc);
|
||||
|
||||
pgbc.gridx = 2;
|
||||
pgbc.weightx = 0;
|
||||
JLabel ppLabel = new JLabel("Port:");
|
||||
ppLabel.setFont(labelFont);
|
||||
proxyGroup.add(ppLabel, pgbc);
|
||||
|
||||
pgbc.gridx = 3;
|
||||
pgbc.weightx = 0.4;
|
||||
proxyPortField = createField(5);
|
||||
proxyGroup.add(proxyPortField, pgbc);
|
||||
|
||||
// Proxy User & Password
|
||||
pgbc.gridx = 0;
|
||||
pgbc.gridy = 2;
|
||||
pgbc.gridwidth = 1;
|
||||
pgbc.weightx = 0;
|
||||
JLabel puLabel = new JLabel("Username:");
|
||||
puLabel.setFont(labelFont);
|
||||
proxyGroup.add(puLabel, pgbc);
|
||||
|
||||
pgbc.gridx = 1;
|
||||
pgbc.weightx = 1.0;
|
||||
proxyUserField = createField(12);
|
||||
proxyGroup.add(proxyUserField, pgbc);
|
||||
|
||||
pgbc.gridx = 2;
|
||||
pgbc.weightx = 0;
|
||||
JLabel pwLabel = new JLabel("Password:");
|
||||
pwLabel.setFont(labelFont);
|
||||
proxyGroup.add(pwLabel, pgbc);
|
||||
|
||||
pgbc.gridx = 3;
|
||||
pgbc.weightx = 0.4;
|
||||
proxyPassField = createPasswordField(10);
|
||||
proxyGroup.add(proxyPassField, pgbc);
|
||||
|
||||
// Dynamic enable/disable based on Proxy Type
|
||||
Runnable updateProxyFields = () -> {
|
||||
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
|
||||
boolean active = (pt != null && pt != ConnectionConfig.ProxyType.NONE);
|
||||
proxyHostField.setEnabled(active);
|
||||
proxyPortField.setEnabled(active);
|
||||
proxyUserField.setEnabled(active);
|
||||
proxyPassField.setEnabled(active && pt != ConnectionConfig.ProxyType.SOCKS4);
|
||||
|
||||
if (active) {
|
||||
String currentPPort = proxyPortField.getText().trim();
|
||||
if (currentPPort.isEmpty() || "0".equals(currentPPort)) {
|
||||
proxyPortField.setText(pt == ConnectionConfig.ProxyType.HTTP ? "8080" : "1080");
|
||||
}
|
||||
}
|
||||
};
|
||||
proxyTypeCombo.addActionListener(e -> updateProxyFields.run());
|
||||
updateProxyFields.run();
|
||||
|
||||
contentPanel.add(proxyGroup, gbc);
|
||||
|
||||
gridy++;
|
||||
|
||||
// 14. Action Buttons
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = gridy;
|
||||
gbc.gridwidth = 2;
|
||||
gbc.weightx = 1.0;
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
JButton connectBtn = new JButton("Connect");
|
||||
@@ -285,13 +487,14 @@ public class ConnectDialog extends JDialog {
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(connectBtn);
|
||||
contentPanel.add(buttonPanel, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 12;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
JScrollPane scrollPane = new JScrollPane(contentPanel);
|
||||
scrollPane.setBorder(BorderFactory.createEmptyBorder());
|
||||
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scrollPane.getVerticalScrollBar().setUnitIncrement(14);
|
||||
setContentPane(scrollPane);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Enter key triggers connect
|
||||
@@ -305,6 +508,189 @@ public class ConnectDialog extends JDialog {
|
||||
return field;
|
||||
}
|
||||
|
||||
private JPasswordField createPasswordField(int cols) {
|
||||
JPasswordField field = new JPasswordField(cols);
|
||||
field.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextField(field);
|
||||
return field;
|
||||
}
|
||||
|
||||
// ================= Profile Management =================
|
||||
|
||||
private void refreshProfileCombo() {
|
||||
profileCombo.removeAllItems();
|
||||
profileCombo.addItem(NEW_PROFILE_LABEL);
|
||||
List<SavedHost> saved = HostStorage.getSavedHosts();
|
||||
for (SavedHost h : saved) {
|
||||
profileCombo.addItem(h);
|
||||
}
|
||||
if (selectedProfileId != null) {
|
||||
for (int i = 0; i < profileCombo.getItemCount(); i++) {
|
||||
Object item = profileCombo.getItemAt(i);
|
||||
if (item instanceof SavedHost && ((SavedHost) item).getId().equals(selectedProfileId)) {
|
||||
profileCombo.setSelectedIndex(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
profileCombo.setSelectedIndex(0);
|
||||
}
|
||||
|
||||
private void onProfileSelectionChanged() {
|
||||
Object sel = profileCombo.getSelectedItem();
|
||||
if (sel instanceof SavedHost) {
|
||||
SavedHost h = (SavedHost) sel;
|
||||
selectedProfileId = h.getId();
|
||||
deleteProfileBtn.setEnabled(true);
|
||||
loadProfileFields(h);
|
||||
} else {
|
||||
selectedProfileId = null;
|
||||
deleteProfileBtn.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void loadProfile(SavedHost h) {
|
||||
if (h == null) return;
|
||||
selectedProfileId = h.getId();
|
||||
refreshProfileCombo();
|
||||
loadProfileFields(h);
|
||||
}
|
||||
|
||||
private void loadProfileFields(SavedHost h) {
|
||||
if (h == null) return;
|
||||
hostField.setText(h.getHost());
|
||||
portField.setText(String.valueOf(h.getPort()));
|
||||
if (h.getModel() == 0) {
|
||||
modelCombo.setSelectedItem(TerminalModel.IBM_DYNAMIC);
|
||||
} else {
|
||||
try {
|
||||
modelCombo.setSelectedItem(TerminalModel.forModel(h.getModel(), true));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
|
||||
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
|
||||
luField.setText(h.getLuName() != null ? h.getLuName() : "");
|
||||
tlsCheckBox.setSelected(h.isUseTls());
|
||||
verifyCertCheckBox.setEnabled(h.isUseTls());
|
||||
verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
|
||||
tn3270eCheckBox.setSelected(h.isTn3270e());
|
||||
setInitialCodePage(h.getCodePage());
|
||||
|
||||
if (h.getGraphicsMode() != null) {
|
||||
graphicsCombo.setSelectedItem(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(h.getGraphicsMode()));
|
||||
}
|
||||
|
||||
// Per-connection proxy parameters
|
||||
String ptStr = h.getProxyType();
|
||||
try {
|
||||
ConnectionConfig.ProxyType pt = (ptStr != null && !ptStr.trim().isEmpty())
|
||||
? ConnectionConfig.ProxyType.valueOf(ptStr.toUpperCase())
|
||||
: ConnectionConfig.ProxyType.NONE;
|
||||
proxyTypeCombo.setSelectedItem(pt);
|
||||
} catch (IllegalArgumentException e) {
|
||||
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
|
||||
}
|
||||
proxyHostField.setText(h.getProxyHost() != null ? h.getProxyHost() : "");
|
||||
proxyPortField.setText(h.getProxyPort() > 0 ? String.valueOf(h.getProxyPort()) : "");
|
||||
proxyUserField.setText(h.getProxyUsername() != null ? h.getProxyUsername() : "");
|
||||
proxyPassField.setText(h.getProxyPassword() != null ? h.getProxyPassword() : "");
|
||||
}
|
||||
|
||||
private void onSaveProfile() {
|
||||
String hStr = hostField.getText().trim();
|
||||
if (hStr.isEmpty()) {
|
||||
hostField.requestFocus();
|
||||
JOptionPane.showMessageDialog(this, "Please specify a host before saving profile.", "Missing Host", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
String defaultName = hStr;
|
||||
Object currentSel = profileCombo.getSelectedItem();
|
||||
if (currentSel instanceof SavedHost) {
|
||||
defaultName = ((SavedHost) currentSel).getName();
|
||||
}
|
||||
|
||||
String profileName = (String) JOptionPane.showInputDialog(
|
||||
this,
|
||||
"Enter Host Profile Name:",
|
||||
"Save Profile",
|
||||
JOptionPane.PLAIN_MESSAGE,
|
||||
null,
|
||||
null,
|
||||
defaultName
|
||||
);
|
||||
|
||||
if (profileName == null || profileName.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
profileName = profileName.trim();
|
||||
|
||||
SavedHost hostToSave = buildCurrentSavedHost(profileName);
|
||||
HostStorage.saveHost(hostToSave);
|
||||
selectedProfileId = hostToSave.getId();
|
||||
refreshProfileCombo();
|
||||
}
|
||||
|
||||
private void onDeleteProfile() {
|
||||
if (selectedProfileId == null) return;
|
||||
int opt = JOptionPane.showConfirmDialog(this,
|
||||
"Are you sure you want to delete this host profile?",
|
||||
"Delete Profile",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (opt == JOptionPane.YES_OPTION) {
|
||||
HostStorage.deleteHost(selectedProfileId);
|
||||
selectedProfileId = null;
|
||||
refreshProfileCombo();
|
||||
}
|
||||
}
|
||||
|
||||
private SavedHost buildCurrentSavedHost(String profileName) {
|
||||
String id = selectedProfileId != null ? selectedProfileId : java.util.UUID.randomUUID().toString();
|
||||
String h = hostField.getText().trim();
|
||||
int port = 23;
|
||||
try {
|
||||
port = Integer.parseInt(portField.getText().trim());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
|
||||
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
|
||||
int mNum = (m != null) ? m.getModelNumber() : 4;
|
||||
int dRows = (Integer) dynamicRowsSpinner.getValue();
|
||||
int dCols = (Integer) dynamicColsSpinner.getValue();
|
||||
String lu = luField.getText().trim();
|
||||
boolean tls = tlsCheckBox.isSelected();
|
||||
boolean verify = verifyCertCheckBox.isSelected();
|
||||
boolean tn3270e = tn3270eCheckBox.isSelected();
|
||||
|
||||
String gfx = "BOTH";
|
||||
if (graphicsCombo.getSelectedItem() != null) {
|
||||
gfx = graphicsCombo.getSelectedItem().toString();
|
||||
}
|
||||
|
||||
String cp = "037";
|
||||
Object cpSel = codePageCombo.getSelectedItem();
|
||||
if (cpSel != null) {
|
||||
String s = cpSel.toString().trim();
|
||||
int dash = s.indexOf(" -");
|
||||
cp = (dash > 0) ? s.substring(0, dash).trim() : s;
|
||||
}
|
||||
|
||||
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
|
||||
String ptStr = (pt != null) ? pt.name() : "NONE";
|
||||
String pHost = proxyHostField.getText().trim();
|
||||
int pPort = 0;
|
||||
try {
|
||||
pPort = Integer.parseInt(proxyPortField.getText().trim());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
String pUser = proxyUserField.getText().trim();
|
||||
String pPass = new String(proxyPassField.getPassword());
|
||||
|
||||
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
|
||||
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
|
||||
}
|
||||
|
||||
// ================= Connect Action =================
|
||||
|
||||
private void onConnect() {
|
||||
String host = hostField.getText().trim();
|
||||
if (host.isEmpty()) {
|
||||
@@ -321,19 +707,32 @@ public class ConnectDialog extends JDialog {
|
||||
}
|
||||
|
||||
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
|
||||
if (selectedModel == null) {
|
||||
selectedModel = TerminalModel.IBM_3279_4;
|
||||
}
|
||||
|
||||
// Support full host string parsing if the user pasted flags or prefixes into host field
|
||||
if (host.contains(" ") || host.startsWith("L:") || host.startsWith("P:") || host.startsWith("--proxy=") || host.startsWith("-proxy=")) {
|
||||
result = ConnectionConfig.parseHostString(host, port, selectedModel);
|
||||
} else {
|
||||
result = new ConnectionConfig(host, port, selectedModel);
|
||||
if (selectedModel != null && selectedModel.isDynamic()) {
|
||||
}
|
||||
|
||||
if (selectedModel.isDynamic()) {
|
||||
int dRows = (Integer) dynamicRowsSpinner.getValue();
|
||||
int dCols = (Integer) dynamicColsSpinner.getValue();
|
||||
result.setDynamicDimensions(dRows, dCols);
|
||||
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
|
||||
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
|
||||
}
|
||||
|
||||
String lu = luField.getText().trim();
|
||||
if (!lu.isEmpty()) {
|
||||
result.setLuName(lu);
|
||||
}
|
||||
if (graphicsCombo.getSelectedItem() != null) {
|
||||
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
||||
}
|
||||
|
||||
Object cpSelection = codePageCombo.getSelectedItem();
|
||||
if (cpSelection != null) {
|
||||
@@ -361,6 +760,22 @@ public class ConnectDialog extends JDialog {
|
||||
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
|
||||
|
||||
// Apply per-connection proxy parameters strictly to this connection
|
||||
ConnectionConfig.ProxyType pType = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
|
||||
String pHost = proxyHostField.getText().trim();
|
||||
if (pType != null && pType != ConnectionConfig.ProxyType.NONE && !pHost.isEmpty()) {
|
||||
int pPort = 0;
|
||||
try {
|
||||
pPort = Integer.parseInt(proxyPortField.getText().trim());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
if (pPort <= 0) {
|
||||
pPort = (pType == ConnectionConfig.ProxyType.HTTP) ? 8080 : 1080;
|
||||
}
|
||||
String pUser = proxyUserField.getText().trim();
|
||||
String pPass = new String(proxyPassField.getPassword());
|
||||
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
|
||||
}
|
||||
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
@@ -407,4 +822,49 @@ public class ConnectDialog extends JDialog {
|
||||
codePageCombo.setSelectedItem(cp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial proxy settings for this connection.
|
||||
*/
|
||||
public void setInitialProxy(ConnectionConfig.ProxyType type, String host, int port, String user, String pass) {
|
||||
if (proxyTypeCombo != null) {
|
||||
proxyTypeCombo.setSelectedItem(type != null ? type : ConnectionConfig.ProxyType.NONE);
|
||||
}
|
||||
if (proxyHostField != null) {
|
||||
proxyHostField.setText(host != null ? host : "");
|
||||
}
|
||||
if (proxyPortField != null) {
|
||||
proxyPortField.setText(port > 0 ? String.valueOf(port) : "");
|
||||
}
|
||||
if (proxyUserField != null) {
|
||||
proxyUserField.setText(user != null ? user : "");
|
||||
}
|
||||
if (proxyPassField != null) {
|
||||
proxyPassField.setText(pass != null ? pass : "");
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectionConfig.ProxyType getSelectedProxyType() {
|
||||
return (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
|
||||
}
|
||||
|
||||
public String getProxyHost() {
|
||||
return proxyHostField.getText().trim();
|
||||
}
|
||||
|
||||
public int getProxyPort() {
|
||||
try {
|
||||
return Integer.parseInt(proxyPortField.getText().trim());
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getProxyUsername() {
|
||||
return proxyUserField.getText().trim();
|
||||
}
|
||||
|
||||
public String getProxyPassword() {
|
||||
return new String(proxyPassField.getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
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 java.awt.Frame;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class HostStorageAndProxyTest {
|
||||
|
||||
private List<SavedHost> originalHosts;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalHosts = HostStorage.getSavedHosts();
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
HostStorage.clearAll();
|
||||
HostStorage.saveAll(originalHosts);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost correctly models per-connection proxy and converts to ConnectionConfig")
|
||||
public void testSavedHostProxyToConnectionConfig() {
|
||||
// 1. Host with SOCKS5 proxy and auth
|
||||
SavedHost host1 = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Prod Mainframe",
|
||||
"mvs.corp.local",
|
||||
2323,
|
||||
4,
|
||||
43,
|
||||
80,
|
||||
"TSO01",
|
||||
false,
|
||||
"TSO",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"037",
|
||||
"SOCKS5",
|
||||
"proxy.internal.corp",
|
||||
1080,
|
||||
"admin",
|
||||
"secret123"
|
||||
);
|
||||
|
||||
ConnectionConfig cfg1 = host1.toConnectionConfig();
|
||||
assertEquals("mvs.corp.local", cfg1.getHost());
|
||||
assertEquals(2323, cfg1.getPort());
|
||||
assertTrue(cfg1.isUseTls());
|
||||
assertEquals("TSO01", cfg1.getLuName());
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, cfg1.getProxyType());
|
||||
assertEquals("proxy.internal.corp", cfg1.getProxyHost());
|
||||
assertEquals(1080, cfg1.getProxyPort());
|
||||
assertEquals("admin", cfg1.getProxyUsername());
|
||||
assertEquals("secret123", cfg1.getProxyPassword());
|
||||
|
||||
// 2. Host with direct connection (NONE proxy)
|
||||
SavedHost host2 = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Local VM370",
|
||||
"127.0.0.1",
|
||||
23,
|
||||
2,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
false,
|
||||
"CMS",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
"BOTH",
|
||||
"037",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
ConnectionConfig cfg2 = host2.toConnectionConfig();
|
||||
assertEquals("127.0.0.1", cfg2.getHost());
|
||||
assertEquals(23, cfg2.getPort());
|
||||
assertFalse(cfg2.isUseTls());
|
||||
assertEquals(ConnectionConfig.ProxyType.NONE, cfg2.getProxyType());
|
||||
assertNull(cfg2.getProxyHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage persists and restores multiple host profiles with isolated per-connection proxies")
|
||||
public void testHostStoragePersistenceAndIsolation() {
|
||||
SavedHost h1 = new SavedHost(
|
||||
"id-1",
|
||||
"System A (SOCKS5)",
|
||||
"sysa.corp",
|
||||
23,
|
||||
4,
|
||||
62,
|
||||
160,
|
||||
"",
|
||||
false,
|
||||
"TSO",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"037",
|
||||
"SOCKS5",
|
||||
"socks.gateway.net",
|
||||
1080,
|
||||
"alice",
|
||||
"passA"
|
||||
);
|
||||
|
||||
SavedHost h2 = new SavedHost(
|
||||
"id-2",
|
||||
"System B (HTTP CONNECT)",
|
||||
"sysb.dmz",
|
||||
992,
|
||||
5,
|
||||
27,
|
||||
132,
|
||||
"LU99",
|
||||
false,
|
||||
"TSO",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"1047",
|
||||
"HTTP",
|
||||
"squid.proxy.org",
|
||||
3128,
|
||||
"bob",
|
||||
"passB"
|
||||
);
|
||||
|
||||
SavedHost h3 = new SavedHost(
|
||||
"id-3",
|
||||
"System C (Direct)",
|
||||
"sysc.lan",
|
||||
23,
|
||||
2,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
true, // autoConnect
|
||||
"CMS",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"500",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
HostStorage.saveHost(h1);
|
||||
HostStorage.saveHost(h2);
|
||||
HostStorage.saveHost(h3);
|
||||
|
||||
List<SavedHost> loaded = HostStorage.getSavedHosts();
|
||||
assertEquals(3, loaded.size());
|
||||
|
||||
SavedHost loadedA = HostStorage.findById("id-1");
|
||||
assertNotNull(loadedA);
|
||||
assertEquals("System A (SOCKS5)", loadedA.getName());
|
||||
assertEquals("SOCKS5", loadedA.getProxyType());
|
||||
assertEquals("socks.gateway.net", loadedA.getProxyHost());
|
||||
assertEquals(1080, loadedA.getProxyPort());
|
||||
assertEquals("alice", loadedA.getProxyUsername());
|
||||
assertEquals("passA", loadedA.getProxyPassword());
|
||||
|
||||
SavedHost loadedB = HostStorage.findById("id-2");
|
||||
assertNotNull(loadedB);
|
||||
assertEquals("HTTP", loadedB.getProxyType());
|
||||
assertEquals("squid.proxy.org", loadedB.getProxyHost());
|
||||
assertEquals(3128, loadedB.getProxyPort());
|
||||
assertEquals("bob", loadedB.getProxyUsername());
|
||||
assertEquals("passB", loadedB.getProxyPassword());
|
||||
|
||||
SavedHost loadedC = HostStorage.findById("id-3");
|
||||
assertNotNull(loadedC);
|
||||
assertEquals("NONE", loadedC.getProxyType());
|
||||
assertEquals("", loadedC.getProxyHost());
|
||||
assertTrue(loadedC.isAutoConnect());
|
||||
|
||||
// Verify getAutoConnectHost returns System C with its own connection settings
|
||||
SavedHost auto = HostStorage.getAutoConnectHost();
|
||||
assertNotNull(auto);
|
||||
assertEquals("id-3", auto.getId());
|
||||
assertEquals(ConnectionConfig.ProxyType.NONE, auto.toConnectionConfig().getProxyType());
|
||||
|
||||
// Verify deleteHost
|
||||
HostStorage.deleteHost("id-2");
|
||||
assertEquals(2, HostStorage.getSavedHosts().size());
|
||||
assertNull(HostStorage.findById("id-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage JSON matches a3270 format and parses a3270 JSON strings")
|
||||
public void testA3270JsonFormatCompatibility() {
|
||||
// String representative of a3270 saved_hosts preference JSON
|
||||
String a3270Json = "[\n" +
|
||||
" {\n" +
|
||||
" \"id\": \"android-uuid-1\",\n" +
|
||||
" \"name\": \"Dallas Mainframe\",\n" +
|
||||
" \"host\": \"dallas.ibm.com\",\n" +
|
||||
" \"port\": 2323,\n" +
|
||||
" \"model\": 4,\n" +
|
||||
" \"dynamicRows\": 62,\n" +
|
||||
" \"dynamicCols\": 160,\n" +
|
||||
" \"luName\": \"DAL01\",\n" +
|
||||
" \"autoConnect\": false,\n" +
|
||||
" \"hostType\": \"TSO\",\n" +
|
||||
" \"useTls\": true,\n" +
|
||||
" \"tlsVerifyCert\": false,\n" +
|
||||
" \"tn3270e\": true,\n" +
|
||||
" \"graphicsMode\": \"BOTH\",\n" +
|
||||
" \"codePage\": \"037\",\n" +
|
||||
" \"proxyType\": \"SOCKS5\",\n" +
|
||||
" \"proxyHost\": \"10.20.30.40\",\n" +
|
||||
" \"proxyPort\": 9050,\n" +
|
||||
" \"proxyUsername\": \"dallasuser\",\n" +
|
||||
" \"proxyPassword\": \"dallaspass\"\n" +
|
||||
" }\n" +
|
||||
"]";
|
||||
|
||||
List<SavedHost> parsed = HostStorage.parseHostsJson(a3270Json);
|
||||
assertEquals(1, parsed.size());
|
||||
SavedHost h = parsed.get(0);
|
||||
assertEquals("android-uuid-1", h.getId());
|
||||
assertEquals("Dallas Mainframe", h.getName());
|
||||
assertEquals("dallas.ibm.com", h.getHost());
|
||||
assertEquals(2323, h.getPort());
|
||||
assertEquals(4, h.getModel());
|
||||
assertTrue(h.isUseTls());
|
||||
assertFalse(h.isTlsVerifyCert());
|
||||
assertEquals("SOCKS5", h.getProxyType());
|
||||
assertEquals("10.20.30.40", h.getProxyHost());
|
||||
assertEquals(9050, h.getProxyPort());
|
||||
assertEquals("dallasuser", h.getProxyUsername());
|
||||
assertEquals("dallaspass", h.getProxyPassword());
|
||||
|
||||
// Roundtrip serialization
|
||||
String serialized = HostStorage.serializeHostsJson(parsed);
|
||||
List<SavedHost> roundtripped = HostStorage.parseHostsJson(serialized);
|
||||
assertEquals(1, roundtripped.size());
|
||||
SavedHost r = roundtripped.get(0);
|
||||
assertEquals(h.getId(), r.getId());
|
||||
assertEquals(h.getProxyType(), r.getProxyType());
|
||||
assertEquals(h.getProxyHost(), r.getProxyHost());
|
||||
assertEquals(h.getProxyPort(), r.getProxyPort());
|
||||
assertEquals(h.getProxyUsername(), r.getProxyUsername());
|
||||
assertEquals(h.getProxyPassword(), r.getProxyPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectDialog loads profile proxy and applies per-connection proxy to ConnectionConfig")
|
||||
public void testConnectDialogProxyHandling() {
|
||||
if (!GraphicsEnvironment.isHeadless()) {
|
||||
ConnectDialog dialog = new ConnectDialog((Frame) null);
|
||||
dialog.setInitialHost("test.mainframe.com");
|
||||
dialog.setInitialPort(23);
|
||||
dialog.setInitialProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", 1080, "testuser", "testpass");
|
||||
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, dialog.getSelectedProxyType());
|
||||
assertEquals("127.0.0.1", dialog.getProxyHost());
|
||||
assertEquals(1080, dialog.getProxyPort());
|
||||
assertEquals("testuser", dialog.getProxyUsername());
|
||||
assertEquals("testpass", dialog.getProxyPassword());
|
||||
|
||||
// Loading a profile updates dialog proxy fields
|
||||
SavedHost profile = new SavedHost(
|
||||
"p1", "Profile 1", "prod.ibm.net", 992, 4, 43, 80, "", false,
|
||||
"TSO", true, true, true, "BOTH", "037",
|
||||
"HTTP", "proxy.corp.net", 8080, "corpuser", "corppass"
|
||||
);
|
||||
dialog.loadProfile(profile);
|
||||
|
||||
assertEquals(ConnectionConfig.ProxyType.HTTP, dialog.getSelectedProxyType());
|
||||
assertEquals("proxy.corp.net", dialog.getProxyHost());
|
||||
assertEquals(8080, dialog.getProxyPort());
|
||||
assertEquals("corpuser", dialog.getProxyUsername());
|
||||
assertEquals("corppass", dialog.getProxyPassword());
|
||||
dialog.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,6 +548,8 @@ public class DataStreamProcessor {
|
||||
applyExtendedAttribute(ea, attrType, attrValue);
|
||||
}
|
||||
}
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 2 + nPairs * 2;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
|
||||
+73
@@ -198,4 +198,77 @@ public class DataStreamProcessorTest {
|
||||
processor.processRecord(ewStream.toByteArray(), 0, ewStream.size(), true);
|
||||
assertEquals((byte) 0, screen.getCell(0).fg);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyFieldAdvancesBufferAddressAndPreservesFieldAttributes() throws java.io.IOException {
|
||||
// Step 1: Initialize screen with an Erase/Write defining two fields
|
||||
java.io.ByteArrayOutputStream initStream = new java.io.ByteArrayOutputStream();
|
||||
initStream.write(CMD_EW);
|
||||
initStream.write(0xC3);
|
||||
// Field 1 at pos 0: SFE with FA_PRINTABLE, text "ABC"
|
||||
initStream.write(ORDER_SBA);
|
||||
initStream.write(0x40); initStream.write(0x40); // pos 0
|
||||
initStream.write(ORDER_SFE);
|
||||
initStream.write(0x01); // 1 pair
|
||||
initStream.write(XA_3270);
|
||||
initStream.write(FA_PRINTABLE);
|
||||
initStream.write(translator.stringToEbcdic("ABC"));
|
||||
|
||||
// Field 2 at pos 10: SFE with FA_PRINTABLE, text "XYZ"
|
||||
initStream.write(ORDER_SBA);
|
||||
byte[] addr10 = encodeAddress(10, screen.getRows(), screen.getCols());
|
||||
initStream.write(addr10);
|
||||
initStream.write(ORDER_SFE);
|
||||
initStream.write(0x01);
|
||||
initStream.write(XA_3270);
|
||||
initStream.write(FA_PRINTABLE);
|
||||
initStream.write(translator.stringToEbcdic("XYZ"));
|
||||
|
||||
processor.processRecord(initStream.toByteArray(), 0, initStream.size(), true);
|
||||
|
||||
assertTrue(screen.getCell(0).isFieldAttribute());
|
||||
assertEquals('A', screen.getCell(1).ucs4);
|
||||
assertTrue(screen.getCell(10).isFieldAttribute());
|
||||
assertEquals('X', screen.getCell(11).ucs4);
|
||||
|
||||
// Step 2: Issue a Write command that uses ORDER_MF on Field 1 and Field 2
|
||||
java.io.ByteArrayOutputStream updateStream = new java.io.ByteArrayOutputStream();
|
||||
updateStream.write(CMD_W);
|
||||
updateStream.write(0xC1);
|
||||
// SBA to pos 0
|
||||
updateStream.write(ORDER_SBA);
|
||||
updateStream.write(0x40); updateStream.write(0x40);
|
||||
// MF on pos 0: set foreground to green (0xF4)
|
||||
updateStream.write(ORDER_MF);
|
||||
updateStream.write(0x01); // 1 pair
|
||||
updateStream.write(XA_FOREGROUND);
|
||||
updateStream.write(0xF4);
|
||||
// Data "123" following MF directly
|
||||
updateStream.write(translator.stringToEbcdic("123"));
|
||||
|
||||
// MF on pos 10
|
||||
updateStream.write(ORDER_SBA);
|
||||
updateStream.write(addr10);
|
||||
updateStream.write(ORDER_MF);
|
||||
updateStream.write(0x01);
|
||||
updateStream.write(XA_FOREGROUND);
|
||||
updateStream.write(0xF5); // turquoise
|
||||
updateStream.write(translator.stringToEbcdic("789"));
|
||||
|
||||
processor.processRecord(updateStream.toByteArray(), 0, updateStream.size(), false);
|
||||
|
||||
// Verify Field 1: pos 0 MUST still be a field attribute, NOT overwritten by '1'
|
||||
assertTrue(screen.getCell(0).isFieldAttribute(), "Position 0 must remain a field attribute");
|
||||
assertEquals((byte) 0xF4, screen.getCell(0).fg, "Position 0 field attribute must have updated fg");
|
||||
assertEquals('1', screen.getCell(1).ucs4, "Data must start at pos 1, not overwrite pos 0");
|
||||
assertEquals('2', screen.getCell(2).ucs4);
|
||||
assertEquals('3', screen.getCell(3).ucs4);
|
||||
|
||||
// Verify Field 2: pos 10 MUST still be a field attribute, NOT overwritten by '7'
|
||||
assertTrue(screen.getCell(10).isFieldAttribute(), "Position 10 must remain a field attribute");
|
||||
assertEquals((byte) 0xF5, screen.getCell(10).fg, "Position 10 field attribute must have updated fg");
|
||||
assertEquals('7', screen.getCell(11).ucs4, "Data must start at pos 11, not overwrite pos 10");
|
||||
assertEquals('8', screen.getCell(12).ucs4);
|
||||
assertEquals('9', screen.getCell(13).ucs4);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user