This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
/**
|
||||
* Configuration for a 3270 terminal connection.
|
||||
*/
|
||||
public class ConnectionConfig {
|
||||
|
||||
private String host;
|
||||
private int port = 23;
|
||||
private TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
private String luName = null;
|
||||
private boolean extendedDataStream = true;
|
||||
private boolean useTls = false;
|
||||
private boolean tlsVerifyCert = true;
|
||||
private haus.nightmare.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
|
||||
private String sslProtocol = "TLS";
|
||||
private int connectTimeoutMs = 15000;
|
||||
private int nopIntervalSeconds = 0;
|
||||
private String terminalName = null; // override terminal type string
|
||||
private boolean tn3270eEnabled = true;
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
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 TerminalModel getModel() { return model; }
|
||||
public void setModel(TerminalModel model) { this.model = model; }
|
||||
|
||||
public String getLuName() { return luName; }
|
||||
public void setLuName(String luName) { this.luName = luName; }
|
||||
|
||||
public boolean isExtendedDataStream() { return extendedDataStream; }
|
||||
public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; }
|
||||
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
||||
|
||||
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
|
||||
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
|
||||
|
||||
public haus.nightmare.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||
public void setCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||
|
||||
public String getSslProtocol() { return sslProtocol; }
|
||||
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
|
||||
|
||||
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
||||
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
|
||||
|
||||
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
||||
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
|
||||
public void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
public String getTerminalName() { return terminalName; }
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
|
||||
*/
|
||||
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||
return new ConnectionConfig("localhost", defaultPort, defaultModel);
|
||||
}
|
||||
String s = hostStr.trim();
|
||||
boolean tls = false;
|
||||
boolean tn3270e = true;
|
||||
|
||||
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
|
||||
boolean prefixFound = true;
|
||||
while (prefixFound) {
|
||||
prefixFound = false;
|
||||
if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) {
|
||||
tls = true;
|
||||
s = s.substring(2);
|
||||
prefixFound = true;
|
||||
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
||||
tls = true;
|
||||
s = s.substring(4);
|
||||
prefixFound = true;
|
||||
} else if (s.startsWith("N:") || s.startsWith("n:") || s.toLowerCase().startsWith("notls:") || s.toLowerCase().startsWith("nossl:")) {
|
||||
tls = false;
|
||||
int colon = s.indexOf(':');
|
||||
s = s.substring(colon + 1);
|
||||
prefixFound = true;
|
||||
} else if (s.startsWith("P:") || s.startsWith("p:")) {
|
||||
tn3270e = false;
|
||||
s = s.substring(2);
|
||||
prefixFound = true;
|
||||
} else if (s.toLowerCase().startsWith("plain:") || s.toLowerCase().startsWith("non-e:")) {
|
||||
tn3270e = false;
|
||||
int colon = s.indexOf(':');
|
||||
s = s.substring(colon + 1);
|
||||
prefixFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
String host = s;
|
||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||
|
||||
// Check for host:port (handle IPv6 [::1]:port)
|
||||
if (s.startsWith("[") && s.contains("]")) {
|
||||
int closeBracket = s.indexOf(']');
|
||||
host = s.substring(1, closeBracket);
|
||||
if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(closeBracket + 2));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
} else {
|
||||
int colon = s.lastIndexOf(':');
|
||||
if (colon > 0 && colon < s.length() - 1) {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(colon + 1));
|
||||
host = s.substring(0, colon);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective terminal type string to send during negotiation.
|
||||
*/
|
||||
public String getEffectiveTerminalType() {
|
||||
if (terminalName != null) {
|
||||
return terminalName;
|
||||
}
|
||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
/**
|
||||
* Connection state machine states.
|
||||
* Mirrors the cstate enum from globals.h in x3270.
|
||||
*/
|
||||
public enum ConnectionState {
|
||||
NOT_CONNECTED, // No socket, unknown mode
|
||||
RECONNECTING, // Delay before automatic reconnect
|
||||
RESOLVING, // Resolving hostname
|
||||
TCP_PENDING, // Socket connection pending
|
||||
TLS_PENDING, // TLS negotiation pending
|
||||
PROXY_PENDING, // Proxy negotiation pending
|
||||
TELNET_PENDING, // Telnet negotiation pending
|
||||
CONNECTED_NVT, // Connected in NVT line mode
|
||||
CONNECTED_NVT_CHAR, // Connected in NVT character-at-a-time mode
|
||||
CONNECTED_3270, // Connected in RFC 1576 TN3270 mode
|
||||
CONNECTED_UNBOUND, // Connected in TN3270E mode, unbound
|
||||
CONNECTED_E_NVT, // Connected in TN3270E NVT mode
|
||||
CONNECTED_SSCP, // Connected in TN3270E SSCP-LU mode
|
||||
CONNECTED_TN3270E; // Connected in TN3270E 3270 mode
|
||||
|
||||
/** True if any kind of connection exists (even half-connected). */
|
||||
public boolean isConnected() {
|
||||
return this.ordinal() > NOT_CONNECTED.ordinal();
|
||||
}
|
||||
|
||||
/** True if in a half-connected state (resolving through telnet pending). */
|
||||
public boolean isHalfConnected() {
|
||||
return this.ordinal() >= RESOLVING.ordinal() && this.ordinal() < CONNECTED_NVT.ordinal();
|
||||
}
|
||||
|
||||
/** True if fully connected (past TCP pending). */
|
||||
public boolean isFullyConnected() {
|
||||
return this.ordinal() > TCP_PENDING.ordinal();
|
||||
}
|
||||
|
||||
/** True if in NVT mode (any flavor). */
|
||||
public boolean isNvt() {
|
||||
return this == CONNECTED_NVT || this == CONNECTED_NVT_CHAR || this == CONNECTED_E_NVT;
|
||||
}
|
||||
|
||||
/** True if in 3270 mode (any flavor). */
|
||||
public boolean is3270() {
|
||||
return this == CONNECTED_3270 || this == CONNECTED_TN3270E || this == CONNECTED_SSCP;
|
||||
}
|
||||
|
||||
/** True if in SSCP-LU mode. */
|
||||
public boolean isSscp() {
|
||||
return this == CONNECTED_SSCP;
|
||||
}
|
||||
|
||||
/** True if in TN3270E mode (any submode). */
|
||||
public boolean isTn3270e() {
|
||||
return this.ordinal() >= CONNECTED_UNBOUND.ordinal();
|
||||
}
|
||||
|
||||
/** True if in a full data session (NVT or 3270). */
|
||||
public boolean isFullSession() {
|
||||
return isNvt() || is3270();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.listener.*;
|
||||
|
||||
import java.util.logging.*;
|
||||
|
||||
/**
|
||||
* Diagnostic: login as guest1/guest and trace the Welcome → MOTD transition.
|
||||
*/
|
||||
public class DiagnosticClient {
|
||||
|
||||
static volatile int screenUpdateCount = 0;
|
||||
static Telnet3270Client client;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Logger rootLogger = Logger.getLogger("haus.nightmare.lib3270j");
|
||||
rootLogger.setLevel(Level.ALL);
|
||||
Handler handler = new ConsoleHandler();
|
||||
handler.setLevel(Level.ALL);
|
||||
handler.setFormatter(new SimpleFormatter());
|
||||
rootLogger.addHandler(handler);
|
||||
Logger.getLogger("").setLevel(Level.WARNING);
|
||||
|
||||
String host = args.length >= 1 ? args[0] : "192.168.0.30";
|
||||
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4);
|
||||
client = new Telnet3270Client(config);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
System.out.println(">>> STATE: " + oldState + " -> " + newState);
|
||||
}
|
||||
@Override
|
||||
public void onConnectionError(String message) {
|
||||
System.out.println(">>> ERROR: " + message);
|
||||
}
|
||||
@Override
|
||||
public void onTN3270ENegotiated(String deviceType, String deviceName) {
|
||||
System.out.println(">>> TN3270E: type=" + deviceType + " name=" + deviceName);
|
||||
}
|
||||
});
|
||||
|
||||
client.addScreenUpdateListener(new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
screenUpdateCount++;
|
||||
System.out.println("\n>>> SCREEN UPDATE #" + screenUpdateCount);
|
||||
dumpScreen();
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
@Override
|
||||
public void onSoundAlarm() {
|
||||
System.out.println(">>> ALARM");
|
||||
}
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
System.out.println(">>> SCREEN SIZE: " + rows + "x" + cols);
|
||||
}
|
||||
});
|
||||
|
||||
System.out.println("=== Connecting to " + host + ":" + port + " ===");
|
||||
client.connect();
|
||||
|
||||
// Wait for login screen
|
||||
waitForUpdates(5000);
|
||||
|
||||
// Type guest1 at login field
|
||||
System.out.println("\n=== Typing 'guest1' ===");
|
||||
typeAndWait("guest1", 2000);
|
||||
|
||||
// Press Enter to submit login
|
||||
System.out.println("\n=== Pressing Enter (submit login) ===");
|
||||
client.sendEnter();
|
||||
waitForUpdates(3000);
|
||||
|
||||
// Check if we need to enter password
|
||||
System.out.println("\n=== Typing password 'guest' ===");
|
||||
typeAndWait("guest", 2000);
|
||||
client.sendEnter();
|
||||
waitForUpdates(5000);
|
||||
|
||||
// Now we should see Welcome/allocations screen
|
||||
// Wait for *** prompt
|
||||
Thread.sleep(3000);
|
||||
System.out.println("\n=== Current screen (should be Welcome/allocations with ***) ===");
|
||||
dumpScreen();
|
||||
|
||||
// Press Enter at *** to continue — THIS is where the screen should clear
|
||||
System.out.println("\n=== Pressing Enter at *** (Welcome → MOTD transition) ===");
|
||||
client.sendEnter();
|
||||
waitForUpdates(5000);
|
||||
|
||||
// Wait for more updates
|
||||
Thread.sleep(3000);
|
||||
System.out.println("\n=== After *** Enter — screen should have been cleared for MOTD ===");
|
||||
dumpScreen();
|
||||
|
||||
// If there's another *** prompt, press Enter again
|
||||
System.out.println("\n=== Pressing Enter again ===");
|
||||
client.sendEnter();
|
||||
waitForUpdates(5000);
|
||||
Thread.sleep(2000);
|
||||
dumpScreen();
|
||||
|
||||
// One more Enter
|
||||
System.out.println("\n=== Pressing Enter one more time ===");
|
||||
client.sendEnter();
|
||||
waitForUpdates(5000);
|
||||
Thread.sleep(2000);
|
||||
dumpScreen();
|
||||
|
||||
// Let it sit for any more data
|
||||
Thread.sleep(3000);
|
||||
|
||||
System.out.println("\n=== Final screen ===");
|
||||
dumpScreen();
|
||||
|
||||
client.disconnect();
|
||||
System.out.println(">>> Disconnected");
|
||||
}
|
||||
|
||||
static void typeAndWait(String text, int waitMs) throws InterruptedException {
|
||||
for (char c : text.toCharArray()) {
|
||||
client.typeCharacter(c);
|
||||
Thread.sleep(30);
|
||||
}
|
||||
Thread.sleep(waitMs);
|
||||
}
|
||||
|
||||
static void waitForUpdates(int timeoutMs) throws InterruptedException {
|
||||
int start = screenUpdateCount;
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
while (screenUpdateCount == start && System.currentTimeMillis() < deadline) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
if (screenUpdateCount == start) {
|
||||
System.out.println(">>> (no screen update within " + timeoutMs + "ms)");
|
||||
}
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
static void dumpScreen() {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getRows();
|
||||
int cols = sb.getCols();
|
||||
System.out.println("--- Screen " + rows + "x" + cols +
|
||||
" cursor=" + sb.getCursorAddress() +
|
||||
" formatted=" + sb.isFormatted() +
|
||||
" state=" + client.getConnectionState() + " ---");
|
||||
for (int r = 0; r < rows; r++) {
|
||||
StringBuilder line = new StringBuilder();
|
||||
boolean hasContent = false;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
ExtendedAttribute ea = sb.getCell(r * cols + c);
|
||||
if (ea.isFieldAttribute()) {
|
||||
line.append('|');
|
||||
} else if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
|
||||
line.append(ea.ucs4);
|
||||
hasContent = true;
|
||||
} else {
|
||||
line.append(' ');
|
||||
}
|
||||
}
|
||||
if (hasContent) {
|
||||
System.out.println(String.format("%02d: %s", r, line.toString().stripTrailing()));
|
||||
}
|
||||
}
|
||||
System.out.println("---");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetConnection;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Main API entry point for lib3270j.
|
||||
*
|
||||
* Provides a high-level interface for connecting to a TN3270 host,
|
||||
* managing the screen buffer, and handling user input.
|
||||
*
|
||||
* Usage:
|
||||
* <pre>
|
||||
* ConnectionConfig config = new ConnectionConfig("hostname", 23, TerminalModel.IBM_3279_4);
|
||||
* Telnet3270Client client = new Telnet3270Client(config);
|
||||
* client.addConnectionListener(myListener);
|
||||
* client.addScreenUpdateListener(myScreenListener);
|
||||
* client.connect();
|
||||
* // ... interact with screen ...
|
||||
* client.disconnect();
|
||||
* </pre>
|
||||
*/
|
||||
public class Telnet3270Client {
|
||||
|
||||
private static final Logger log = Logger.getLogger(Telnet3270Client.class.getName());
|
||||
|
||||
private final ConnectionConfig config;
|
||||
private final EbcdicTranslator translator;
|
||||
private final ScreenBuffer screenBuffer;
|
||||
private final DataStreamProcessor dsProcessor;
|
||||
private final TelnetFSM fsm;
|
||||
private final InputProcessor inputProcessor;
|
||||
private TelnetConnection connection;
|
||||
|
||||
public Telnet3270Client(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
this.translator = new EbcdicTranslator();
|
||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
|
||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||
dsProcessor.setInputProcessor(inputProcessor);
|
||||
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
||||
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the configured host.
|
||||
* This method blocks until the TCP connection is established,
|
||||
* then returns while telnet/TN3270E negotiation continues asynchronously.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
|
||||
" model=" + config.getModel());
|
||||
|
||||
connection = new TelnetConnection(config, fsm);
|
||||
fsm.setConnection(connection);
|
||||
|
||||
// Establish TCP connection
|
||||
connection.connect();
|
||||
|
||||
// Notify FSM that TCP is connected — begins telnet negotiation
|
||||
fsm.onConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
fsm.onDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected (any state past TCP pending).
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return connection != null && connection.isConnected();
|
||||
}
|
||||
|
||||
// ========== Listener management ==========
|
||||
|
||||
public void addConnectionListener(ConnectionListener l) {
|
||||
fsm.addConnectionListener(l);
|
||||
}
|
||||
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
fsm.addScreenUpdateListener(l);
|
||||
dsProcessor.addScreenUpdateListener(l);
|
||||
}
|
||||
|
||||
// ========== Screen access ==========
|
||||
|
||||
/** Get the screen buffer for rendering. */
|
||||
public ScreenBuffer getScreenBuffer() { return screenBuffer; }
|
||||
|
||||
/** Get the EBCDIC translator. */
|
||||
public EbcdicTranslator getTranslator() { return translator; }
|
||||
|
||||
/** Get the current connection state. */
|
||||
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
|
||||
|
||||
/** Get the input processor for keyboard operations. */
|
||||
public InputProcessor getInputProcessor() { return inputProcessor; }
|
||||
|
||||
/** Get the data stream processor. */
|
||||
public DataStreamProcessor getDataStreamProcessor() { return dsProcessor; }
|
||||
|
||||
/** Get the connection config. */
|
||||
public ConnectionConfig getConfig() { return config; }
|
||||
|
||||
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||
config.setCertificateVerifier(verifier);
|
||||
}
|
||||
|
||||
/** Get the active SSLSession if connected over TLS, or null. */
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return connection.getSslSession();
|
||||
}
|
||||
|
||||
// ========== Convenience input methods ==========
|
||||
|
||||
/** Type a character at the cursor position. */
|
||||
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
|
||||
|
||||
/** Type a string at the cursor position. */
|
||||
public void typeString(String s) {
|
||||
for (char ch : s.toCharArray()) {
|
||||
inputProcessor.typeCharacter(ch);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emulate input of a string, pressing Enter on newlines. */
|
||||
public void emulateInput(String s) {
|
||||
inputProcessor.emulateInput(s);
|
||||
}
|
||||
|
||||
/** Send Enter key. */
|
||||
public void sendEnter() {
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
}
|
||||
|
||||
/** Send a PF key (1-24). */
|
||||
public void sendPF(int number) {
|
||||
int aid;
|
||||
switch (number) {
|
||||
case 1: aid = 0xf1; break; case 2: aid = 0xf2; break;
|
||||
case 3: aid = 0xf3; break; case 4: aid = 0xf4; break;
|
||||
case 5: aid = 0xf5; break; case 6: aid = 0xf6; break;
|
||||
case 7: aid = 0xf7; break; case 8: aid = 0xf8; break;
|
||||
case 9: aid = 0xf9; break; case 10: aid = 0x7a; break;
|
||||
case 11: aid = 0x7b; break; case 12: aid = 0x7c; break;
|
||||
case 13: aid = 0xc1; break; case 14: aid = 0xc2; break;
|
||||
case 15: aid = 0xc3; break; case 16: aid = 0xc4; break;
|
||||
case 17: aid = 0xc5; break; case 18: aid = 0xc6; break;
|
||||
case 19: aid = 0xc7; break; case 20: aid = 0xc8; break;
|
||||
case 21: aid = 0xc9; break; case 22: aid = 0x4a; break;
|
||||
case 23: aid = 0x4b; break; case 24: aid = 0x4c; break;
|
||||
default: return;
|
||||
}
|
||||
inputProcessor.sendAid(aid);
|
||||
}
|
||||
|
||||
/** Send a PA key (1-3). */
|
||||
public void sendPA(int number) {
|
||||
int aid;
|
||||
switch (number) {
|
||||
case 1: aid = 0x6c; break;
|
||||
case 2: aid = 0x6e; break;
|
||||
case 3: aid = 0x6b; break;
|
||||
default: return;
|
||||
}
|
||||
inputProcessor.sendAid(aid);
|
||||
}
|
||||
|
||||
/** Send Clear key. */
|
||||
public void sendClear() {
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||
}
|
||||
|
||||
/** Move cursor up. */
|
||||
public void cursorUp() { inputProcessor.cursorUp(); }
|
||||
/** Move cursor down. */
|
||||
public void cursorDown() { inputProcessor.cursorDown(); }
|
||||
/** Move cursor left. */
|
||||
public void cursorLeft() { inputProcessor.cursorLeft(); }
|
||||
/** Move cursor right. */
|
||||
public void cursorRight() { inputProcessor.cursorRight(); }
|
||||
/** Move cursor to home position. */
|
||||
public void cursorHome() { inputProcessor.cursorHome(); }
|
||||
/** Tab to next unprotected field. */
|
||||
public void tab() { inputProcessor.tab(); }
|
||||
/** Back-tab to previous unprotected field. */
|
||||
public void backTab() { inputProcessor.backTab(); }
|
||||
/** Move cursor to next line. */
|
||||
public void newline() { inputProcessor.newline(); }
|
||||
/** Erase all unprotected fields. */
|
||||
public void eraseInput() { inputProcessor.eraseInput(); }
|
||||
/** Insert Duplicate order. */
|
||||
public void dup() { inputProcessor.dup(); }
|
||||
/** Insert Field Mark order. */
|
||||
public void fieldMark() { inputProcessor.fieldMark(); }
|
||||
/** Attention key. */
|
||||
public void attn() { inputProcessor.attn(); }
|
||||
/** SysReq key. */
|
||||
public void sysReq() { inputProcessor.sysReq(); }
|
||||
/** Reset (unlock keyboard). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return dsProcessor.getProgramSymbolManager();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||
return dsProcessor.getGraphicsPlane();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return dsProcessor.getGocaDecoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Terminal model definitions for IBM 3278 and 3279 terminals.
|
||||
* Models 2-5 are supported, each with default (24x80) and alternate screen sizes.
|
||||
*/
|
||||
public enum TerminalModel {
|
||||
IBM_3278_2(2, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
|
||||
IBM_3278_3(3, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
|
||||
IBM_3278_4(4, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
|
||||
IBM_3278_5(5, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS),
|
||||
IBM_3279_2(2, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
|
||||
IBM_3279_3(3, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
|
||||
IBM_3279_4(4, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
|
||||
IBM_3279_5(5, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS);
|
||||
|
||||
private final int modelNumber;
|
||||
private final boolean color;
|
||||
private final int defaultRows;
|
||||
private final int defaultCols;
|
||||
private final int alternateRows;
|
||||
private final int alternateCols;
|
||||
|
||||
TerminalModel(int modelNumber, boolean color,
|
||||
int defaultRows, int defaultCols,
|
||||
int alternateRows, int alternateCols) {
|
||||
this.modelNumber = modelNumber;
|
||||
this.color = color;
|
||||
this.defaultRows = defaultRows;
|
||||
this.defaultCols = defaultCols;
|
||||
this.alternateRows = alternateRows;
|
||||
this.alternateCols = alternateCols;
|
||||
}
|
||||
|
||||
public int getModelNumber() { return modelNumber; }
|
||||
public boolean isColor() { return color; }
|
||||
public int getDefaultRows() { return defaultRows; }
|
||||
public int getDefaultCols() { return defaultCols; }
|
||||
public int getAlternateRows() { return alternateRows; }
|
||||
public int getAlternateCols() { return alternateCols; }
|
||||
|
||||
/**
|
||||
* Returns the terminal type string for TN3270E negotiation.
|
||||
* e.g., "IBM-3279-4-E" for a color model 4 with extended data stream.
|
||||
*/
|
||||
public String getTerminalType() {
|
||||
return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base terminal type without "-E" suffix (for non-extended mode).
|
||||
*/
|
||||
public String getBaseTerminalType() {
|
||||
return String.format("IBM-327%c-%d", color ? '9' : '8', modelNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a model by number and color mode.
|
||||
*/
|
||||
public static TerminalModel forModel(int number, boolean isColor) {
|
||||
for (TerminalModel m : values()) {
|
||||
if (m.modelNumber == number && m.color == isColor) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown model: " + number + " color=" + isColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTerminalType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* EBCDIC ↔ Unicode translator.
|
||||
* Default: Code Page 037 (US/Canada EBCDIC).
|
||||
*/
|
||||
public class EbcdicTranslator {
|
||||
|
||||
/**
|
||||
* EBCDIC Code Page 037 to Unicode mapping.
|
||||
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint.
|
||||
*/
|
||||
private static final int[] CP037_TO_UNICODE = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F (space, accent chars, punctuation)
|
||||
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
|
||||
// 80-8F (lowercase a-i)
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F (lowercase j-r)
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF (lowercase s-z)
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF (uppercase A-I)
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF (uppercase J-R)
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF (uppercase S-Z)
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF (digits 0-9)
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
/**
|
||||
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1).
|
||||
* Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable).
|
||||
*/
|
||||
private static final int[] UNICODE_TO_CP037 = new int[256];
|
||||
|
||||
static {
|
||||
// Build reverse mapping
|
||||
java.util.Arrays.fill(UNICODE_TO_CP037, -1);
|
||||
for (int i = 0; i < 256; i++) {
|
||||
int unicode = CP037_TO_UNICODE[i];
|
||||
if (unicode < 256) {
|
||||
UNICODE_TO_CP037[unicode] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate EBCDIC byte to Unicode character.
|
||||
*/
|
||||
public char ebcdicToUnicode(int ebc) {
|
||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to translate EBCDIC byte to Unicode character.
|
||||
*/
|
||||
public static char toUnicode(int ebc) {
|
||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to translate EBCDIC byte to ASCII character.
|
||||
*/
|
||||
public static char ebcdicToAscii(int ebc) {
|
||||
return toUnicode(ebc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate Unicode character to EBCDIC byte.
|
||||
* Returns -1 if the character cannot be mapped.
|
||||
*/
|
||||
public int unicodeToEbcdic(char unicode) {
|
||||
if (unicode < 256) {
|
||||
return UNICODE_TO_CP037[unicode];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
|
||||
*/
|
||||
public byte unicodeToEbcdicSafe(char unicode) {
|
||||
int ebc = unicodeToEbcdic(unicode);
|
||||
return (byte) (ebc >= 0 ? ebc : 0x40);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a byte array from EBCDIC to a Unicode string.
|
||||
*/
|
||||
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
|
||||
StringBuilder sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a Unicode string to EBCDIC byte array.
|
||||
*/
|
||||
public byte[] stringToEbcdic(String s) {
|
||||
byte[] result = new byte[s.length()];
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
result[i] = unicodeToEbcdicSafe(s.charAt(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,515 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||
*/
|
||||
public class QueryReplyBuilder {
|
||||
|
||||
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
|
||||
|
||||
// Canned values from 3279-2 (matching sf.c)
|
||||
private static final int SW_3279_2 = 0x09;
|
||||
private static final int SH_3279_2 = 0x0c;
|
||||
private static final int Xr_3279_2 = 0x000a02e5;
|
||||
private static final int Yr_3279_2 = 0x0002006f;
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
||||
|
||||
// Base query reply codes (text mode)
|
||||
private static final int[] SUPPORTED_QR_BASE = {
|
||||
QR_SUMMARY, // 0x80 — summary must list itself
|
||||
QR_USABLE_AREA, // 0x81
|
||||
QR_ALPHA_PART, // 0x84
|
||||
QR_CHARSETS, // 0x85
|
||||
QR_COLOR, // 0x86
|
||||
QR_HIGHLIGHTING, // 0x87
|
||||
QR_REPLY_MODES, // 0x88
|
||||
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
|
||||
QR_IMP_PART, // 0xa6
|
||||
};
|
||||
|
||||
// Vector graphics query reply codes matching HOD DS3270.java line 1723
|
||||
private static final int[] SUPPORTED_QR_VECTOR = {
|
||||
QR_SUMMARY, // 0x80
|
||||
QR_USABLE_AREA, // 0x81
|
||||
QR_ALPHA_PART, // 0x84
|
||||
QR_CHARSETS, // 0x85
|
||||
QR_COLOR, // 0x86
|
||||
QR_HIGHLIGHTING, // 0x87
|
||||
QR_REPLY_MODES, // 0x88
|
||||
QR_SAVE_RESTORE, // 0x8c
|
||||
QR_DDM, // 0x95
|
||||
QR_TRANSPARENCY, // 0x99
|
||||
QR_IMP_PART, // 0xa6
|
||||
QR_RPQ_NAMES, // 0xa8
|
||||
QR_GRAPHICS, // 0xb0
|
||||
QR_GIMAGE, // 0xb1
|
||||
QR_AUX_DEV, // 0xb2
|
||||
QR_OEM_FMT, // 0xb3
|
||||
QR_GCOLOR, // 0xb4
|
||||
QR_GSYMBOLS, // 0xb6
|
||||
};
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen, GraphicsMode graphicsMode) {
|
||||
this.screen = screen;
|
||||
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
public GraphicsMode getGraphicsMode() {
|
||||
return graphicsMode;
|
||||
}
|
||||
|
||||
public void setGraphicsMode(GraphicsMode graphicsMode) {
|
||||
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all query replies as a single AID_SF + structured field response.
|
||||
*/
|
||||
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||
|
||||
// AID byte for structured field
|
||||
out.write(AID_SF);
|
||||
|
||||
// Summary
|
||||
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||
|
||||
// Usable Area
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
|
||||
// Alpha Partitions
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
|
||||
// Character Sets
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
|
||||
// Color
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
|
||||
// Highlighting
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
|
||||
// Reply Modes (0x88)
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
// Save/Restore (0x8C)
|
||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||
}
|
||||
|
||||
// Distributed Data Management (0x95)
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
// Transparency (0x99)
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
}
|
||||
|
||||
// Implicit Partition (0xA6)
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
// Vector Graphics QRs if enabled
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1
|
||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
|
||||
appendOemFmt(out); // 0xB3
|
||||
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
|
||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
|
||||
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
|
||||
* matching x3270 sf.c behavior.
|
||||
*/
|
||||
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
|
||||
if (requestedCodes == null || requestedCodes.length == 0) {
|
||||
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||
out.write(AID_SF);
|
||||
|
||||
for (byte codeByte : requestedCodes) {
|
||||
int code = codeByte & 0xFF;
|
||||
switch (code) {
|
||||
case QR_SUMMARY:
|
||||
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||
break;
|
||||
case QR_USABLE_AREA:
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
break;
|
||||
case QR_ALPHA_PART:
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
break;
|
||||
case QR_CHARSETS:
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
break;
|
||||
case QR_COLOR:
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
break;
|
||||
case QR_HIGHLIGHTING:
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
break;
|
||||
case QR_REPLY_MODES:
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
break;
|
||||
case QR_SAVE_RESTORE:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_DDM:
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
break;
|
||||
case QR_TRANSPARENCY:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_IMP_PART:
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
break;
|
||||
case QR_RPQ_NAMES:
|
||||
case QR_RPQNAMES:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, code, buildRpqNames());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GRAPHICS:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows));
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GIMAGE:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows));
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_AUX_DEV:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_OEM_FMT:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendOemFmt(out);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GCOLOR:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GSYMBOLS:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unsupported query reply code — emit QR_NULL
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private void appendQueryReply(ByteArrayOutputStream out, int code, byte[] data) {
|
||||
// Length includes the 2-byte length field + SFID_QREPLY + code + data
|
||||
int len = 4 + data.length;
|
||||
out.write((len >> 8) & 0xFF);
|
||||
out.write(len & 0xFF);
|
||||
out.write(SFID_QREPLY);
|
||||
out.write(code);
|
||||
out.write(data, 0, data.length);
|
||||
}
|
||||
|
||||
private byte[] buildSummary() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||
for (int code : codes) {
|
||||
out.write(code);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
||||
out.write(0x01); // 12/14-bit addressing
|
||||
out.write(0x00); // no special character features
|
||||
out.write((maxCols >> 8) & 0xFF); // usable width high
|
||||
out.write(maxCols & 0xFF); // usable width low
|
||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||
out.write(maxRows & 0xFF); // usable height low
|
||||
out.write(0x01); // units (mm)
|
||||
// Xr (4 bytes) - canned from 3279-2
|
||||
out.write((Xr_3279_2 >> 24) & 0xFF);
|
||||
out.write((Xr_3279_2 >> 16) & 0xFF);
|
||||
out.write((Xr_3279_2 >> 8) & 0xFF);
|
||||
out.write(Xr_3279_2 & 0xFF);
|
||||
// Yr (4 bytes) - canned from 3279-2
|
||||
out.write((Yr_3279_2 >> 24) & 0xFF);
|
||||
out.write((Yr_3279_2 >> 16) & 0xFF);
|
||||
out.write((Yr_3279_2 >> 8) & 0xFF);
|
||||
out.write(Yr_3279_2 & 0xFF);
|
||||
out.write(SW_3279_2); // AW
|
||||
out.write(SH_3279_2); // AH
|
||||
int buf = maxCols * maxRows;
|
||||
out.write((buf >> 8) & 0xFF); // buffer size high
|
||||
out.write(buf & 0xFF); // buffer size low
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildAlphaPartitions(int maxRows) {
|
||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
|
||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||
out.write(bufSize & 0xFF); // total partition storage low
|
||||
out.write(0x00); // flags
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW (9)
|
||||
out.write(SH_3279_2); // SDH (12)
|
||||
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
||||
out.write(0x00); // Load PS device type (high)
|
||||
out.write(0x00); // Load PS device type (low)
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x07); // DL = 7 bytes per descriptor
|
||||
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, CP037)
|
||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
|
||||
// Descriptor 2 (SET 1): APL/GE character set
|
||||
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane)
|
||||
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - Flags = 0xC0 (0x80 Loadable | 0x40 Triple-plane)
|
||||
out.write(0x04); out.write(0xc0); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x05); out.write(0xc0); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x06); out.write(0xc0); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x07); out.write(0xc0); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
// Standard 3179G / Base character sets (matches sf.c / HOD)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||
out.write(0x82); // flags: GE, CGCSGID present
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW - default char width (9)
|
||||
out.write(SH_3279_2); // SDH - default char height (12)
|
||||
out.write(0x00); // LoadPS format (0x00)
|
||||
out.write(0x00);
|
||||
out.write(0x00);
|
||||
out.write(0x00);
|
||||
out.write(0x07); // DL = 7
|
||||
// Set 0 (Base EBCDIC - Non-loadable, single plane, CP037)
|
||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
|
||||
// Set 1 (APL/Text)
|
||||
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildColor() {
|
||||
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
||||
return new byte[] {
|
||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||
(byte) 0xF1, (byte) 0xF1, // Blue
|
||||
(byte) 0xF2, (byte) 0xF2, // Red
|
||||
(byte) 0xF3, (byte) 0xF3, // Pink
|
||||
(byte) 0xF4, (byte) 0xF4, // Green
|
||||
(byte) 0xF5, (byte) 0xF5, // Turquoise
|
||||
(byte) 0xF6, (byte) 0xF6, // Yellow
|
||||
(byte) 0xF7, (byte) 0xF7 // Neutral/White
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildHighlighting() {
|
||||
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
||||
return new byte[] {
|
||||
0x04, 0x00, (byte) 0xF0,
|
||||
(byte) 0xF1, (byte) 0xF1,
|
||||
(byte) 0xF2, (byte) 0xF2,
|
||||
(byte) 0xF4, (byte) 0xF4
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildReplyModes() {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
}
|
||||
|
||||
private byte[] buildDdm(int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x00); // reserved
|
||||
out.write((bufferSize >> 8) & 0xFF); // inbound length limit (INLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write((bufferSize >> 8) & 0xFF); // outbound length limit (OUTLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write(0x01); // NSS = 01
|
||||
out.write(0x01); // DDMSS = 01
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||
// Implicit partition sizes, 2 self-defining parameters
|
||||
|
||||
// SDP 1: Default screen size (Model 2: 80x24)
|
||||
out.write(0x00); // flags
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x0b); // SDP length (11 bytes)
|
||||
out.write(0x01); // SDP type: implicit partition sizes
|
||||
out.write(0x00); // reserved
|
||||
// Default size
|
||||
out.write((MODEL_2_COLS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_COLS & 0xFF);
|
||||
out.write((MODEL_2_ROWS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_ROWS & 0xFF);
|
||||
// Alternate size (Model 4: 80x43, Model 5: 132x27, etc.)
|
||||
out.write((maxCols >> 8) & 0xFF);
|
||||
out.write(maxCols & 0xFF);
|
||||
out.write((maxRows >> 8) & 0xFF);
|
||||
out.write(maxRows & 0xFF);
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildGraphics(int maxCols, int maxRows) {
|
||||
int width = maxCols * 9;
|
||||
int height = maxRows * 12;
|
||||
return new byte[]{
|
||||
(byte) 0x80, 0x02,
|
||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
||||
0x00
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildGImage(int maxCols, int maxRows) {
|
||||
int width = maxCols * 9;
|
||||
int height = maxRows * 12;
|
||||
return new byte[]{
|
||||
0x00, 0x01,
|
||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
||||
0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildAuxDev() {
|
||||
return new byte[]{
|
||||
0x00, 0x09, 0x00, 0x07,
|
||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildSaveRestore() {
|
||||
// HOD DS3270.java line 1768: 6 bytes payload
|
||||
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
}
|
||||
|
||||
private byte[] buildTransparency() {
|
||||
// HOD DS3270.java line 1782: 2 bytes payload
|
||||
return new byte[]{ 0x00, 0x00 };
|
||||
}
|
||||
|
||||
private byte[] buildRpqNames() {
|
||||
// HOD DS3270.java line 1798: 5 bytes payload
|
||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||
}
|
||||
|
||||
private void appendOemFmt(ByteArrayOutputStream out) {
|
||||
// HOD DS3270.java line 1814: 4 distinct OEM format sub-fields
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] buildGColor() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
|
||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out.write(0x00);
|
||||
out.write(i);
|
||||
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||
int r = (argb >> 16) & 0xFF;
|
||||
int g = (argb >> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
out.write(r);
|
||||
out.write(g);
|
||||
out.write(b);
|
||||
out.write(0x00); // 6th byte in HOD color table
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildGSymbols() {
|
||||
return new byte[]{
|
||||
0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
|
||||
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
/**
|
||||
* Configuration for an IND$FILE file transfer session.
|
||||
* Ported from x3270's ft_conf_t (ft_private.h).
|
||||
*/
|
||||
public class FTConfig {
|
||||
|
||||
/** Host operating system type */
|
||||
public enum HostType {
|
||||
TSO, CMS, CICS
|
||||
}
|
||||
|
||||
/** Record format for datasets (TSO/CMS sends only) */
|
||||
public enum RecordFormat {
|
||||
DEFAULT, FIXED, VARIABLE, UNDEFINED
|
||||
}
|
||||
|
||||
/** Space allocation units (TSO sends only) */
|
||||
public enum AllocationUnit {
|
||||
DEFAULT, TRACKS, CYLINDERS, AVBLOCK
|
||||
}
|
||||
|
||||
/** Transfer direction */
|
||||
public enum Direction {
|
||||
RECEIVE, SEND
|
||||
}
|
||||
|
||||
/** Transfer mode */
|
||||
public enum TransferMode {
|
||||
ASCII, BINARY
|
||||
}
|
||||
|
||||
/** CR/LF handling */
|
||||
public enum CrAction {
|
||||
ADD, REMOVE, KEEP
|
||||
}
|
||||
|
||||
/** Behavior when destination file already exists */
|
||||
public enum ExistAction {
|
||||
KEEP, REPLACE, APPEND
|
||||
}
|
||||
|
||||
// ========== Transfer Parameters ==========
|
||||
|
||||
private String hostFilename;
|
||||
private String localFilename;
|
||||
private Direction direction = Direction.RECEIVE;
|
||||
private HostType hostType = HostType.TSO;
|
||||
private TransferMode transferMode = TransferMode.ASCII;
|
||||
private CrAction crAction = CrAction.REMOVE;
|
||||
private boolean remapFlag = true;
|
||||
private ExistAction existAction = ExistAction.KEEP;
|
||||
private RecordFormat recfm = RecordFormat.DEFAULT;
|
||||
private AllocationUnit units = AllocationUnit.DEFAULT;
|
||||
private int lrecl = 0;
|
||||
private int blksize = 0;
|
||||
private int primarySpace = 0;
|
||||
private int secondarySpace = 0;
|
||||
private int avblock = 0;
|
||||
private int dftBufferSize = FTConstants.DFT_BUF;
|
||||
private String otherOptions = null;
|
||||
|
||||
// ========== Derived convenience getters ==========
|
||||
|
||||
public boolean isReceive() { return direction == Direction.RECEIVE; }
|
||||
public boolean isSend() { return direction == Direction.SEND; }
|
||||
public boolean isAscii() { return transferMode == TransferMode.ASCII; }
|
||||
public boolean isBinary() { return transferMode == TransferMode.BINARY; }
|
||||
public boolean isCrFlag() {
|
||||
// CR processing is only applicable for ASCII transfers
|
||||
return isAscii() && crAction != CrAction.KEEP;
|
||||
}
|
||||
public boolean isAppend() { return existAction == ExistAction.APPEND; }
|
||||
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
|
||||
|
||||
// ========== Standard getters and setters ==========
|
||||
|
||||
public String getHostFilename() { return hostFilename; }
|
||||
public void setHostFilename(String hostFilename) { this.hostFilename = hostFilename; }
|
||||
|
||||
public String getLocalFilename() { return localFilename; }
|
||||
public void setLocalFilename(String localFilename) { this.localFilename = localFilename; }
|
||||
|
||||
public Direction getDirection() { return direction; }
|
||||
public void setDirection(Direction direction) { this.direction = direction; }
|
||||
|
||||
public HostType getHostType() { return hostType; }
|
||||
public void setHostType(HostType hostType) { this.hostType = hostType; }
|
||||
|
||||
public TransferMode getTransferMode() { return transferMode; }
|
||||
public void setTransferMode(TransferMode transferMode) { this.transferMode = transferMode; }
|
||||
|
||||
public CrAction getCrAction() { return crAction; }
|
||||
public void setCrAction(CrAction crAction) { this.crAction = crAction; }
|
||||
|
||||
public boolean isRemapFlag() { return remapFlag; }
|
||||
public void setRemapFlag(boolean remapFlag) { this.remapFlag = remapFlag; }
|
||||
|
||||
public ExistAction getExistAction() { return existAction; }
|
||||
public void setExistAction(ExistAction existAction) { this.existAction = existAction; }
|
||||
|
||||
public RecordFormat getRecfm() { return recfm; }
|
||||
public void setRecfm(RecordFormat recfm) { this.recfm = recfm; }
|
||||
|
||||
public AllocationUnit getUnits() { return units; }
|
||||
public void setUnits(AllocationUnit units) { this.units = units; }
|
||||
|
||||
public int getLrecl() { return lrecl; }
|
||||
public void setLrecl(int lrecl) { this.lrecl = lrecl; }
|
||||
|
||||
public int getBlksize() { return blksize; }
|
||||
public void setBlksize(int blksize) { this.blksize = blksize; }
|
||||
|
||||
public int getPrimarySpace() { return primarySpace; }
|
||||
public void setPrimarySpace(int primarySpace) { this.primarySpace = primarySpace; }
|
||||
|
||||
public int getSecondarySpace() { return secondarySpace; }
|
||||
public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; }
|
||||
|
||||
public int getAvblock() { return avblock; }
|
||||
public void setAvblock(int avblock) { this.avblock = avblock; }
|
||||
|
||||
public int getDftBufferSize() { return dftBufferSize; }
|
||||
public void setDftBufferSize(int dftBufferSize) {
|
||||
this.dftBufferSize = Math.max(FTConstants.DFT_MIN_BUF,
|
||||
Math.min(FTConstants.DFT_MAX_BUF, dftBufferSize));
|
||||
}
|
||||
|
||||
public String getOtherOptions() { return otherOptions; }
|
||||
public void setOtherOptions(String otherOptions) {
|
||||
this.otherOptions = (otherOptions != null && !otherOptions.trim().isEmpty())
|
||||
? otherOptions.trim() : null;
|
||||
}
|
||||
|
||||
// ========== Convenience UI setters ==========
|
||||
|
||||
public void setAppend(boolean append) {
|
||||
if (append) this.existAction = ExistAction.APPEND;
|
||||
}
|
||||
|
||||
public void setOverwrite(boolean overwrite) {
|
||||
if (overwrite) this.existAction = ExistAction.REPLACE;
|
||||
}
|
||||
|
||||
public void setReceive(boolean receive) {
|
||||
setDirection(receive ? Direction.RECEIVE : Direction.SEND);
|
||||
}
|
||||
|
||||
public void setAscii(boolean ascii) {
|
||||
setTransferMode(ascii ? TransferMode.ASCII : TransferMode.BINARY);
|
||||
}
|
||||
|
||||
public void setCrFlag(boolean cr) {
|
||||
setCrAction(cr ? CrAction.REMOVE : CrAction.KEEP);
|
||||
}
|
||||
|
||||
public void setRecfm(String r) {
|
||||
if (r == null || r.trim().isEmpty()) {
|
||||
this.recfm = RecordFormat.DEFAULT;
|
||||
} else {
|
||||
String u = r.trim().toUpperCase();
|
||||
if (u.equals("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED;
|
||||
else if (u.equals("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE;
|
||||
else if (u.equals("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED;
|
||||
else this.recfm = RecordFormat.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLrecl(String l) {
|
||||
try { this.lrecl = Integer.parseInt(l.trim()); } catch (NumberFormatException e) { this.lrecl = 0; }
|
||||
}
|
||||
|
||||
public void setBlksize(String b) {
|
||||
try { this.blksize = Integer.parseInt(b.trim()); } catch (NumberFormatException e) { this.blksize = 0; }
|
||||
}
|
||||
|
||||
public void setSpace(String s) {
|
||||
// Space string like "10,5" for primary/secondary
|
||||
if (s == null || s.trim().isEmpty()) return;
|
||||
String[] parts = s.split(",");
|
||||
try {
|
||||
if (parts.length > 0) this.primarySpace = Integer.parseInt(parts[0].trim());
|
||||
if (parts.length > 1) this.secondarySpace = Integer.parseInt(parts[1].trim());
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public void setOptions(String opts) {
|
||||
setOtherOptions(opts);
|
||||
}
|
||||
|
||||
// ========== Validation ==========
|
||||
|
||||
/**
|
||||
* Validate the configuration before starting a transfer.
|
||||
* @return null if valid, or an error message string
|
||||
*/
|
||||
public String validate() {
|
||||
if (hostFilename == null || hostFilename.trim().isEmpty()) {
|
||||
return "Host file name is required";
|
||||
}
|
||||
if (localFilename == null || localFilename.trim().isEmpty()) {
|
||||
return "Local file name is required";
|
||||
}
|
||||
if (hostType == HostType.TSO && isSend() &&
|
||||
units != AllocationUnit.DEFAULT && primarySpace <= 0) {
|
||||
return "Primary space is required when allocation is specified";
|
||||
}
|
||||
if (hostType == HostType.TSO && isSend() &&
|
||||
units == AllocationUnit.AVBLOCK && avblock <= 0) {
|
||||
return "Avblock value is required when allocation is AVBLOCK";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the IND$FILE command string to send to the host.
|
||||
* Ported from x3270's ft_go() in ft.c.
|
||||
*/
|
||||
public String buildCommand() {
|
||||
StringBuilder cmd = new StringBuilder();
|
||||
|
||||
// IND$FILE GET/PUT hostfile
|
||||
cmd.append("IND$FILE ");
|
||||
cmd.append(isReceive() ? "GET " : "PUT ");
|
||||
cmd.append(hostFilename != null ? hostFilename.trim() : "");
|
||||
|
||||
// Collect options into a separate buffer
|
||||
StringBuilder opts = new StringBuilder();
|
||||
|
||||
// Mode
|
||||
if (isAscii()) {
|
||||
opts.append("ASCII");
|
||||
} else if (hostType == HostType.CICS) {
|
||||
opts.append("BINARY");
|
||||
}
|
||||
|
||||
// CR/LF handling
|
||||
if (isAscii() && isCrFlag()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("CRLF");
|
||||
} else if (hostType == HostType.CICS && !isAscii()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("NOCRLF");
|
||||
}
|
||||
|
||||
// Append (send only)
|
||||
if (isAppend() && isSend()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("APPEND");
|
||||
}
|
||||
|
||||
// Host-specific send options
|
||||
if (isSend()) {
|
||||
if (hostType == HostType.TSO) {
|
||||
if (recfm != RecordFormat.DEFAULT) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("RECFM(");
|
||||
switch (recfm) {
|
||||
case FIXED: opts.append("F"); break;
|
||||
case VARIABLE: opts.append("V"); break;
|
||||
case UNDEFINED: opts.append("U"); break;
|
||||
default: break;
|
||||
}
|
||||
opts.append(")");
|
||||
if (lrecl > 0) {
|
||||
opts.append(" LRECL(").append(lrecl).append(")");
|
||||
}
|
||||
if (blksize > 0) {
|
||||
opts.append(" BLKSIZE(").append(blksize).append(")");
|
||||
}
|
||||
}
|
||||
if (units != AllocationUnit.DEFAULT) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("SPACE(").append(primarySpace);
|
||||
if (secondarySpace > 0) {
|
||||
opts.append(",").append(secondarySpace);
|
||||
}
|
||||
opts.append(")");
|
||||
switch (units) {
|
||||
case TRACKS: opts.append(" TRACKS"); break;
|
||||
case CYLINDERS: opts.append(" CYLINDERS"); break;
|
||||
case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
} else if (hostType == HostType.CMS) {
|
||||
if (recfm != RecordFormat.DEFAULT) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("RECFM ");
|
||||
switch (recfm) {
|
||||
case FIXED: opts.append("F"); break;
|
||||
case VARIABLE: opts.append("V"); break;
|
||||
default: break;
|
||||
}
|
||||
if (lrecl > 0) {
|
||||
opts.append(" LRECL ").append(lrecl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional options
|
||||
if (otherOptions != null && !otherOptions.trim().isEmpty()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append(otherOptions.trim());
|
||||
}
|
||||
|
||||
// Apply options with host-specific prefix only if options are present
|
||||
if (opts.length() > 0) {
|
||||
if (hostType != HostType.TSO) {
|
||||
cmd.append(" (").append(opts.toString());
|
||||
} else {
|
||||
cmd.append(" ").append(opts.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return cmd.toString().trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
/**
|
||||
* Constants for IND$FILE file transfer protocol.
|
||||
* Ported from x3270: ft_cut_ds.h, ft_dft_ds.h, ft.c
|
||||
*/
|
||||
public final class FTConstants {
|
||||
|
||||
private FTConstants() {} // utility class
|
||||
|
||||
// ========== CUT Mode Frame Layout ==========
|
||||
|
||||
/** Offset to the CUT structured field at the end of the screen */
|
||||
public static final int O_SF = 1919;
|
||||
|
||||
// Primary area offsets
|
||||
public static final int O_FRAME_TYPE = 0;
|
||||
|
||||
// Control Code frame (host → terminal)
|
||||
public static final int FT_CONTROL_CODE = 0xC3;
|
||||
public static final int O_CC_FRAME_SEQ = 1;
|
||||
public static final int O_CC_STATUS_CODE = 2;
|
||||
public static final int O_CC_MESSAGE = 4;
|
||||
|
||||
// Control Code status codes
|
||||
public static final int SC_HOST_ACK = 0x8181;
|
||||
public static final int SC_XFER_COMPLETE = 0x8189;
|
||||
public static final int SC_ABORT_FILE = 0x8194;
|
||||
public static final int SC_ABORT_XMIT = 0x8198;
|
||||
|
||||
// Data Request frame (host → terminal, for uploads)
|
||||
public static final int FT_DATA_REQUEST = 0xC2;
|
||||
public static final int O_DR_SF = 1;
|
||||
public static final int O_DR_DATA_CODE = 2;
|
||||
public static final int O_DR_FRAME_SEQ = 3;
|
||||
|
||||
// Retransmit frame
|
||||
public static final int FT_RETRANSMIT = 0x4C;
|
||||
|
||||
// Data frame (bidirectional)
|
||||
public static final int FT_DATA = 0xC1;
|
||||
public static final int O_DT_FRAME_SEQ = 1;
|
||||
public static final int O_DT_CSUM = 2;
|
||||
public static final int O_DT_LEN = 3;
|
||||
public static final int O_DT_DATA = 5;
|
||||
|
||||
// Response Area (near end of screen)
|
||||
public static final int O_RESPONSE = O_SF - 5;
|
||||
public static final int RO_FRAME_TYPE = O_RESPONSE + 1;
|
||||
public static final int RO_FRAME_SEQ = O_RESPONSE + 2;
|
||||
public static final int RO_REASON_CODE = O_RESPONSE + 3;
|
||||
|
||||
// Response frame types
|
||||
public static final int RFT_RETRANSMIT = 0x4C;
|
||||
public static final int RFT_CONTROL_CODE = 0xC3;
|
||||
|
||||
// Special EOF data markers
|
||||
public static final int EOF_DATA1 = 0x5C;
|
||||
public static final int EOF_DATA2 = 0xA9;
|
||||
|
||||
// Upload data area offsets
|
||||
public static final int O_UP_DATA_CODE = 2;
|
||||
public static final int O_UP_FRAME_SEQ = 3;
|
||||
public static final int O_UP_CSUM = 4;
|
||||
public static final int O_UP_LEN = 5;
|
||||
public static final int O_UP_DATA = 7;
|
||||
public static final int O_UP_MAX = O_SF - O_UP_DATA;
|
||||
|
||||
// ========== CUT Mode AID codes ==========
|
||||
|
||||
public static final int ACK_OK = 0x7D; // AID_ENTER
|
||||
public static final int ACK_RETRANSMIT = 0xF1; // AID_PF1
|
||||
public static final int ACK_RESYNC_VM = 0x6D; // AID_CLEAR
|
||||
public static final int ACK_RESYNC_TSO = 0x6E; // AID_PA2
|
||||
public static final int ACK_ABORT = 0xF2; // AID_PF2
|
||||
|
||||
// ========== DFT Mode Structured Field Codes ==========
|
||||
|
||||
/** Structured field type for file transfer data */
|
||||
public static final int SF_TRANSFER_DATA = 0xD0;
|
||||
|
||||
// Host requests
|
||||
public static final int TR_OPEN_REQ = 0x0012;
|
||||
public static final int TR_CLOSE_REQ = 0x4112;
|
||||
public static final int TR_SET_CUR_REQ = 0x4511;
|
||||
public static final int TR_GET_REQ = 0x4611;
|
||||
public static final int TR_INSERT_REQ = 0x4711;
|
||||
public static final int TR_DATA_INSERT = 0x4704;
|
||||
|
||||
// PC replies
|
||||
public static final int TR_GET_REPLY = 0x4605;
|
||||
public static final int TR_NORMAL_REPLY = 0x4705;
|
||||
public static final int TR_ERROR_REPLY = 0x08; // low 8 bits
|
||||
public static final int TR_CLOSE_REPLY = 0x4109;
|
||||
|
||||
// Other headers
|
||||
public static final int TR_RECNUM_HDR = 0x6306;
|
||||
public static final int TR_ERROR_HDR = 0x6904;
|
||||
public static final int TR_NOT_COMPRESSED = 0xC080;
|
||||
public static final int TR_BEGIN_DATA = 0x61;
|
||||
|
||||
// Error codes
|
||||
public static final int TR_ERR_EOF = 0x2200;
|
||||
public static final int TR_ERR_CMDFAIL = 0x0100;
|
||||
|
||||
// DFT buffer size limits
|
||||
public static final int DFT_MIN_BUF = 256;
|
||||
public static final int DFT_MAX_BUF = 32768;
|
||||
public static final int DFT_BUF = 4096;
|
||||
|
||||
// AID for structured field response
|
||||
public static final int AID_SF = 0x88;
|
||||
|
||||
// ========== Transfer State ==========
|
||||
|
||||
public enum FTState {
|
||||
NONE, // No transfer in progress
|
||||
AWAIT_ACK, // IND$FILE sent, awaiting acknowledgement
|
||||
RUNNING, // Ack received, data flowing
|
||||
ABORT_WAIT, // Awaiting chance to send an abort
|
||||
ABORT_SENT // Abort sent; awaiting response
|
||||
}
|
||||
|
||||
// ========== Encoding Tables ==========
|
||||
|
||||
/**
|
||||
* Base-64-like encoding table used by CUT mode for lengths and checksums.
|
||||
* 64 characters: a-z, &, -, ., :, +, A-Z, 0-5
|
||||
*/
|
||||
public static final String TABLE6 =
|
||||
"abcdefghijklmnopqrstuvwxyz&-.,:+ABCDEFGHIJKLMNOPQRSTUVWXYZ012345";
|
||||
|
||||
/**
|
||||
* IND$FILE's fixed ASCII-to-EBCDIC translation table (i_asc2ft).
|
||||
* This is NOT the standard CP037 mapping — it's IND$FILE's own table.
|
||||
* Used when remap=true to invert IND$FILE's built-in translation.
|
||||
*/
|
||||
public static final int[] ASC2FT = {
|
||||
0x00,0x01,0x02,0x03,0x37,0x2d,0x2e,0x2f,0x16,0x05,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
|
||||
0x10,0x11,0x12,0x13,0x3c,0x3d,0x32,0x26,0x18,0x19,0x3f,0x27,0x1c,0x1d,0x1e,0x1f,
|
||||
0x40,0x5a,0x7f,0x7b,0x5b,0x6c,0x50,0x7d,0x4d,0x5d,0x5c,0x4e,0x6b,0x60,0x4b,0x61,
|
||||
0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0x7a,0x5e,0x4c,0x7e,0x6e,0x6f,
|
||||
0x7c,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xd1,0xd2,0xd3,0xd4,0xd5,0xd6,
|
||||
0xd7,0xd8,0xd9,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0x4a,0xe0,0x4f,0x5f,0x6d,
|
||||
0x79,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x91,0x92,0x93,0x94,0x95,0x96,
|
||||
0x97,0x98,0x99,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xc0,0x6a,0xd0,0xa1,0x07,
|
||||
0x20,0x21,0x22,0x23,0x24,0x15,0x06,0x17,0x28,0x29,0x2a,0x2b,0x2c,0x09,0x0a,0x1b,
|
||||
0x30,0x31,0x1a,0x33,0x34,0x35,0x36,0x08,0x38,0x39,0x3a,0x3b,0x04,0x14,0x3e,0xe1,
|
||||
0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x51,0x52,0x53,0x54,0x55,0x56,0x57,
|
||||
0x58,0x59,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x70,0x71,0x72,0x73,0x74,0x75,
|
||||
0x76,0x77,0x78,0x80,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,0x90,0x9a,0x9b,0x9c,0x9d,0x9e,
|
||||
0x9f,0xa0,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,
|
||||
0xb8,0xb9,0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xca,0xcb,0xcc,0xcd,0xce,0xcf,0xda,0xdb,
|
||||
0xdc,0xdd,0xde,0xdf,0xea,0xeb,0xec,0xed,0xee,0xef,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
|
||||
};
|
||||
|
||||
/**
|
||||
* IND$FILE's fixed EBCDIC-to-ASCII translation table (i_ft2asc).
|
||||
* The inverse of ASC2FT.
|
||||
*/
|
||||
public static final int[] FT2ASC = {
|
||||
0x00,0x01,0x02,0x03,0x9c,0x09,0x86,0x7f,0x97,0x8d,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
|
||||
0x10,0x11,0x12,0x13,0x9d,0x85,0x08,0x87,0x18,0x19,0x92,0x8f,0x1c,0x1d,0x1e,0x1f,
|
||||
0x80,0x81,0x82,0x83,0x84,0x00,0x17,0x1b,0x88,0x89,0x8a,0x8b,0x8c,0x05,0x06,0x07,
|
||||
0x90,0x91,0x16,0x93,0x94,0x95,0x96,0x04,0x98,0x99,0x9a,0x9b,0x14,0x15,0x9e,0x1a,
|
||||
0x20,0xa0,0xa1,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0x5b,0x2e,0x3c,0x28,0x2b,0x5d,
|
||||
0x26,0xa9,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0x21,0x24,0x2a,0x29,0x3b,0x5e,
|
||||
0x2d,0x2f,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0x7c,0x2c,0x25,0x5f,0x3e,0x3f,
|
||||
0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xc0,0xc1,0xc2,0x60,0x3a,0x23,0x40,0x27,0x3d,0x22,
|
||||
0xc3,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,
|
||||
0xca,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xcb,0xcc,0xcd,0xce,0xcf,0xd0,
|
||||
0xd1,0x7e,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,
|
||||
0xd8,0xd9,0xda,0xdb,0xdc,0xdd,0xde,0xdf,0xe0,0xe1,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,
|
||||
0x7b,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0xe8,0xe9,0xea,0xeb,0xec,0xed,
|
||||
0x7d,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f,0x50,0x51,0x52,0xee,0xef,0xf0,0xf1,0xf2,0xf3,
|
||||
0x5c,0x9f,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,
|
||||
0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
|
||||
};
|
||||
|
||||
// ========== Utility Methods ==========
|
||||
|
||||
/**
|
||||
* Decode a CUT-mode base-64 encoded integer from EBCDIC.
|
||||
* Converts a table6-encoded EBCDIC character to its 6-bit value.
|
||||
*/
|
||||
public static int from6(int ebcdicByte, haus.nightmare.lib3270j.charset.EbcdicTranslator translator) {
|
||||
char ascii = translator.ebcdicToUnicode(ebcdicByte & 0xFF);
|
||||
int idx = TABLE6.indexOf(ascii);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
public static int from6(int ebcdicByte) {
|
||||
// First convert EBCDIC to ASCII via IND$FILE's table
|
||||
int ascii = FT2ASC[ebcdicByte & 0xFF];
|
||||
int idx = TABLE6.indexOf((char) ascii);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a 6-bit value into a table6-encoded EBCDIC character.
|
||||
*/
|
||||
public static int to6(int value, haus.nightmare.lib3270j.charset.EbcdicTranslator translator) {
|
||||
char ascii = TABLE6.charAt(value & 0x3F);
|
||||
int ebc = translator.unicodeToEbcdic(ascii);
|
||||
return ebc >= 0 ? ebc : ASC2FT[ascii & 0xFF];
|
||||
}
|
||||
|
||||
public static int to6(int value) {
|
||||
char ascii = TABLE6.charAt(value & 0x3F);
|
||||
return ASC2FT[ascii & 0xFF];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* CUT (Character Unit Transfer) mode file transfer handler.
|
||||
* Data flows through the screen buffer using a framed protocol.
|
||||
* Ported from x3270's ft_cut.c.
|
||||
*/
|
||||
public class FTCut {
|
||||
|
||||
private static final Logger log = Logger.getLogger(FTCut.class.getName());
|
||||
|
||||
/** Callback for transfer events */
|
||||
public interface FTCutListener {
|
||||
void onCutRunning();
|
||||
void onTransferComplete(String errorMessage);
|
||||
void onTransferAborted(String errorMessage);
|
||||
void onBytesTransferred(long bytes);
|
||||
FTConstants.FTState getCurrentState();
|
||||
void setState(FTConstants.FTState state);
|
||||
FTConfig getConfig();
|
||||
File getLocalFile();
|
||||
}
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private final InputProcessor input;
|
||||
private final EbcdicTranslator translator;
|
||||
private final FTCutListener listener;
|
||||
|
||||
// CUT mode state
|
||||
private boolean xferInProgress = false;
|
||||
private long expandedLength = 0;
|
||||
private int quadrant = -1;
|
||||
private boolean cutEof = false;
|
||||
|
||||
// Upload translation buffer
|
||||
private static final int XLATE_NBUF = 32;
|
||||
private int xlateBuffered = 0;
|
||||
private int xlateBufIx = 0;
|
||||
private final int[] xlateBuf = new int[XLATE_NBUF];
|
||||
|
||||
private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?";
|
||||
|
||||
private static final int SELECTOR_0 = 0x5E; // ';' (EBCDIC)
|
||||
private static final int SELECTOR_1 = 0x7E; // '=' (EBCDIC)
|
||||
private static final int SELECTOR_2 = 0x5C; // '*' (EBCDIC)
|
||||
private static final int SELECTOR_3 = 0x7D; // '\'' (EBCDIC)
|
||||
|
||||
private static final int[] XLATE_0 = {
|
||||
0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,
|
||||
0xd3,0xd4,0xd5,0xd6, 0xd7,0xd8,0xd9,0xe2, 0xe3,0xe4,0xe5,0xe6,
|
||||
0xe7,0xe8,0xe9,0x81, 0x82,0x83,0x84,0x85, 0x86,0x87,0x88,0x89,
|
||||
0x91,0x92,0x93,0x94, 0x95,0x96,0x97,0x98, 0x99,0xa2,0xa3,0xa4,
|
||||
0xa5,0xa6,0xa7,0xa8, 0xa9,0xf0,0xf1,0xf2, 0xf3,0xf4,0xf5,0xf6,
|
||||
0xf7,0xf8,0xf9,0x6c, 0x50,0x6d,0x4d,0x5d, 0x4c,0x4e,0x6b,0x60,
|
||||
0x4b,0x61,0x7a,0x6e, 0x6f
|
||||
};
|
||||
|
||||
private static final int[] XLATE_1 = {
|
||||
0x20,0x41,0x42,0x43, 0x44,0x45,0x46,0x47, 0x48,0x49,0x4a,0x4b,
|
||||
0x4c,0x4d,0x4e,0x4f, 0x50,0x51,0x52,0x53, 0x54,0x55,0x56,0x57,
|
||||
0x58,0x59,0x5a,0x61, 0x62,0x63,0x64,0x65, 0x66,0x67,0x68,0x69,
|
||||
0x6a,0x6b,0x6c,0x6d, 0x6e,0x6f,0x70,0x71, 0x72,0x73,0x74,0x75,
|
||||
0x76,0x77,0x78,0x79, 0x7a,0x30,0x31,0x32, 0x33,0x34,0x35,0x36,
|
||||
0x37,0x38,0x39,0x25, 0x26,0x27,0x28,0x29, 0x2a,0x2b,0x2c,0x2d,
|
||||
0x2e,0x2f,0x3a,0x3b, 0x3f
|
||||
};
|
||||
|
||||
private static final int[] XLATE_2 = {
|
||||
0x00,0x00,0x01,0x02, 0x03,0x04,0x05,0x06, 0x07,0x08,0x09,0x0a,
|
||||
0x0b,0x0c,0x0d,0x0e, 0x0f,0x10,0x11,0x12, 0x13,0x14,0x15,0x16,
|
||||
0x17,0x18,0x19,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00, 0x00,0x3c,0x3d,0x3e, 0x00,0xfa,0xfb,0xfc,
|
||||
0xfd,0xfe,0xff,0x7b, 0x7c,0x7d,0x7e,0x7f, 0x1a,0x1b,0x1c,0x1d,
|
||||
0x1e,0x1f,0x00,0x00, 0x00
|
||||
};
|
||||
|
||||
private static final int[] XLATE_3 = {
|
||||
0x00,0xa0,0xa1,0xea, 0xeb,0xec,0xed,0xee, 0xef,0xe0,0xe1,0xaa,
|
||||
0xab,0xac,0xad,0xae, 0xaf,0xb0,0xb1,0xb2, 0xb3,0xb4,0xb5,0xb6,
|
||||
0xb7,0xb8,0xb9,0x80, 0x00,0xca,0xcb,0xcc, 0xcd,0xce,0xcf,0xc0,
|
||||
0x00,0x8a,0x8b,0x8c, 0x8d,0x8e,0x8f,0x90, 0x00,0xda,0xdb,0xdc,
|
||||
0xdd,0xde,0xdf,0xd0, 0x00,0x00,0x21,0x22, 0x23,0x24,0x5b,0x5c,
|
||||
0x00,0x5e,0x5f,0x00, 0x9c,0x9d,0x9e,0x9f, 0xba,0xbb,0xbc,0xbd,
|
||||
0xbe,0xbf,0x9a,0x9b, 0x00
|
||||
};
|
||||
|
||||
private static final int[][] QUADS = { XLATE_0, XLATE_1, XLATE_2, XLATE_3 };
|
||||
private static final int[] SELECTORS = { SELECTOR_0, SELECTOR_1, SELECTOR_2, SELECTOR_3 };
|
||||
|
||||
// File I/O
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private boolean lastCr = false;
|
||||
|
||||
public FTCut(ScreenBuffer screen, InputProcessor input,
|
||||
EbcdicTranslator translator, FTCutListener listener) {
|
||||
this.screen = screen;
|
||||
this.input = input;
|
||||
this.translator = translator;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize CUT mode with active streams.
|
||||
*/
|
||||
public void initTransfer(InputStream in, OutputStream out) {
|
||||
this.inputStream = in;
|
||||
this.outputStream = out;
|
||||
resetState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize CUT mode for a new transfer.
|
||||
*/
|
||||
public void initTransfer(File localFile) throws IOException {
|
||||
FTConfig config = listener.getConfig();
|
||||
resetState();
|
||||
|
||||
if (config.isReceive()) {
|
||||
boolean append = config.isAppend();
|
||||
outputStream = new FileOutputStream(localFile, append);
|
||||
inputStream = null;
|
||||
} else {
|
||||
inputStream = new FileInputStream(localFile);
|
||||
outputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
xferInProgress = false;
|
||||
expandedLength = 0;
|
||||
quadrant = -1;
|
||||
xlateBuffered = 0;
|
||||
xlateBufIx = 0;
|
||||
cutEof = false;
|
||||
lastCr = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up CUT mode resources.
|
||||
*/
|
||||
public void cleanup() {
|
||||
xferInProgress = false;
|
||||
try {
|
||||
if (inputStream != null) { inputStream.close(); inputStream = null; }
|
||||
if (outputStream != null) { outputStream.close(); outputStream = null; }
|
||||
} catch (IOException e) {
|
||||
log.warning("Error closing file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a CUT-mode screen update.
|
||||
* Called by the transfer coordinator when the screen changes during a transfer.
|
||||
*/
|
||||
public void processScreenUpdate() {
|
||||
if (listener.getCurrentState() == FTState.NONE) return;
|
||||
|
||||
// CUT frames MUST have a skip field attribute at O_SF (1919)
|
||||
byte sfAttr = screen.getCellFAByte(O_SF);
|
||||
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
||||
// Not a CUT frame (likely local echo, menu return, or intermediate screen)
|
||||
if (xferInProgress) {
|
||||
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
||||
xferInProgress = false;
|
||||
listener.onTransferAborted("Host aborted transfer (returned to menu)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int frameType = screen.getCellEC(O_FRAME_TYPE);
|
||||
|
||||
switch (frameType) {
|
||||
case FT_CONTROL_CODE:
|
||||
cutControlCode();
|
||||
break;
|
||||
case FT_DATA_REQUEST:
|
||||
cutDataRequest();
|
||||
break;
|
||||
case FT_RETRANSMIT:
|
||||
cutRetransmit();
|
||||
break;
|
||||
case FT_DATA:
|
||||
cutData();
|
||||
break;
|
||||
default:
|
||||
log.fine("Ignoring non-CUT frame type 0x" + Integer.toHexString(frameType));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSkip(byte attr) {
|
||||
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
|
||||
}
|
||||
|
||||
// ========== Control Code Processing ==========
|
||||
|
||||
private void cutControlCode() {
|
||||
int code = (screen.getCellEC(O_CC_STATUS_CODE) << 8) |
|
||||
screen.getCellEC(O_CC_STATUS_CODE + 1);
|
||||
log.fine("CUT: CONTROL_CODE 0x" + Integer.toHexString(code));
|
||||
|
||||
switch (code) {
|
||||
case SC_HOST_ACK:
|
||||
log.info("CUT: HOST_ACK received — transfer running");
|
||||
xferInProgress = true;
|
||||
expandedLength = 0;
|
||||
quadrant = -1;
|
||||
xlateBuffered = 0;
|
||||
xlateBufIx = 0;
|
||||
cutEof = false;
|
||||
cutAck();
|
||||
listener.onCutRunning();
|
||||
break;
|
||||
|
||||
case SC_XFER_COMPLETE:
|
||||
log.info("CUT: Transfer complete");
|
||||
cutAck();
|
||||
xferInProgress = false;
|
||||
listener.onTransferComplete(null);
|
||||
break;
|
||||
|
||||
case SC_ABORT_FILE:
|
||||
case SC_ABORT_XMIT:
|
||||
log.warning("CUT: ABORT received");
|
||||
xferInProgress = false;
|
||||
cutAck();
|
||||
|
||||
String msg = extractHostMessage();
|
||||
listener.onTransferAborted(msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
log.warning("CUT: Unknown control code 0x" + Integer.toHexString(code));
|
||||
cutAbort("Unknown CUT control code", SC_ABORT_XMIT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the error message from the host's control code frame.
|
||||
* The message starts at O_CC_MESSAGE and is up to 80 EBCDIC characters.
|
||||
*/
|
||||
private String extractHostMessage() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 80; i++) {
|
||||
int ebc = screen.getCellEC(O_CC_MESSAGE + i);
|
||||
if (ebc == 0) continue;
|
||||
char ch = translator.ebcdicToUnicode(ebc);
|
||||
sb.append(ch);
|
||||
}
|
||||
String msg = sb.toString().stripTrailing();
|
||||
if (msg.endsWith("$")) {
|
||||
msg = msg.substring(0, msg.length() - 1).stripTrailing();
|
||||
}
|
||||
return msg.isEmpty() ? "Host cancelled transfer" : msg;
|
||||
}
|
||||
|
||||
// ========== Data Request (Upload) ==========
|
||||
|
||||
private void cutDataRequest() {
|
||||
int seqEbc = screen.getCellEC(O_DR_FRAME_SEQ);
|
||||
int seq = FTConstants.from6(seqEbc, translator);
|
||||
log.fine("CUT: DATA_REQUEST seq=" + seq);
|
||||
|
||||
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
|
||||
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
FTConfig config = listener.getConfig();
|
||||
|
||||
int count = 0;
|
||||
try {
|
||||
while (count < O_UP_MAX && !cutEof) {
|
||||
int c = xlateGetc(config);
|
||||
if (c == -1) {
|
||||
cutEof = true;
|
||||
break;
|
||||
}
|
||||
screen.setCell(O_UP_DATA + count, c);
|
||||
count++;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warning("CUT: Read error: " + e.getMessage());
|
||||
cutAbort("Read error: " + e.getMessage(), SC_ABORT_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == 0 && cutEof) {
|
||||
screen.setCell(O_UP_DATA, EOF_DATA1);
|
||||
screen.setCell(O_UP_DATA + 1, EOF_DATA2);
|
||||
count = 2;
|
||||
}
|
||||
|
||||
screen.setCell(O_UP_FRAME_SEQ, seqEbc);
|
||||
|
||||
int cs = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
cs ^= screen.getCellEC(O_UP_DATA + i);
|
||||
}
|
||||
screen.setCell(O_UP_CSUM, FTConstants.to6(cs & 0x3F, translator));
|
||||
screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator));
|
||||
screen.setCell(O_UP_LEN + 1, FTConstants.to6(count & 0x3F, translator));
|
||||
|
||||
// Hide data field by setting zero intensity on field attribute
|
||||
byte attr = screen.getCellFAByte(O_DR_SF);
|
||||
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL | FA_MODIFY);
|
||||
screen.setCellFA(O_DR_SF, attr);
|
||||
|
||||
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
|
||||
expandedLength += count;
|
||||
listener.onBytesTransferred(expandedLength);
|
||||
input.sendAidForFT(AID_ENTER);
|
||||
}
|
||||
|
||||
// ========== Data (Download) ==========
|
||||
|
||||
private void cutData() {
|
||||
log.fine("CUT: DATA");
|
||||
|
||||
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
|
||||
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
FTConfig config = listener.getConfig();
|
||||
|
||||
int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) |
|
||||
FTConstants.from6(screen.getCellEC(O_DT_LEN + 1), translator);
|
||||
|
||||
if (rawLength > O_RESPONSE - O_DT_DATA) {
|
||||
cutAbort("Oversized CUT data frame", SC_ABORT_XMIT);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] rawData = new byte[rawLength];
|
||||
for (int i = 0; i < rawLength; i++) {
|
||||
rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i);
|
||||
}
|
||||
|
||||
if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 &&
|
||||
(rawData[1] & 0xFF) == EOF_DATA2) {
|
||||
log.fine("CUT: EOF marker received");
|
||||
cutAck();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] converted = convertDownload(rawData, rawLength, config);
|
||||
if (outputStream != null) {
|
||||
outputStream.write(converted);
|
||||
expandedLength += converted.length;
|
||||
listener.onBytesTransferred(expandedLength);
|
||||
}
|
||||
cutAck();
|
||||
} catch (IOException e) {
|
||||
log.warning("CUT: Write error: " + e.getMessage());
|
||||
cutAbort("Write error: " + e.getMessage(), SC_ABORT_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Retransmit ==========
|
||||
|
||||
private void cutRetransmit() {
|
||||
log.warning("CUT: RETRANSMIT (not supported, aborting)");
|
||||
cutAbort("Retransmit not supported", SC_ABORT_XMIT);
|
||||
}
|
||||
|
||||
// ========== Acknowledge ==========
|
||||
|
||||
private void cutAck() {
|
||||
log.fine("CUT: > ACK (Enter)");
|
||||
input.sendAidForFT(AID_ENTER);
|
||||
}
|
||||
|
||||
// ========== Abort ==========
|
||||
|
||||
private void cutAbort(String message, int reason) {
|
||||
log.warning("CUT: ABORT — " + message);
|
||||
|
||||
screen.setCell(RO_FRAME_TYPE, RFT_CONTROL_CODE);
|
||||
screen.setCell(RO_FRAME_SEQ, screen.getCellEC(O_DT_FRAME_SEQ));
|
||||
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
|
||||
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
|
||||
|
||||
input.sendAidForFT(AID_PF2);
|
||||
listener.onTransferAborted(message);
|
||||
}
|
||||
|
||||
// ========== Character Translation ==========
|
||||
|
||||
/**
|
||||
* Convert received CUT EBCDIC data to local format.
|
||||
* Matching x3270 upload_convert logic.
|
||||
*/
|
||||
private byte[] convertDownload(byte[] rawData, int length, FTConfig config)
|
||||
throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(length * 2);
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int c = rawData[i] & 0xFF;
|
||||
|
||||
while (true) {
|
||||
if (quadrant < 0) {
|
||||
for (quadrant = 0; quadrant < 4; quadrant++) {
|
||||
if (c == SELECTORS[quadrant]) break;
|
||||
}
|
||||
if (quadrant >= 4) {
|
||||
throw new IOException("CUT conversion error (quadrant selector not found)");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (c < 0x40 || c > 0xF9) {
|
||||
throw new IOException("CUT conversion error (data out of bounds)");
|
||||
}
|
||||
|
||||
char asciiChar = (char) FT2ASC[c & 0xFF];
|
||||
int ix = ALPHAS.indexOf(asciiChar);
|
||||
if (ix < 0) {
|
||||
quadrant = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(quadrant == 2 && c == 0xC1) && QUADS[quadrant][ix] == 0) {
|
||||
quadrant = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int decoded = QUADS[quadrant][ix];
|
||||
|
||||
if (config.isAscii() && config.isCrFlag() && (decoded == 0x0D || decoded == 0x1A)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!config.isAscii() || !config.isRemapFlag()) {
|
||||
out.write(decoded);
|
||||
break;
|
||||
}
|
||||
|
||||
if (decoded < 0x20 || (decoded >= 0x80 && decoded < 0xA0 && decoded != 0x9F)) {
|
||||
out.write(String.valueOf((char) decoded).getBytes(StandardCharsets.UTF_8));
|
||||
} else if (decoded == 0xFF) {
|
||||
out.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
int ebc = ASC2FT[decoded & 0xFF];
|
||||
char unicodeChar = translator.ebcdicToUnicode(ebc);
|
||||
out.write(String.valueOf(unicodeChar).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private int xlateGetc(FTConfig config) throws IOException {
|
||||
if (xlateBuffered > 0) {
|
||||
int r = xlateBuf[xlateBufIx++];
|
||||
xlateBuffered--;
|
||||
return r;
|
||||
}
|
||||
|
||||
if (inputStream == null) return -1;
|
||||
|
||||
int c = inputStream.read();
|
||||
if (c == -1) return -1;
|
||||
|
||||
int localByte = c & 0xFF;
|
||||
int nc = 0;
|
||||
int[] cbuf = new int[4];
|
||||
|
||||
if (config.isAscii()) {
|
||||
if (config.isCrFlag() && !lastCr && localByte == '\n') {
|
||||
nc += uploadConvert('\r', cbuf, nc, config);
|
||||
}
|
||||
lastCr = (localByte == '\r');
|
||||
}
|
||||
|
||||
nc += uploadConvert(localByte, cbuf, nc, config);
|
||||
|
||||
if (nc > 1) {
|
||||
for (int i = 1; i < nc; i++) {
|
||||
xlateBuf[xlateBuffered++] = cbuf[i];
|
||||
}
|
||||
xlateBufIx = 0;
|
||||
}
|
||||
return cbuf[0];
|
||||
}
|
||||
|
||||
private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) {
|
||||
int ebc;
|
||||
if (localByte == 0) {
|
||||
if (quadrant != 2) {
|
||||
quadrant = 2; // OTHER_2
|
||||
cbuf[offset] = SELECTORS[quadrant];
|
||||
cbuf[offset + 1] = 0xC1; // XLATE_NULL
|
||||
return 2;
|
||||
} else {
|
||||
cbuf[offset] = 0xC1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.isAscii() || !config.isRemapFlag()) {
|
||||
ebc = localByte & 0xFF;
|
||||
} else {
|
||||
int standardEbc = translator.unicodeToEbcdic((char) localByte);
|
||||
if (standardEbc < 0) {
|
||||
standardEbc = 0x40;
|
||||
}
|
||||
ebc = FTConstants.FT2ASC[standardEbc & 0xFF];
|
||||
}
|
||||
|
||||
return storeUpload(ebc, cbuf, offset);
|
||||
}
|
||||
|
||||
private int storeUpload(int pseudoAsciiByte, int[] obBuf, int offset) {
|
||||
if (quadrant >= 0) {
|
||||
for (int i = 0; i < 77; i++) {
|
||||
if (QUADS[quadrant][i] == pseudoAsciiByte) {
|
||||
char ch = ALPHAS.charAt(i);
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
obBuf[offset] = ebc >= 0 ? ebc : 0x40;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
int oq = quadrant;
|
||||
for (quadrant = 0; quadrant < 4; quadrant++) {
|
||||
if (quadrant == oq) continue;
|
||||
for (int i = 0; i < 77; i++) {
|
||||
if (QUADS[quadrant][i] == pseudoAsciiByte) {
|
||||
char ch = ALPHAS.charAt(i);
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
obBuf[offset] = SELECTORS[quadrant];
|
||||
obBuf[offset + 1] = ebc >= 0 ? ebc : 0x40;
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
quadrant = -1;
|
||||
int questionEbc = translator.unicodeToEbcdic('?');
|
||||
obBuf[offset] = questionEbc >= 0 ? questionEbc : 0x6F;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* DFT (Distributed Function Terminal) mode file transfer handler.
|
||||
* Uses Structured Fields (SF_TRANSFER_DATA = 0xD0) for data exchange.
|
||||
* Offers better performance than CUT mode with configurable buffer sizes.
|
||||
* Ported from x3270's ft_dft.c.
|
||||
*/
|
||||
public class FTDft {
|
||||
|
||||
private static final Logger log = Logger.getLogger(FTDft.class.getName());
|
||||
|
||||
private static final String OPEN_MSG = "FT:MSG";
|
||||
private static final String END_TRANSFER = "TRANS03";
|
||||
|
||||
/** Callback for transfer events (shared interface with CUT) */
|
||||
public interface FTDftListener {
|
||||
void onDftRunning();
|
||||
void onTransferComplete(String errorMessage);
|
||||
void onTransferAborted(String errorMessage);
|
||||
void onBytesTransferred(long bytes);
|
||||
FTConstants.FTState getCurrentState();
|
||||
void setState(FTConstants.FTState state);
|
||||
FTConfig getConfig();
|
||||
File getLocalFile();
|
||||
}
|
||||
|
||||
private final InputProcessor input;
|
||||
private final EbcdicTranslator translator;
|
||||
private final FTDftListener listener;
|
||||
|
||||
// DFT state
|
||||
private long recnum = 1;
|
||||
private boolean dftEof = false;
|
||||
private boolean messageFlag = false;
|
||||
private long bytesTransferred = 0;
|
||||
|
||||
// Savebuf for Read Modified retransmit
|
||||
private byte[] dftSaveBuf = null;
|
||||
private int dftSaveBufLen = 0;
|
||||
|
||||
// File I/O
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private boolean lastCr = false;
|
||||
|
||||
public FTDft(InputProcessor input, EbcdicTranslator translator,
|
||||
FTDftListener listener) {
|
||||
this.input = input;
|
||||
this.translator = translator;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DFT mode with active streams.
|
||||
*/
|
||||
public void initTransfer(InputStream in, OutputStream out) {
|
||||
this.inputStream = in;
|
||||
this.outputStream = out;
|
||||
resetState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DFT mode for a new transfer from file.
|
||||
*/
|
||||
public void initTransfer(File localFile) throws IOException {
|
||||
FTConfig config = listener.getConfig();
|
||||
resetState();
|
||||
|
||||
if (config.isReceive()) {
|
||||
outputStream = new FileOutputStream(localFile, config.isAppend());
|
||||
inputStream = null;
|
||||
} else {
|
||||
inputStream = new FileInputStream(localFile);
|
||||
outputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
recnum = 1;
|
||||
dftEof = false;
|
||||
messageFlag = false;
|
||||
bytesTransferred = 0;
|
||||
dftSaveBuf = null;
|
||||
dftSaveBufLen = 0;
|
||||
lastCr = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up DFT mode resources.
|
||||
*/
|
||||
public void cleanup() {
|
||||
try {
|
||||
if (inputStream != null) { inputStream.close(); inputStream = null; }
|
||||
if (outputStream != null) { outputStream.close(); outputStream = null; }
|
||||
} catch (IOException e) {
|
||||
log.warning("Error closing file: " + e.getMessage());
|
||||
}
|
||||
dftSaveBuf = null;
|
||||
dftSaveBufLen = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a DFT structured field from the host.
|
||||
* Called by DataStreamProcessor when SF type is SF_TRANSFER_DATA (0xD0).
|
||||
*
|
||||
* @param data raw structured field data
|
||||
* @param offset start of the SF (after length + type byte)
|
||||
* @param length total SF length
|
||||
*/
|
||||
public void processStructuredField(byte[] data, int offset, int length) {
|
||||
// The SF payload starts after the 2-byte length + 1-byte SF type
|
||||
int payloadStart = offset + 3;
|
||||
if (payloadStart + 2 > offset + length) {
|
||||
log.warning("DFT: SF too short");
|
||||
return;
|
||||
}
|
||||
|
||||
// First 2 bytes of payload are the DFT request code
|
||||
int requestCode = ((data[payloadStart] & 0xFF) << 8) |
|
||||
(data[payloadStart + 1] & 0xFF);
|
||||
|
||||
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
|
||||
|
||||
switch (requestCode) {
|
||||
case TR_OPEN_REQ:
|
||||
dftOpenRequest(data, offset, length);
|
||||
break;
|
||||
case TR_INSERT_REQ:
|
||||
dftInsertRequest(data, payloadStart, offset + length - payloadStart);
|
||||
break;
|
||||
case TR_DATA_INSERT:
|
||||
dftDataInsert(data, payloadStart, offset + length - payloadStart);
|
||||
break;
|
||||
case TR_SET_CUR_REQ:
|
||||
// No-op, same as x3270
|
||||
log.fine("DFT: SetCursor (ignored)");
|
||||
break;
|
||||
case TR_GET_REQ:
|
||||
dftGetRequest();
|
||||
break;
|
||||
case TR_CLOSE_REQ:
|
||||
dftCloseRequest();
|
||||
break;
|
||||
default:
|
||||
log.warning("DFT: Unknown request code 0x" + Integer.toHexString(requestCode));
|
||||
dftAbort("Unknown DFT request", TR_DATA_INSERT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Open Request ==========
|
||||
|
||||
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
||||
log.fine("DFT: Open request");
|
||||
|
||||
// Parse open request payload matching x3270
|
||||
// sfLength is the 2-byte length value at sfOffset
|
||||
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
|
||||
String nameBuf = "";
|
||||
|
||||
if (sfLenVal == 0x23 && sfOffset + 3 + 25 <= data.length) {
|
||||
nameBuf = extractName(data, sfOffset + 3 + 25, 7);
|
||||
} else if (sfLenVal == 0x29 && sfOffset + 3 + 31 <= data.length) {
|
||||
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
||||
}
|
||||
|
||||
if (OPEN_MSG.equalsIgnoreCase(nameBuf)) {
|
||||
messageFlag = true;
|
||||
log.info("DFT: Open request for message stream");
|
||||
} else {
|
||||
messageFlag = false;
|
||||
listener.onDftRunning();
|
||||
}
|
||||
|
||||
dftEof = false;
|
||||
recnum = 1;
|
||||
|
||||
// Acknowledge Open matching x3270 (SF_TRANSFER_DATA + 0x0009)
|
||||
dftOpenAck();
|
||||
}
|
||||
|
||||
private String extractName(byte[] data, int start, int maxLen) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < maxLen && (start + i) < data.length; i++) {
|
||||
int b = data[start + i] & 0xFF;
|
||||
if (b == 0) break;
|
||||
char ch = translator.ebcdicToUnicode(b);
|
||||
sb.append(ch);
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private void dftOpenAck() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(5); // SF length
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write(0); out.write(9); // OpenAck response code 0x0009
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
}
|
||||
|
||||
// ========== Insert Request (host sending data for download) ==========
|
||||
|
||||
private void dftInsertRequest(byte[] data, int offset, int length) {
|
||||
log.fine("DFT: Insert");
|
||||
dftDataInsert(data, offset, length);
|
||||
}
|
||||
|
||||
private void dftDataInsert(byte[] data, int offset, int length) {
|
||||
FTConfig config = listener.getConfig();
|
||||
|
||||
if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) {
|
||||
dftAbort("Transfer cancelled by user", TR_DATA_INSERT);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip the 2-byte request code
|
||||
int pos = offset + 2;
|
||||
int end = offset + length;
|
||||
|
||||
// Look for TR_BEGIN_DATA marker
|
||||
while (pos < end) {
|
||||
int headerCode = data[pos] & 0xFF;
|
||||
|
||||
if (headerCode == TR_BEGIN_DATA) {
|
||||
if (pos + 3 > end) break;
|
||||
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
||||
int actualDataLen = dataLen - 3;
|
||||
pos += 3;
|
||||
|
||||
if (actualDataLen > 0 && pos + actualDataLen <= end) {
|
||||
if (messageFlag) {
|
||||
// Handle message payload from host
|
||||
dftDataAck();
|
||||
handleHostMessage(data, pos, actualDataLen);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
writeDownloadData(data, pos, actualDataLen, config);
|
||||
bytesTransferred += actualDataLen;
|
||||
listener.onBytesTransferred(bytesTransferred);
|
||||
} catch (IOException e) {
|
||||
dftAbort("Write error: " + e.getMessage(), TR_DATA_INSERT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pos += Math.max(0, actualDataLen);
|
||||
} else if (pos + 1 < end) {
|
||||
int hdrCode16 = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
|
||||
if (hdrCode16 == TR_RECNUM_HDR) {
|
||||
pos += 6;
|
||||
} else if (hdrCode16 == TR_NOT_COMPRESSED) {
|
||||
pos += 2;
|
||||
} else {
|
||||
pos += 2;
|
||||
}
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
// Send acknowledgement for file data
|
||||
dftDataAck();
|
||||
}
|
||||
|
||||
private void handleHostMessage(byte[] data, int offset, int length) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
int b = data[offset + i] & 0xFF;
|
||||
if (b == 0 || b == '$') break;
|
||||
char ch = translator.ebcdicToUnicode(b);
|
||||
sb.append(ch);
|
||||
}
|
||||
String msg = sb.toString().trim();
|
||||
log.info("DFT message: " + msg);
|
||||
|
||||
String msgLower = msg.toLowerCase();
|
||||
if (msg.startsWith(END_TRANSFER) || msgLower.contains("complete") || msgLower.contains("transferred") || msgLower.contains("success")) {
|
||||
listener.onTransferComplete(null);
|
||||
} else if (listener.getCurrentState() == FTState.ABORT_SENT || msgLower.contains("error") || msgLower.contains("failed") || msgLower.contains("abort")) {
|
||||
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
||||
} else {
|
||||
// Informational message (default success)
|
||||
listener.onTransferComplete(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write download data to the local file, handling ASCII conversion.
|
||||
* Matching x3270 upload_convert logic.
|
||||
*/
|
||||
private void writeDownloadData(byte[] data, int offset, int length,
|
||||
FTConfig config) throws IOException {
|
||||
if (outputStream == null) return;
|
||||
|
||||
if (!config.isAscii()) {
|
||||
outputStream.write(data, offset, length);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int b = data[offset + i] & 0xFF;
|
||||
|
||||
if (config.isCrFlag() && (b == '\r' || b == 0x1A)) {
|
||||
continue; // Strip CR and EOF ^Z
|
||||
}
|
||||
|
||||
if (!config.isRemapFlag()) {
|
||||
outputStream.write(b);
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* ASCII mode with remap:
|
||||
* Host IND$FILE sends pseudo-ASCII byte b.
|
||||
* Map pseudo-ASCII b to EBCDIC byte via ASC2FT[b],
|
||||
* then convert EBCDIC byte to Unicode UTF-8 character.
|
||||
*/
|
||||
if (b < 0x20 || (b >= 0x80 && b < 0xA0 && b != 0x9F)) {
|
||||
// Control code — write as Unicode directly
|
||||
outputStream.write(String.valueOf((char) b).getBytes(StandardCharsets.UTF_8));
|
||||
} else if (b == 0xFF) {
|
||||
// Special 0xFF -> U+009F
|
||||
outputStream.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
int ebc = ASC2FT[b & 0xFF];
|
||||
char ch = translator.ebcdicToUnicode(ebc);
|
||||
outputStream.write(String.valueOf(ch).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Get Request (host wants data for upload) ==========
|
||||
|
||||
private void dftGetRequest() {
|
||||
FTConfig config = listener.getConfig();
|
||||
log.fine("DFT: Get");
|
||||
|
||||
if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) {
|
||||
dftAbort("Transfer cancelled by user", TR_GET_REQ);
|
||||
return;
|
||||
}
|
||||
|
||||
int bufferSize = config.getDftBufferSize();
|
||||
int numbytes = bufferSize - 27;
|
||||
byte[] readBuf = new byte[numbytes];
|
||||
int totalRead = 0;
|
||||
|
||||
try {
|
||||
while (!dftEof && totalRead < numbytes) {
|
||||
if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) {
|
||||
int b = dftAsciiRead(config);
|
||||
if (b == -1) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
readBuf[totalRead++] = (byte) b;
|
||||
} else {
|
||||
if (inputStream == null) { dftEof = true; break; }
|
||||
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
dftAbort("Read error: " + e.getMessage(), TR_GET_REQ);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
|
||||
out.write(AID_SF);
|
||||
|
||||
int sfLenPos = out.size();
|
||||
out.write(0); out.write(0);
|
||||
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
|
||||
if (totalRead > 0) {
|
||||
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
|
||||
|
||||
out.write((TR_GET_REPLY >> 8) & 0xFF);
|
||||
out.write(TR_GET_REPLY & 0xFF);
|
||||
|
||||
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
|
||||
out.write(TR_RECNUM_HDR & 0xFF);
|
||||
out.write((int) ((recnum >> 24) & 0xFF));
|
||||
out.write((int) ((recnum >> 16) & 0xFF));
|
||||
out.write((int) ((recnum >> 8) & 0xFF));
|
||||
out.write((int) (recnum & 0xFF));
|
||||
recnum++;
|
||||
|
||||
out.write((TR_NOT_COMPRESSED >> 8) & 0xFF);
|
||||
out.write(TR_NOT_COMPRESSED & 0xFF);
|
||||
|
||||
out.write(TR_BEGIN_DATA);
|
||||
int dataFieldLen = totalRead + 5;
|
||||
out.write((dataFieldLen >> 8) & 0xFF);
|
||||
out.write(dataFieldLen & 0xFF);
|
||||
|
||||
out.write(readBuf, 0, totalRead);
|
||||
|
||||
bytesTransferred += totalRead;
|
||||
} else {
|
||||
log.fine("DFT: > GetReply EOF");
|
||||
|
||||
out.write((TR_GET_REQ >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_REPLY & 0xFF);
|
||||
|
||||
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_HDR & 0xFF);
|
||||
|
||||
out.write((TR_ERR_EOF >> 8) & 0xFF);
|
||||
out.write(TR_ERR_EOF & 0xFF);
|
||||
|
||||
dftEof = true;
|
||||
}
|
||||
|
||||
byte[] result = out.toByteArray();
|
||||
int sfLen = result.length - 1;
|
||||
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
|
||||
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
|
||||
|
||||
dftSaveBuf = result.clone();
|
||||
dftSaveBufLen = result.length;
|
||||
|
||||
input.sendStructuredFieldData(result);
|
||||
listener.onBytesTransferred(bytesTransferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a byte from local file for upload, handling ASCII conversion and remapping.
|
||||
* Matching x3270 dft_ascii_read logic.
|
||||
*/
|
||||
private int dftAsciiRead(FTConfig config) throws IOException {
|
||||
if (inputStream == null) return -1;
|
||||
|
||||
int c = inputStream.read();
|
||||
if (c == -1) return -1;
|
||||
|
||||
if (config.isCrFlag() && !lastCr && c == '\n') {
|
||||
lastCr = false;
|
||||
// Expand \n to \r\n: return \r byte now
|
||||
int rEbc = translator.unicodeToEbcdic('\r');
|
||||
if (rEbc < 0) rEbc = 0x0D;
|
||||
return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc;
|
||||
}
|
||||
lastCr = (c == '\r');
|
||||
|
||||
if (!config.isRemapFlag()) {
|
||||
int ebc = translator.unicodeToEbcdic((char) c);
|
||||
return ebc >= 0 ? ebc : 0x40;
|
||||
}
|
||||
|
||||
/*
|
||||
* ASCII mode with remap:
|
||||
* Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc].
|
||||
*/
|
||||
int ebc;
|
||||
if (c < 0x20 || (c >= 0x80 && c < 0x9F)) {
|
||||
ebc = ASC2FT[c & 0xFF];
|
||||
} else if (c == 0x9F) {
|
||||
ebc = 0xFF;
|
||||
} else {
|
||||
ebc = translator.unicodeToEbcdic((char) c);
|
||||
}
|
||||
if (ebc < 0) ebc = 0x40;
|
||||
|
||||
return FT2ASC[ebc & 0xFF];
|
||||
}
|
||||
|
||||
// ========== Close Request ==========
|
||||
|
||||
private void dftCloseRequest() {
|
||||
log.fine("DFT: Close");
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(5);
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
|
||||
out.write(TR_CLOSE_REPLY & 0xFF);
|
||||
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
|
||||
if (!messageFlag) {
|
||||
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
||||
listener.onTransferComplete(null);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Data Acknowledgement ==========
|
||||
|
||||
private void dftDataAck() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(5);
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
|
||||
out.write(TR_NORMAL_REPLY & 0xFF);
|
||||
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
}
|
||||
|
||||
// ========== Abort ==========
|
||||
|
||||
private void dftAbort(String message, int code) {
|
||||
log.warning("DFT: ABORT — " + message);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(9);
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write((code >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_REPLY & 0xFF);
|
||||
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_HDR & 0xFF);
|
||||
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
|
||||
out.write(TR_ERR_CMDFAIL & 0xFF);
|
||||
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
listener.onTransferAborted(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a Read Modified command when upload data is pending.
|
||||
* Retransmits the last saved buffer.
|
||||
*/
|
||||
public void readModified() {
|
||||
if (dftSaveBuf != null && dftSaveBufLen > 0) {
|
||||
log.fine("DFT: Retransmitting saved buffer");
|
||||
input.sendStructuredFieldData(dftSaveBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture)
|
||||
* vector graphics and Programmed Symbols (PS).
|
||||
*/
|
||||
public final class GocaConstants {
|
||||
|
||||
private GocaConstants() {}
|
||||
|
||||
// Structured Field IDs
|
||||
public static final int SF_LOADPS = 0x0F; // 2-byte Structured Field prefix (or 0x06 for direct Load PS)
|
||||
public static final int SF_LOADPS_DIRECT = 0x06; // Load Programmed Symbols direct SFID
|
||||
public static final int SF_OBJCNTL = 0x24; // Object Control
|
||||
public static final int SF_OBJDATA = 0x85; // Graphics Object Data
|
||||
public static final int SF_3270_G = 0x20; // 3270 Graphics / Picture
|
||||
|
||||
// Structured Field Sub-IDs (for SF 0x0F)
|
||||
public static final int SF_LOADPS_SUB = 0x06; // Load Programmed Symbols
|
||||
public static final int SF_LOADLT_SUB = 0x07; // Load Line Type / Symbol Set
|
||||
public static final int SF_OBJDATA_SUB = 0x0F; // Graphics Object Data (GOCA draw orders)
|
||||
public static final int SF_OBJPICT_SUB = 0x10; // Graphics Object Picture (Segment draw orders)
|
||||
public static final int SF_OBJCNTL_SUB = 0x11; // Graphics Object Control (Procedure orders)
|
||||
|
||||
// Procedure Orders (for SF_OBJCNTL_SUB 0x11)
|
||||
public static final int P_NOP1 = 0x00; // Procedure NOOP
|
||||
public static final int P_COMT = 0x01; // Procedure Comment
|
||||
public static final int P_ATTCUR = 0x08; // Attach Graphic Cursor
|
||||
public static final int P_DETCUR = 0x09; // Detach Graphic Cursor
|
||||
public static final int P_ERASE = 0x0A; // Erase Graphics Presentation Space
|
||||
public static final int P_STOPDR = 0x0F; // Stop Draw
|
||||
public static final int P_SCUDEF = 0x21; // Set Current Defaults
|
||||
public static final int P_BEGPROC = 0x30; // Begin Procedure
|
||||
public static final int P_SETCUR = 0x31; // Set Graphic Cursor Position
|
||||
|
||||
// Coordinate space
|
||||
public static final int VIRTUAL_COORD_MAX = 4096;
|
||||
|
||||
// GOCA Drawing / Segment Orders
|
||||
public static final int G_NOP1 = 0x00; // NOOP 1-byte
|
||||
public static final int G_COMT = 0x01; // Comment
|
||||
public static final int G_GSMC = 0x07; // Set Marker Color
|
||||
public static final int G_GSPS = 0x08; // Set Pattern Set
|
||||
public static final int G_GSCOL = 0x0A; // Set Color
|
||||
public static final int G_GSMX = 0x0C; // Set Foreground Mix
|
||||
public static final int G_GSBMX = 0x0D; // Set Background Mix
|
||||
public static final int G_GSFLW = 0x11; // Set Fractional Line Width
|
||||
public static final int G_GSLT = 0x18; // Set Line Type
|
||||
public static final int G_GSLW = 0x19; // Set Line Width
|
||||
public static final int G_GSMS = 0x1B; // Set Marker Size
|
||||
public static final int G_GSCP = 0x21; // Set Current Position
|
||||
public static final int G_GSAP = 0x22; // Arc Parameters
|
||||
public static final int G_GSECOL = 0x26; // Set Extended Color
|
||||
public static final int G_GSVW = 0x27; // Set Viewing Window
|
||||
public static final int G_GSPT = 0x28; // Set Pattern Symbol
|
||||
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type
|
||||
public static final int G_GCALL = 0x2A; // Call Segment
|
||||
public static final int G_GSCH = 0x33; // Set Character Cell
|
||||
public static final int G_GSCA = 0x34; // Set Character Angle
|
||||
public static final int G_GSCR = 0x35; // Set Character Shear
|
||||
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
||||
public static final int G_GSCS = 0x38; // Set Character Set
|
||||
public static final int G_GSMP = 0x39; // Set Marker Precision
|
||||
public static final int G_GSETAG = 0x39; // Set Pick Identifier / Tag
|
||||
public static final int G_GSCD = 0x3A; // Set Character Direction
|
||||
public static final int G_GSCC = 0x3B; // Set Character Precision
|
||||
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
|
||||
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
|
||||
public static final int G_GPOP = 0x3F; // Pop Attribute
|
||||
public static final int G_GEAR = 0x60; // End Area
|
||||
public static final int G_GBAR = 0x68; // Begin Area
|
||||
public static final int G_BEGSEGM = 0x70; // Begin Segment
|
||||
public static final int G_ENDSEGM = 0x71; // End Segment
|
||||
public static final int G_GERASE = 0x7E; // Erase Graphics Plane
|
||||
public static final int G_GCLINE = 0x81; // Line at Current Position
|
||||
public static final int G_GCMRK = 0x82; // Marker at Current Position
|
||||
public static final int G_GCCHST = 0x83; // Character String at Current Position
|
||||
public static final int G_GCFLT = 0x85; // Fillet at Current Position
|
||||
public static final int G_GCARC = 0x86; // Partial Arc at Current Position
|
||||
public static final int G_GCFARC = 0x87; // Full Arc at Current Position
|
||||
public static final int G_GEIMG = 0x91; // End Image
|
||||
public static final int G_GIMD = 0x92; // Image Data
|
||||
public static final int G_GCRLIN = 0xA1; // Relative Line at Current Position
|
||||
public static final int G_GLINE = 0xC1; // Line (Absolute)
|
||||
public static final int G_GMRK = 0xC2; // Marker (Absolute)
|
||||
public static final int G_GCHST = 0xC3; // Character String (Absolute)
|
||||
public static final int G_GFLT = 0xC5; // Fillet (Absolute)
|
||||
public static final int G_GARC = 0xC6; // Partial Arc (Absolute)
|
||||
public static final int G_GFARC = 0xC7; // Full Arc (Absolute)
|
||||
public static final int G_GBIMG = 0xD1; // Begin Image
|
||||
public static final int G_GRLINE = 0xE1; // Relative Line (Absolute Start)
|
||||
|
||||
// Line Types (GSLT)
|
||||
public static final int LT_DEFAULT = 0;
|
||||
public static final int LT_DOT = 1;
|
||||
public static final int LT_SHORTDASH= 2;
|
||||
public static final int LT_DASHDOT = 3;
|
||||
public static final int LT_DOUBLEDOT= 4;
|
||||
public static final int LT_LONGDASH = 5;
|
||||
public static final int LT_DASHDOUBLEDOT = 6;
|
||||
public static final int LT_SOLID = 7;
|
||||
|
||||
// Line Widths (GSLW)
|
||||
public static final int LW_DEFAULT = 0;
|
||||
public static final int LW_NORMAL = 1;
|
||||
public static final int LW_THICK = 2;
|
||||
|
||||
// Character Precision (G_GSCC / 0x3B)
|
||||
public static final int CP_DEFAULT = 0;
|
||||
public static final int CP_STRING = 1;
|
||||
public static final int CP_CHAR = 2;
|
||||
public static final int CP_STROKE = 3;
|
||||
|
||||
// Fill Patterns (GSPT)
|
||||
public static final int PT_DEFAULT = 0;
|
||||
public static final int PT_D1 = 1;
|
||||
public static final int PT_D2 = 2;
|
||||
public static final int PT_D3 = 3;
|
||||
public static final int PT_D4 = 4;
|
||||
public static final int PT_D5 = 5;
|
||||
public static final int PT_D6 = 6;
|
||||
public static final int PT_D7 = 7;
|
||||
public static final int PT_D8 = 8;
|
||||
public static final int PT_VERT_LINE = 9;
|
||||
public static final int PT_HORIZ_LINE = 10;
|
||||
public static final int PT_DIAG_BLTR = 11;
|
||||
public static final int PT_DIAG_BLTR2 = 12;
|
||||
public static final int PT_DIAG_TLBR = 13;
|
||||
public static final int PT_DIAG_TLBR2 = 14;
|
||||
public static final int PT_EMPTY = 15;
|
||||
public static final int PT_SOLID = 16;
|
||||
|
||||
// Marker Symbols (GSMT)
|
||||
public static final int MK_DEFAULT = 0;
|
||||
public static final int MK_CROSS = 1; // x
|
||||
public static final int MK_PLUS = 2; // +
|
||||
public static final int MK_DIAMOND = 3; // <>
|
||||
public static final int MK_SQUARE = 4; // []
|
||||
public static final int MK_6STAR = 5; // * 6-point
|
||||
public static final int MK_8STAR = 6; // * 8-point
|
||||
public static final int MK_SDIAMOND = 7; // solid diamond
|
||||
public static final int MK_SSQUARE = 8; // solid square
|
||||
public static final int MK_DOT = 9; // .
|
||||
public static final int MK_CIRCLE = 10;// o
|
||||
|
||||
// Character Direction (GSCD)
|
||||
public static final int CD_DEFAULT = 0;
|
||||
public static final int CD_LR = 1; // Left to Right
|
||||
public static final int CD_TB = 2; // Top to Bottom
|
||||
public static final int CD_RL = 3; // Right to Left
|
||||
public static final int CD_BT = 4; // Bottom to Top
|
||||
|
||||
// Foreground / Background Mix Modes (GSMX / GSBMX)
|
||||
public static final int MIX_DEFAULT = 0;
|
||||
public static final int MIX_OR = 1;
|
||||
public static final int MIX_OVER = 2;
|
||||
public static final int MIX_LEAVE = 3;
|
||||
public static final int MIX_XOR = 4;
|
||||
public static final int MIX_UNDER = 5;
|
||||
|
||||
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
|
||||
public static final int[] GOCA_COLORS = new int[] {
|
||||
0xFF00FF00, // 0: Default (Green)
|
||||
0xFF7890F0, // 1: Blue (120, 144, 240)
|
||||
0xFFFF0000, // 2: Red (255, 0, 0)
|
||||
0xFFFF00FF, // 3: Pink / Magenta (255, 0, 255)
|
||||
0xFF00FF00, // 4: Green (0, 255, 0)
|
||||
0xFF00FFFF, // 5: Turquoise / Cyan (0, 255, 255)
|
||||
0xFFFFFF00, // 6: Yellow (255, 255, 0)
|
||||
0xFFFFFFFF, // 7: Neutral White (255, 255, 255)
|
||||
0xFF000000, // 8: Black (0, 0, 0)
|
||||
0xFF000080, // 9: Deep Blue (0, 0, 128)
|
||||
0xFF800000, // 10: Orange / Dark Red (128, 0, 0)
|
||||
0xFF800080, // 11: Purple (128, 0, 128)
|
||||
0xFF008000, // 12: Pale Green (0, 128, 0)
|
||||
0xFF008080, // 13: Pale Cyan (0, 128, 128)
|
||||
0xFFD79700, // 14: Mustard (215, 151, 0)
|
||||
0xFFC0C0C0, // 15: Grey / Light White (192, 192, 192)
|
||||
0xFF492400 // 16: Brown (73, 36, 0)
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the 32-bit ARGB color value for a given GOCA color index (0-16).
|
||||
* Returns Green (0xFF00FF00) if the index is out of range.
|
||||
*/
|
||||
public static int getGocaColorArgb(int colorIndex) {
|
||||
if (colorIndex == 0xFF) {
|
||||
return GOCA_COLORS[7];
|
||||
}
|
||||
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
|
||||
return GOCA_COLORS[colorIndex];
|
||||
}
|
||||
return GOCA_COLORS[0];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Builds the 56-byte IBM 3179G / 3270G Graphic Input Structured Field.
|
||||
* Used for light-pen and graphics cursor interactive input per IBM HOD / GDDM specifications.
|
||||
*/
|
||||
public class GraphicInputBuilder {
|
||||
|
||||
// 56-byte template mask from IBM Host On-Demand (HODInput.java)
|
||||
private static final byte[] MASK = new byte[] {
|
||||
0x00, 0x34, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
|
||||
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
|
||||
0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field.
|
||||
*
|
||||
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
||||
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
||||
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
|
||||
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
|
||||
* @param isShift true if shift key was down
|
||||
* @param isCtrl true if ctrl key was down
|
||||
* @return 56-byte payload
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||
return buildGraphicInput(gocaX, gocaY, aidCode, isMouseAction, isShift, isCtrl, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
|
||||
*
|
||||
* IMPORTANT ARCHITECTURE NOTE:
|
||||
* Per IBM GA23-0059 / GDDM specifications:
|
||||
* - Bytes 24-27: (gocaX, gocaY) cursor coordinates.
|
||||
* - Bytes 28-31: Picked Segment Identifier (32-bit big-endian). When clicking on a menu item or
|
||||
* interactive element (e.g. DRAW button = Segment 2, EXIT button = Segment 5), GDDM requires
|
||||
* the exact segment ID in bytes 28-31. If hardcoded or mismatched, GDDM rejects the click
|
||||
* with a WCC 0xF7 alarm beep.
|
||||
* - Bytes 32-33: Pick Correlation Tag (16-bit big-endian) set by G_GSETAG (0x39).
|
||||
*
|
||||
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
||||
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
||||
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
|
||||
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
|
||||
* @param isShift true if shift key was down
|
||||
* @param isCtrl true if ctrl key was down
|
||||
* @param pickedSegId Picked GOCA segment ID (0 if none)
|
||||
* @param pickTag Pick correlation tag
|
||||
* @return 56-byte payload
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||
int pickedSegId, int pickTag) {
|
||||
byte[] sf = new byte[MASK.length];
|
||||
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||
|
||||
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
|
||||
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
|
||||
sf[25] = (byte) (gocaX & 0xFF);
|
||||
|
||||
// Byte 26-27: GOCA Y coordinate (signed 16-bit big-endian)
|
||||
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
|
||||
sf[27] = (byte) (gocaY & 0xFF);
|
||||
|
||||
if (pickedSegId != 0) {
|
||||
// Byte 28-31: Picked Segment ID (4 bytes big-endian)
|
||||
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
|
||||
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
|
||||
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
|
||||
sf[31] = (byte) (pickedSegId & 0xFF);
|
||||
}
|
||||
|
||||
if (isMouseAction) {
|
||||
if (pickTag != 0) {
|
||||
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
|
||||
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
|
||||
sf[33] = (byte) (pickTag & 0xFF);
|
||||
}
|
||||
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
|
||||
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
|
||||
} else {
|
||||
// Keyboard AID (Enter, PF keys)
|
||||
sf[31] = 0x07;
|
||||
sf[33] = 0x07;
|
||||
sf[34] = (byte) 0xFF;
|
||||
sf[35] = (byte) (aidCode & 0xFF);
|
||||
}
|
||||
|
||||
return sf;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Graphics modes supported by j3270 / lib3270j.
|
||||
*/
|
||||
public enum GraphicsMode {
|
||||
/** Text only (no graphics query replies, classic 3279-4 terminal behavior). */
|
||||
NONE("None (Text Only)"),
|
||||
|
||||
/** Programmed Symbols only (custom character matrices and APL, single & triple plane). */
|
||||
PROGRAMMED_SYMBOLS("Programmed Symbols Only"),
|
||||
|
||||
/** Vector graphics only (GOCA / 3179G drawing orders). */
|
||||
VECTOR_GRAPHICS("Vector Graphics Only"),
|
||||
|
||||
/** Full graphics support (both Programmed Symbols and Vector Graphics). */
|
||||
BOTH("Both (Programmed Symbols & Vector Graphics)");
|
||||
|
||||
private final String description;
|
||||
|
||||
GraphicsMode(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isProgrammedSymbolsEnabled() {
|
||||
return this == PROGRAMMED_SYMBOLS || this == BOTH;
|
||||
}
|
||||
|
||||
public boolean isVectorGraphicsEnabled() {
|
||||
return this == VECTOR_GRAPHICS || this == BOTH;
|
||||
}
|
||||
|
||||
public static GraphicsMode fromString(String str) {
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return NONE;
|
||||
}
|
||||
String s = str.trim().toUpperCase();
|
||||
switch (s) {
|
||||
case "BOTH":
|
||||
case "ALL":
|
||||
case "FULL":
|
||||
case "ON":
|
||||
case "TRUE":
|
||||
return BOTH;
|
||||
case "PS":
|
||||
case "PROGRAMMED_SYMBOLS":
|
||||
case "PROGRAMMEDSYMBOLS":
|
||||
case "SYMBOLS":
|
||||
case "APL":
|
||||
return PROGRAMMED_SYMBOLS;
|
||||
case "VECTOR":
|
||||
case "VECTOR_GRAPHICS":
|
||||
case "VECTORGRAPHICS":
|
||||
case "GOCA":
|
||||
case "GDDM":
|
||||
return VECTOR_GRAPHICS;
|
||||
case "NONE":
|
||||
case "OFF":
|
||||
case "FALSE":
|
||||
case "DISABLED":
|
||||
case "TEXT":
|
||||
default:
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,843 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Offscreen rendering surface for GOCA vector graphics.
|
||||
* Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
|
||||
* Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap).
|
||||
*/
|
||||
public class GraphicsPlane {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
|
||||
|
||||
// 17 Standard GOCA 8x8 Fill Patterns
|
||||
public static final byte[][] PATTERN_DATA = new byte[][]{
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1}, // 0: Solid (all 1s)
|
||||
{-1, -1, -1, -18, -1, -1, -1, -18}, // 1: D1
|
||||
{-1, -69, -1, -18, -1, -69, -1, -18}, // 2: D2
|
||||
{119, -35, -69, -18, 119, -35, -69, -18}, // 3: D3
|
||||
{-69, -52, 51, -18, -69, -52, 51, -18}, // 4: D4
|
||||
{85, -86, 85, -86, 85, -86, 85, -86}, // 5: D5 (50% checker)
|
||||
{68, 51, -52, 17, 68, 51, -52, 17}, // 6: D6
|
||||
{-120, 34, 68, 17, -120, 34, 68, 17}, // 7: D7
|
||||
{0, 68, 0, 17, 0, 68, 0, 17}, // 8: D8 (sparse dots)
|
||||
{-128, -128, -128, -128, -128, -128, -128, -128}, // 9: Vertical line
|
||||
{-1, 0, 0, 0, 0, 0, 0, 0}, // 10: Horizontal line
|
||||
{1, 2, 4, 8, 16, 32, 64, -128}, // 11: Diagonal bottom-left to top-right
|
||||
{3, 12, 48, -64, 3, 12, 48, -64}, // 12: Diagonal BL-TR dense
|
||||
{-128, 64, 32, 16, 8, 4, 2, 1}, // 13: Diagonal top-left to bottom-right
|
||||
{-64, 48, 12, 3, -64, 48, 12, 3}, // 14: Diagonal TL-BR dense
|
||||
{0, 0, 0, 0, 0, 0, 0, 0}, // 15: Empty (transparent)
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
||||
};
|
||||
|
||||
private int canvasWidth = 800;
|
||||
private int canvasHeight = 600;
|
||||
private int[] rgbBuffer;
|
||||
private boolean hasContent = false;
|
||||
private long updateCount = 0;
|
||||
|
||||
private int screenCols = 80;
|
||||
private int screenRows = 24;
|
||||
|
||||
public GraphicsPlane(int width, int height) {
|
||||
this.canvasWidth = Math.max(1, width);
|
||||
this.canvasHeight = Math.max(1, height);
|
||||
this.rgbBuffer = new int[canvasWidth * canvasHeight];
|
||||
}
|
||||
|
||||
public synchronized void resize(int width, int height) {
|
||||
int w = Math.max(1, width);
|
||||
int h = Math.max(1, height);
|
||||
if (w == canvasWidth && h == canvasHeight && rgbBuffer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int[] newBuffer = new int[w * h];
|
||||
if (rgbBuffer != null && hasContent) {
|
||||
// Scale existing content to new dimensions using nearest-neighbor
|
||||
for (int dy = 0; dy < h; dy++) {
|
||||
int sy = (dy * canvasHeight) / h;
|
||||
for (int dx = 0; dx < w; dx++) {
|
||||
int sx = (dx * canvasWidth) / w;
|
||||
newBuffer[dy * w + dx] = rgbBuffer[sy * canvasWidth + sx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.canvasWidth = w;
|
||||
this.canvasHeight = h;
|
||||
this.rgbBuffer = newBuffer;
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
if (rgbBuffer != null) {
|
||||
Arrays.fill(rgbBuffer, 0);
|
||||
}
|
||||
hasContent = false;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized long getUpdateCount() {
|
||||
return updateCount;
|
||||
}
|
||||
|
||||
public synchronized boolean hasContent() {
|
||||
return hasContent;
|
||||
}
|
||||
|
||||
public synchronized int[] getRgbBuffer() {
|
||||
return rgbBuffer;
|
||||
}
|
||||
|
||||
public int getCanvasWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
public int getCanvasHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
public synchronized void setScreenDimensions(int cols, int rows) {
|
||||
this.screenCols = cols > 0 ? cols : 80;
|
||||
this.screenRows = rows > 0 ? rows : 24;
|
||||
int targetW = this.screenCols * 9;
|
||||
int targetH = this.screenRows * 12;
|
||||
if (this.canvasWidth != targetW || this.canvasHeight != targetH) {
|
||||
resize(targetW, targetH);
|
||||
}
|
||||
}
|
||||
|
||||
public int getScreenCols() {
|
||||
return screenCols;
|
||||
}
|
||||
|
||||
public int getScreenRows() {
|
||||
return screenRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
|
||||
*/
|
||||
public double mapXDouble(double gocaX) {
|
||||
int nominalWidth = screenCols * 9;
|
||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
||||
double nx = gocaX + xMax;
|
||||
return (nx * canvasWidth) / (double) (nominalWidth > 0 ? nominalWidth : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
|
||||
*/
|
||||
public double mapYDouble(double gocaY) {
|
||||
int nominalHeight = screenRows * 12;
|
||||
int yMax = (nominalHeight - 1) / 2;
|
||||
double ny = yMax - gocaY;
|
||||
return (ny * canvasHeight) / (double) (nominalHeight > 0 ? nominalHeight : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
||||
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
|
||||
*/
|
||||
public int mapX(int gocaX) {
|
||||
return (int) Math.round(mapXDouble((double) gocaX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
||||
* Coordinate space is symmetric: -yMax to +yMax, where nominalHeight = rows * 12 (e.g. 516 for 43 rows).
|
||||
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
|
||||
*/
|
||||
public int mapY(int gocaY) {
|
||||
return (int) Math.round(mapYDouble((double) gocaY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a canvas pixel X coordinate back to GOCA signed coordinate (-xMax..+xMax).
|
||||
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
|
||||
*/
|
||||
public int unmapX(int px) {
|
||||
int nominalWidth = screenCols * 9;
|
||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
||||
int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1));
|
||||
return nx - xMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up).
|
||||
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
|
||||
*/
|
||||
public int unmapY(int py) {
|
||||
int nominalHeight = screenRows * 12;
|
||||
int yMax = (nominalHeight - 1) / 2;
|
||||
int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1));
|
||||
return yMax - ny;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
|
||||
*/
|
||||
public synchronized void setPixel(int x, int y, int colorArgb) {
|
||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||
int srcA = (colorArgb >>> 24) & 0xFF;
|
||||
if (srcA == 0) return;
|
||||
int idx = y * canvasWidth + x;
|
||||
if (srcA == 255) {
|
||||
rgbBuffer[idx] = colorArgb;
|
||||
} else {
|
||||
int dst = rgbBuffer[idx];
|
||||
int dstA = (dst >>> 24) & 0xFF;
|
||||
if (dstA == 0) {
|
||||
rgbBuffer[idx] = colorArgb;
|
||||
} else {
|
||||
int srcR = (colorArgb >>> 16) & 0xFF;
|
||||
int srcG = (colorArgb >>> 8) & 0xFF;
|
||||
int srcB = colorArgb & 0xFF;
|
||||
|
||||
int dstR = (dst >>> 16) & 0xFF;
|
||||
int dstG = (dst >>> 8) & 0xFF;
|
||||
int dstB = dst & 0xFF;
|
||||
|
||||
int invSrcA = 255 - srcA;
|
||||
int outA = srcA + (dstA * invSrcA + 127) / 255;
|
||||
int outR = (srcR * srcA + dstR * invSrcA + 127) / 255;
|
||||
int outG = (srcG * srcA + dstG * invSrcA + 127) / 255;
|
||||
int outB = (srcB * srcA + dstB * invSrcA + 127) / 255;
|
||||
|
||||
rgbBuffer[idx] = ((outA & 0xFF) << 24) | ((outR & 0xFF) << 16) | ((outG & 0xFF) << 8) | (outB & 0xFF);
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plots a pixel with fractional alpha coverage (0.0 to 1.0) for anti-aliasing.
|
||||
*/
|
||||
public synchronized void setPixelCoverage(int x, int y, int colorRgb, double coverage) {
|
||||
if (coverage <= 0.0) return;
|
||||
int alpha = (int) Math.round(coverage * 255.0);
|
||||
if (alpha > 255) alpha = 255;
|
||||
if (alpha <= 0) return;
|
||||
setPixel(x, y, (alpha << 24) | (colorRgb & 0x00FFFFFF));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an anti-aliased line using Xiaolin Wu's algorithm with sub-pixel double coordinates.
|
||||
*/
|
||||
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : 0x00FFFFFF;
|
||||
|
||||
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
|
||||
drawStyledLine((int) Math.round(x0), (int) Math.round(y0),
|
||||
(int) Math.round(x1), (int) Math.round(y1),
|
||||
(0xFF << 24) | color, lineType, lineWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: single point or zero-length line
|
||||
if (Math.abs(x1 - x0) < 1e-5 && Math.abs(y1 - y0) < 1e-5) {
|
||||
drawPixelWithThickness((int) Math.round(x0), (int) Math.round(y0), (0xFF << 24) | color, (lineWidth == GocaConstants.LW_THICK) ? 2 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
|
||||
if (steep) {
|
||||
double tmp = x0; x0 = y0; y0 = tmp;
|
||||
tmp = x1; x1 = y1; y1 = tmp;
|
||||
}
|
||||
if (x0 > x1) {
|
||||
double tmp = x0; x0 = x1; x1 = tmp;
|
||||
tmp = y0; y0 = y1; y1 = tmp;
|
||||
}
|
||||
|
||||
double dx = x1 - x0;
|
||||
double dy = y1 - y0;
|
||||
double gradient = (dx == 0.0) ? 1.0 : (dy / dx);
|
||||
|
||||
// First endpoint
|
||||
double xend = Math.round(x0);
|
||||
double yend = y0 + gradient * (xend - x0);
|
||||
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
|
||||
int xpxl1 = (int) xend;
|
||||
int ypxl1 = (int) Math.floor(yend);
|
||||
|
||||
if (steep) {
|
||||
plotPixelWu(ypxl1, xpxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(ypxl1 + 1, xpxl1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
} else {
|
||||
plotPixelWu(xpxl1, ypxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(xpxl1, ypxl1 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
}
|
||||
double intery = yend + gradient;
|
||||
|
||||
// Second endpoint
|
||||
xend = Math.round(x1);
|
||||
yend = y1 + gradient * (xend - x1);
|
||||
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
|
||||
int xpxl2 = (int) xend;
|
||||
int ypxl2 = (int) Math.floor(yend);
|
||||
|
||||
if (steep) {
|
||||
plotPixelWu(ypxl2, xpxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(ypxl2 + 1, xpxl2, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
} else {
|
||||
plotPixelWu(xpxl2, ypxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(xpxl2, ypxl2 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
}
|
||||
|
||||
// Main anti-aliased stepping loop
|
||||
if (steep) {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int y = (int) Math.floor(intery);
|
||||
double frac = intery - y;
|
||||
plotPixelWu(y, x, color, 1.0 - frac, lineWidth);
|
||||
plotPixelWu(y + 1, x, color, frac, lineWidth);
|
||||
intery += gradient;
|
||||
}
|
||||
} else {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int y = (int) Math.floor(intery);
|
||||
double frac = intery - y;
|
||||
plotPixelWu(x, y, color, 1.0 - frac, lineWidth);
|
||||
plotPixelWu(x, y + 1, color, frac, lineWidth);
|
||||
intery += gradient;
|
||||
}
|
||||
}
|
||||
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
|
||||
if (brightness <= 0.0) return;
|
||||
if (lineWidth == GocaConstants.LW_THICK) {
|
||||
setPixelCoverage(x, y, colorRgb, 1.0);
|
||||
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness));
|
||||
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
|
||||
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
|
||||
} else {
|
||||
// Perceptual gamma correction for crisp contrast on dark backgrounds
|
||||
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
|
||||
setPixelCoverage(x, y, colorRgb, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an absolute or relative line using anti-aliasing for smooth vectors.
|
||||
*/
|
||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth);
|
||||
}
|
||||
|
||||
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
||||
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
||||
|
||||
int dx = Math.abs(x2 - x1);
|
||||
int dy = Math.abs(y2 - y1);
|
||||
int sx = x1 < x2 ? 1 : -1;
|
||||
int sy = y1 < y2 ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
int curX = x1;
|
||||
int curY = y1;
|
||||
int stepIndex = 0;
|
||||
|
||||
while (true) {
|
||||
if (shouldPlotLinePixel(stepIndex, lineType)) {
|
||||
drawPixelWithThickness(curX, curY, color, thickness);
|
||||
}
|
||||
stepIndex++;
|
||||
|
||||
if (curX == x2 && curY == y2) {
|
||||
break;
|
||||
}
|
||||
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
curX += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
curY += sy;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
private boolean shouldPlotLinePixel(int step, int lineType) {
|
||||
switch (lineType) {
|
||||
case GocaConstants.LT_DOT:
|
||||
return (step % 4) < 2;
|
||||
case GocaConstants.LT_SHORTDASH:
|
||||
return (step % 6) < 4;
|
||||
case GocaConstants.LT_DASHDOT:
|
||||
int m12 = step % 12;
|
||||
return m12 < 6 || (m12 >= 8 && m12 < 10);
|
||||
case GocaConstants.LT_DOUBLEDOT:
|
||||
int m10 = step % 10;
|
||||
return m10 < 2 || (m10 >= 4 && m10 < 6);
|
||||
case GocaConstants.LT_LONGDASH:
|
||||
return (step % 11) < 8;
|
||||
case GocaConstants.LT_DASHDOUBLEDOT:
|
||||
int m18 = step % 18;
|
||||
return m18 < 8 || (m18 >= 10 && m18 < 12) || (m18 >= 14 && m18 < 16);
|
||||
case GocaConstants.LT_SOLID:
|
||||
case GocaConstants.LT_DEFAULT:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPixelWithThickness(int x, int y, int color, int thickness) {
|
||||
if (thickness <= 1) {
|
||||
setPixel(x, y, color);
|
||||
} else {
|
||||
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) {
|
||||
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) {
|
||||
setPixel(x + dx, y + dy, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a full or partial arc / ellipse with sub-pixel double precision and anti-aliasing.
|
||||
*/
|
||||
public synchronized void drawArc(double cx, double cy, double rx, double ry, double startAngleDeg, double sweepAngleDeg,
|
||||
int colorArgb, int lineType, int lineWidth, boolean isFull) {
|
||||
if (rx <= 0) rx = 1.0;
|
||||
if (ry <= 0) ry = 1.0;
|
||||
|
||||
int numSteps = (int) Math.max(36, Math.max(rx, ry) * 6);
|
||||
double startRad = Math.toRadians(startAngleDeg);
|
||||
double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg);
|
||||
double stepRad = sweepRad / numSteps;
|
||||
|
||||
double prevX = cx + rx * Math.cos(startRad);
|
||||
double prevY = cy - ry * Math.sin(startRad);
|
||||
|
||||
for (int i = 1; i <= numSteps; i++) {
|
||||
double angle = startRad + i * stepRad;
|
||||
double nextX = cx + rx * Math.cos(angle);
|
||||
double nextY = cy - ry * Math.sin(angle);
|
||||
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
|
||||
prevX = nextX;
|
||||
prevY = nextY;
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
|
||||
int colorArgb, int lineType, int lineWidth, boolean isFull) {
|
||||
drawArc((double) cx, (double) cy, (double) rx, (double) ry, startAngleDeg, sweepAngleDeg, colorArgb, lineType, lineWidth, isFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a Fillet (spline / curve approximation across control points) with sub-pixel precision.
|
||||
*/
|
||||
public synchronized void drawFillet(double[] px, double[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
|
||||
if (px == null || py == null || numPoints < 2) return;
|
||||
|
||||
if (numPoints == 2) {
|
||||
drawLine(px[0], py[0], px[1], py[1], colorArgb, lineType, lineWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
double prevX = px[0];
|
||||
double prevY = py[0];
|
||||
|
||||
for (int i = 0; i < numPoints - 1; i++) {
|
||||
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
|
||||
double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0;
|
||||
double p1x = px[i];
|
||||
double p1y = py[i];
|
||||
double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0;
|
||||
double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0;
|
||||
|
||||
int steps = 30;
|
||||
for (int s = 1; s <= steps; s++) {
|
||||
double t = (double) s / steps;
|
||||
double oneMinusT = 1.0 - t;
|
||||
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
|
||||
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
|
||||
drawLine(prevX, prevY, bx, by, colorArgb, lineType, lineWidth);
|
||||
prevX = bx;
|
||||
prevY = by;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
|
||||
if (px == null || py == null || numPoints < 2) return;
|
||||
double[] dpx = new double[numPoints];
|
||||
double[] dpy = new double[numPoints];
|
||||
for (int i = 0; i < numPoints; i++) {
|
||||
dpx[i] = px[i];
|
||||
dpy[i] = py[i];
|
||||
}
|
||||
drawFillet(dpx, dpy, numPoints, colorArgb, lineType, lineWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a closed polygon area with a solid color or hatching pattern.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) {
|
||||
fillArea(px, py, numPoints, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, 2, 0xFF000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a closed polygon area with a solid color or hatching pattern and background mix.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb) {
|
||||
fillArea(px, py, numPoints, null, 1, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area containing one or more closed polygon subpaths with a solid color or hatching pattern,
|
||||
* using the even-odd fill rule across all subpath contours.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
|
||||
int fillColorArgb, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb) {
|
||||
if (px == null || py == null || numPoints < 3) return;
|
||||
|
||||
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
|
||||
int bg = bgColorArgb;
|
||||
|
||||
if (pattern != GocaConstants.PT_EMPTY) {
|
||||
// Find polygon vertical bounds across all points
|
||||
int minY = py[0];
|
||||
int maxY = py[0];
|
||||
for (int i = 1; i < numPoints; i++) {
|
||||
if (py[i] < minY) minY = py[i];
|
||||
if (py[i] > maxY) maxY = py[i];
|
||||
}
|
||||
minY = Math.max(0, minY);
|
||||
maxY = Math.min(canvasHeight - 1, maxY);
|
||||
|
||||
List<Integer> nodeX = new ArrayList<>();
|
||||
byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0];
|
||||
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
nodeX.clear();
|
||||
int offset = 0;
|
||||
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
|
||||
for (int p = 0; p < polyCount; p++) {
|
||||
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
|
||||
if (pLen >= 3) {
|
||||
int j = pLen - 1;
|
||||
for (int i = 0; i < pLen; i++) {
|
||||
int yi = py[offset + i];
|
||||
int yj = py[offset + j];
|
||||
int xi = px[offset + i];
|
||||
int xj = px[offset + j];
|
||||
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
|
||||
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
|
||||
nodeX.add(x);
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
}
|
||||
offset += pLen;
|
||||
}
|
||||
|
||||
Collections.sort(nodeX);
|
||||
|
||||
for (int i = 0; i < nodeX.size(); i += 2) {
|
||||
if (i + 1 >= nodeX.size()) break;
|
||||
int leftX = Math.max(0, nodeX.get(i));
|
||||
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
|
||||
|
||||
for (int x = leftX; x <= rightX; x++) {
|
||||
if (pattern == GocaConstants.PT_SOLID || pattern == 0 || pattern >= 16) {
|
||||
setPixel(x, y, fill);
|
||||
} else {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix != 0) { // BMX_OVERPAINT (opaque background)
|
||||
setPixel(x, y, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (drawBoundary && boundaryColorArgb != 0) {
|
||||
int offset = 0;
|
||||
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
|
||||
for (int p = 0; p < polyCount; p++) {
|
||||
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
|
||||
if (pLen >= 2) {
|
||||
for (int i = 0; i < pLen - 1; i++) {
|
||||
drawLine((double) px[offset + i], (double) py[offset + i],
|
||||
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
||||
boundaryColorArgb, lineType, lineWidth);
|
||||
}
|
||||
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
|
||||
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
|
||||
(double) px[offset], (double) py[offset],
|
||||
boundaryColorArgb, lineType, lineWidth);
|
||||
}
|
||||
}
|
||||
offset += pLen;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision.
|
||||
*/
|
||||
public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) {
|
||||
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
||||
double s = Math.max(3.0, size > 0 ? (double) size : 5.0);
|
||||
|
||||
switch (markerType) {
|
||||
case GocaConstants.MK_CROSS: // x
|
||||
drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_PLUS: // +
|
||||
case GocaConstants.MK_DEFAULT:
|
||||
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_DIAMOND: // <>
|
||||
drawLine(x, y - s, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x + s, y, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x, y + s, x - s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s, y, x, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_SQUARE: // []
|
||||
drawLine(x - s, y - s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x + s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x + s, y + s, x - s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s, y + s, x - s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_6STAR: // 6-point star
|
||||
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s / 2.0, y - s, x + s / 2.0, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s / 2.0, y + s, x + s / 2.0, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_8STAR: // 8-point star
|
||||
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_SDIAMOND: // solid diamond
|
||||
fillArea(new int[]{(int) Math.round(x), (int) Math.round(x + s), (int) Math.round(x), (int) Math.round(x - s)},
|
||||
new int[]{(int) Math.round(y - s), (int) Math.round(y), (int) Math.round(y + s), (int) Math.round(y)},
|
||||
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||
break;
|
||||
case GocaConstants.MK_SSQUARE: // solid square
|
||||
fillArea(new int[]{(int) Math.round(x - s), (int) Math.round(x + s), (int) Math.round(x + s), (int) Math.round(x - s)},
|
||||
new int[]{(int) Math.round(y - s), (int) Math.round(y - s), (int) Math.round(y + s), (int) Math.round(y + s)},
|
||||
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||
break;
|
||||
case GocaConstants.MK_DOT: // dot
|
||||
drawArc(x, y, 2.0, 2.0, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
|
||||
break;
|
||||
case GocaConstants.MK_CIRCLE: // circle
|
||||
default:
|
||||
drawArc(x, y, s, s, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
|
||||
break;
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
|
||||
drawMarker((double) x, (double) y, markerType, size, colorArgb);
|
||||
}
|
||||
|
||||
private static final int[] VSS_OFFSETS = new int[256];
|
||||
static {
|
||||
Arrays.fill(VSS_OFFSETS, -1);
|
||||
int sym = VectorSymbolData.VSS_SYMBOL_START; // 33
|
||||
if (sym < 256) {
|
||||
VSS_OFFSETS[sym] = 0;
|
||||
}
|
||||
for (int i = 0; i < VectorSymbolData.vss_data.length; i++) {
|
||||
if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF
|
||||
sym++;
|
||||
if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) {
|
||||
VSS_OFFSETS[sym] = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TextRenderer {
|
||||
void drawText(GraphicsPlane plane, double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle);
|
||||
}
|
||||
|
||||
private TextRenderer textRenderer;
|
||||
|
||||
public void setTextRenderer(TextRenderer renderer) {
|
||||
this.textRenderer = renderer;
|
||||
}
|
||||
|
||||
public TextRenderer getTextRenderer() {
|
||||
return this.textRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws character text (using pluggable TextRenderer or fallback vector font).
|
||||
*/
|
||||
public synchronized void drawText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
if (textRenderer != null) {
|
||||
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
} else {
|
||||
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void drawText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws stroked vector text using IBM Vector Symbol Set (VSS) with sub-pixel anti-aliasing.
|
||||
*/
|
||||
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
||||
|
||||
double curX = x;
|
||||
double curY = y;
|
||||
double cw = cellWidth > 0 ? cellWidth : 12.0;
|
||||
double ch = cellHeight > 0 ? cellHeight : 20.0;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
drawVssChar(curX, curY, c, color, cw, ch);
|
||||
|
||||
switch (dir) {
|
||||
case GocaConstants.CD_TB: curY += ch; break;
|
||||
case GocaConstants.CD_RL: curX -= cw; break;
|
||||
case GocaConstants.CD_BT: curY -= ch; break;
|
||||
case GocaConstants.CD_LR:
|
||||
case GocaConstants.CD_DEFAULT:
|
||||
default:
|
||||
curX += cw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
||||
}
|
||||
|
||||
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
||||
int code = (int) c;
|
||||
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
|
||||
return;
|
||||
}
|
||||
int offset = VSS_OFFSETS[code];
|
||||
if (offset < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int ptr = offset;
|
||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||
if (order == 0xC1) {
|
||||
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
|
||||
int numPoints = byteLen / 4;
|
||||
int dataPtr = ptr + 2;
|
||||
|
||||
if (numPoints >= 2) {
|
||||
double[] px = new double[numPoints];
|
||||
double[] py = new double[numPoints];
|
||||
int[] ipx = new int[numPoints];
|
||||
int[] ipy = new int[numPoints];
|
||||
|
||||
for (int p = 0; p < numPoints; p++) {
|
||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||
py[p] = y + ((double) (VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||
ipx[p] = (int) Math.round(px[p]);
|
||||
ipy[p] = (int) Math.round(py[p]);
|
||||
}
|
||||
|
||||
// If contour is closed (e.g. bold character loop), fill with solid color
|
||||
int firstVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
|
||||
int firstVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
|
||||
int lastVx = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 1] & 0xFF);
|
||||
int lastVy = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 3] & 0xFF);
|
||||
|
||||
boolean isClosed = (numPoints >= 4) && (firstVx == lastVx) && (firstVy == lastVy);
|
||||
if (isClosed) {
|
||||
fillArea(ipx, ipy, numPoints, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||
}
|
||||
|
||||
for (int p = 0; p < numPoints - 1; p++) {
|
||||
drawLine(px[p], py[p], px[p + 1], py[p + 1], color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
}
|
||||
}
|
||||
ptr += 2 + byteLen;
|
||||
} else {
|
||||
ptr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws raw image pixel bitmap.
|
||||
*/
|
||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
|
||||
if (imageData == null || width <= 0 || height <= 0) return;
|
||||
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF;
|
||||
int bytesPerRow = (width + 7) / 8;
|
||||
|
||||
for (int row = 0; row < height; row++) {
|
||||
int rowOffset = row * bytesPerRow;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int byteIdx = rowOffset + (col / 8);
|
||||
if (byteIdx < imageData.length) {
|
||||
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
||||
if (bit) {
|
||||
setPixel(x + col, y + row, fgColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Manages IBM 3270 Programmed Symbols (PS / APL) character sets.
|
||||
* Handles the Load Programmed Symbols (LOADPS structured field 0x0F).
|
||||
*/
|
||||
public class ProgramSymbolManager {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ProgramSymbolManager.class.getName());
|
||||
|
||||
public static final int NUMBER_SYMBOL_SETS = 10;
|
||||
public static final int NUMBER_SINGLE_PLANE_PS_SETS = 2; // RWS 2..3 (Sets 0..1)
|
||||
public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5)
|
||||
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
||||
|
||||
private int defaultCellWidth = 9;
|
||||
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
|
||||
|
||||
public void setDefaultCellDimensions(int width, int height) {
|
||||
this.defaultCellWidth = (width > 0) ? width : 9;
|
||||
this.defaultCellHeight = (height > 0) ? height : 12;
|
||||
}
|
||||
|
||||
public int getDefaultCellWidth() {
|
||||
return defaultCellWidth;
|
||||
}
|
||||
|
||||
public int getDefaultCellHeight() {
|
||||
return defaultCellHeight;
|
||||
}
|
||||
|
||||
private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||
private final ProgramSymbolSet[] stagingSets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||
private final ProgramSymbolSet[] lcidMap = new ProgramSymbolSet[256];
|
||||
|
||||
public ProgramSymbolManager() {
|
||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||
boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS);
|
||||
sets[i] = new ProgramSymbolSet(isTriple);
|
||||
stagingSets[i] = new ProgramSymbolSet(isTriple);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all symbol sets.
|
||||
*/
|
||||
public synchronized void clearAll() {
|
||||
Arrays.fill(lcidMap, null);
|
||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||
sets[i].clear();
|
||||
sets[i].setLcid(0);
|
||||
stagingSets[i].clear();
|
||||
stagingSets[i].setLcid(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits all staged multi-plane symbol sets to the active presentation sets atomically.
|
||||
*/
|
||||
public synchronized void commitStagedSymbols() {
|
||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||
int lcid = stagingSets[i].getLcid();
|
||||
if (lcid > 0) {
|
||||
ProgramSymbolSet staged = stagingSets[i];
|
||||
ProgramSymbolSet active = sets[i];
|
||||
active.setLcid(lcid);
|
||||
for (int slot = 0; slot < ProgramSymbolSet.NUM_SLOTS; slot++) {
|
||||
active.setSlot(slot, staged.getSlot(slot));
|
||||
}
|
||||
if (lcid < 256) {
|
||||
lcidMap[lcid] = active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE).
|
||||
*/
|
||||
public ProgramSymbolSet getSymbolSet(int lcid) {
|
||||
if (lcid <= 0 || lcid >= 256) {
|
||||
return null;
|
||||
}
|
||||
ProgramSymbolSet set = lcidMap[lcid];
|
||||
if (set == null) {
|
||||
for (ProgramSymbolSet s : sets) {
|
||||
if (s != null && s.getLcid() == lcid) return s;
|
||||
}
|
||||
for (ProgramSymbolSet s : stagingSets) {
|
||||
if (s != null && s.getLcid() == lcid) return s;
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
|
||||
*/
|
||||
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
||||
ProgramSymbolSet set = getSymbolSet(lcid);
|
||||
if (set == null) {
|
||||
return null;
|
||||
}
|
||||
int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint;
|
||||
return set.getSlot(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
|
||||
*/
|
||||
public synchronized void loadps(byte[] data) {
|
||||
if (data == null || data.length < 4) {
|
||||
logger.warning("LOADPS: Payload too short (" + (data == null ? 0 : data.length) + " bytes)");
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = data[0] & 0xFF;
|
||||
int loadFormat = flags & 0x1F;
|
||||
boolean clearAll = (flags & 0x40) != 0;
|
||||
boolean hasExtHeader = (flags & 0x80) != 0;
|
||||
|
||||
int lcid = data[1] & 0xFF;
|
||||
int startCodePoint = data[2] & 0xFF;
|
||||
int rws = data[3] & 0xFF;
|
||||
|
||||
int setIndex;
|
||||
switch (rws) {
|
||||
case 2: setIndex = 0; break;
|
||||
case 3: setIndex = 1; break;
|
||||
case 4: setIndex = 2; break;
|
||||
case 5: setIndex = 3; break;
|
||||
case 6: setIndex = 4; break;
|
||||
case 7: setIndex = 5; break;
|
||||
case 8: setIndex = 6; break;
|
||||
case 9: setIndex = 7; break;
|
||||
case 10: setIndex = 8; break;
|
||||
case 11: setIndex = 9; break;
|
||||
default:
|
||||
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||
ProgramSymbolSet set = sets[setIndex];
|
||||
|
||||
int extHeaderLen = 0;
|
||||
int cellWidth = defaultCellWidth;
|
||||
int cellHeight = defaultCellHeight;
|
||||
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||
|
||||
if (hasExtHeader && data.length > 4) {
|
||||
extHeaderLen = data[4] & 0xFF;
|
||||
if (extHeaderLen > 3 && data.length > 6) {
|
||||
int lw = data[6] & 0xFF;
|
||||
if (lw > 0) cellWidth = lw;
|
||||
}
|
||||
if (extHeaderLen > 4 && data.length > 7) {
|
||||
int lh = data[7] & 0xFF;
|
||||
if (lh > 0) cellHeight = lh;
|
||||
}
|
||||
if (extHeaderLen >= 6 && data.length > 9) {
|
||||
colorPlane = data[9] & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
set.setLcid(lcid);
|
||||
if (lcid > 0 && lcid < 256) {
|
||||
lcidMap[lcid] = set;
|
||||
}
|
||||
|
||||
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
|
||||
int remaining = data.length - offset;
|
||||
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||
|
||||
int bytesPerSymbol;
|
||||
if (loadFormat == 1) {
|
||||
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
|
||||
} else {
|
||||
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
|
||||
}
|
||||
|
||||
if (bytesPerSymbol <= 0) {
|
||||
bytesPerSymbol = 18;
|
||||
}
|
||||
|
||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||
byte[] pixelData = new byte[cellWidth * cellHeight];
|
||||
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
||||
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
||||
}
|
||||
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
}
|
||||
|
||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
||||
|
||||
codeIndex++;
|
||||
offset += bytesPerSymbol;
|
||||
remaining -= bytesPerSymbol;
|
||||
}
|
||||
|
||||
if (clearAll) {
|
||||
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
|
||||
set.clearSlot(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isLoggable(Level.FINE)) {
|
||||
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
||||
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
||||
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks Format 1 (9x16) symbol slice bit-pattern.
|
||||
*/
|
||||
private void unpackFormat1(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||
boolean isTriplePlane, int colorPlane) {
|
||||
// Format 1 transmits 18 bytes:
|
||||
// Byte 0-1: contains column 0 for each of the 16 rows
|
||||
// Bytes 2-17: contains columns 1-8 for each of the 16 rows
|
||||
int planeMask = (colorPlane != 0) ? colorPlane : (isTriplePlane ? 7 : 1);
|
||||
|
||||
for (int row = 0; row < 16 && row < height; row++) {
|
||||
// Column 0 bit from byte 0 or byte 1
|
||||
int b0 = (row < 8) ? (src[srcOff] & 0xFF) : (src[srcOff + 1] & 0xFF);
|
||||
int bitShift0 = 7 - (row % 8);
|
||||
boolean bit0 = ((b0 >> bitShift0) & 1) != 0;
|
||||
|
||||
int dstIdx0 = row * width;
|
||||
if (dstIdx0 < dst.length) {
|
||||
if (!isTriplePlane) {
|
||||
dst[dstIdx0] = bit0 ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[dstIdx0] = bit0 ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit0) {
|
||||
dst[dstIdx0] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[dstIdx0] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Columns 1..8 from bytes 2..17
|
||||
if (srcOff + 2 + row < src.length) {
|
||||
int rowByte = src[srcOff + 2 + row] & 0xFF;
|
||||
for (int col = 1; col < 9 && col < width; col++) {
|
||||
int dstIdx = row * width + col;
|
||||
if (dstIdx < dst.length) {
|
||||
boolean bit = ((rowByte >> (8 - col)) & 1) != 0;
|
||||
if (!isTriplePlane) {
|
||||
dst[dstIdx] = bit ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[dstIdx] = bit ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit) {
|
||||
dst[dstIdx] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[dstIdx] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks Format 3 variable dimension bit-pattern.
|
||||
*/
|
||||
private void unpackFormat3(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||
boolean isTriplePlane, int colorPlane) {
|
||||
int totalPixels = width * height;
|
||||
for (int i = 0; i < totalPixels; i++) {
|
||||
int byteIndex = srcOff + (i / 8);
|
||||
if (byteIndex >= src.length) break;
|
||||
int bitIndex = 7 - (i % 8);
|
||||
boolean bit = ((src[byteIndex] >> bitIndex) & 1) != 0;
|
||||
|
||||
if (!isTriplePlane) {
|
||||
dst[i] = bit ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[i] = bit ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit) {
|
||||
dst[i] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[i] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
||||
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
||||
*/
|
||||
public class ProgramSymbolSet {
|
||||
|
||||
public static final int NUM_SLOTS = 191; // Code points 0x40 - 0xFE (0..190)
|
||||
|
||||
private int lcid = 0;
|
||||
private final boolean isTriplePlane;
|
||||
private final SymbolSlot[] slots = new SymbolSlot[NUM_SLOTS];
|
||||
|
||||
public ProgramSymbolSet(boolean isTriplePlane) {
|
||||
this.isTriplePlane = isTriplePlane;
|
||||
}
|
||||
|
||||
public int getLcid() {
|
||||
return lcid;
|
||||
}
|
||||
|
||||
public void setLcid(int lcid) {
|
||||
this.lcid = lcid;
|
||||
}
|
||||
|
||||
public boolean isTriplePlane() {
|
||||
return isTriplePlane;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
for (int i = 0; i < NUM_SLOTS; i++) {
|
||||
slots[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSlot(int index) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
slots[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSlot(int index, SymbolSlot slot) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
slots[index] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
public SymbolSlot getSlot(int index) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
return slots[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single custom symbol bitmap.
|
||||
*/
|
||||
public static class SymbolSlot {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
||||
private final boolean isTriplePlane;
|
||||
private int[] cachedRgbArray;
|
||||
private int cachedFgRgb = -1;
|
||||
private int cachedBgRgb = -1;
|
||||
private java.awt.image.BufferedImage cachedImage;
|
||||
private java.awt.image.BufferedImage cachedScaledImage;
|
||||
private int cachedTargetW = 0;
|
||||
private int cachedTargetH = 0;
|
||||
private int cachedScaledFgRgb = -1;
|
||||
private int cachedScaledBgRgb = -1;
|
||||
|
||||
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
||||
this.width = width > 0 ? width : 9;
|
||||
this.height = height > 0 ? height : 16;
|
||||
this.pixelData = pixelData;
|
||||
this.isTriplePlane = isTriplePlane;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public byte[] getPixelData() {
|
||||
return pixelData;
|
||||
}
|
||||
|
||||
public boolean isTriplePlane() {
|
||||
return isTriplePlane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
||||
* Enables unscaled 1:1 hardware blitting in Java2D.
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
if (targetW <= 0 || targetH <= 0) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
}
|
||||
if (targetW == width && targetH == height) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
}
|
||||
if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH
|
||||
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
|
||||
return cachedScaledImage;
|
||||
}
|
||||
int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
|
||||
|
||||
for (int dy = 0; dy < targetH; dy++) {
|
||||
int sy = dy * height / targetH;
|
||||
int srcRowOffset = sy * width;
|
||||
int dstRowOffset = dy * targetW;
|
||||
for (int dx = 0; dx < targetW; dx++) {
|
||||
int sx = dx * width / targetW;
|
||||
dstRgb[dstRowOffset + dx] = srcRgb[srcRowOffset + sx];
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedScaledImage = scaled;
|
||||
this.cachedTargetW = targetW;
|
||||
this.cachedTargetH = targetH;
|
||||
this.cachedScaledFgRgb = fgArgb;
|
||||
this.cachedScaledBgRgb = bgArgb;
|
||||
return scaled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the cached BufferedImage for this symbol glyph.
|
||||
* Eliminates per-cell heap allocations during high frame rate rendering.
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) {
|
||||
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||
return cachedImage;
|
||||
}
|
||||
int[] rgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
|
||||
this.cachedImage = img;
|
||||
this.cachedFgRgb = fgArgb;
|
||||
this.cachedBgRgb = bgArgb;
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the 32-bit ARGB pixel array for this symbol.
|
||||
* The returned array has length (width * height).
|
||||
*/
|
||||
public synchronized int[] getRgbPixels(int fgArgb, int bgArgb) {
|
||||
if (cachedRgbArray != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||
return cachedRgbArray;
|
||||
}
|
||||
|
||||
int[] rgbArray = new int[width * height];
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int idx = y * width + x;
|
||||
int val = (pixelData != null && idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
|
||||
|
||||
if (val == 0) {
|
||||
rgbArray[idx] = bgArgb;
|
||||
} else if (!isTriplePlane) {
|
||||
rgbArray[idx] = fgArgb;
|
||||
} else {
|
||||
// Triple-Plane RGB composite:
|
||||
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||
int r = (val & 0x01) != 0 ? 255 : 0;
|
||||
int g = (val & 0x02) != 0 ? 255 : 0;
|
||||
int b = (val & 0x04) != 0 ? 255 : 0;
|
||||
rgbArray[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedRgbArray = rgbArray;
|
||||
this.cachedFgRgb = fgArgb;
|
||||
this.cachedBgRgb = bgArgb;
|
||||
return rgbArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,776 @@
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Handles keyboard input and generates 3270 outbound data streams.
|
||||
* Manages cursor positioning, character entry, and AID key handling.
|
||||
*/
|
||||
public class InputProcessor {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InputProcessor.class.getName());
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private final EbcdicTranslator translator;
|
||||
private final TelnetFSM fsm;
|
||||
private int lastAid = AID_NO;
|
||||
private boolean keyboardLocked;
|
||||
private boolean insertMode;
|
||||
|
||||
public InputProcessor(ScreenBuffer screen, EbcdicTranslator translator, TelnetFSM fsm) {
|
||||
this.screen = screen;
|
||||
this.translator = translator;
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane;
|
||||
private haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder;
|
||||
|
||||
public void setGraphicsPlane(haus.nightmare.lib3270j.graphics.GraphicsPlane gp) {
|
||||
this.graphicsPlane = gp;
|
||||
}
|
||||
|
||||
public void setGocaDecoder(haus.nightmare.lib3270j.graphics.GocaDecoder gd) {
|
||||
this.gocaDecoder = gd;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return gocaDecoder;
|
||||
}
|
||||
|
||||
public enum OiaStatus {
|
||||
NOT_CONNECTED("OFFLINE"),
|
||||
X_SYSTEM("X SYSTEM"),
|
||||
X_PROT("X PROT"),
|
||||
READY("READY");
|
||||
|
||||
private final String label;
|
||||
OiaStatus(String label) { this.label = label; }
|
||||
public String getLabel() { return label; }
|
||||
}
|
||||
|
||||
public OiaStatus getOiaStatus() {
|
||||
if (fsm == null || fsm.getConnectionState() == null || !fsm.getConnectionState().isConnected()) {
|
||||
return OiaStatus.NOT_CONNECTED;
|
||||
}
|
||||
if (keyboardLocked) {
|
||||
return OiaStatus.X_SYSTEM;
|
||||
}
|
||||
return OiaStatus.READY;
|
||||
}
|
||||
|
||||
public interface LockStateListener {
|
||||
void onLockStateChanged(boolean locked);
|
||||
}
|
||||
|
||||
private LockStateListener lockStateListener;
|
||||
|
||||
public void setLockStateListener(LockStateListener listener) {
|
||||
this.lockStateListener = listener;
|
||||
}
|
||||
|
||||
public boolean isKeyboardLocked() { return keyboardLocked; }
|
||||
public void setKeyboardLocked(boolean locked) {
|
||||
if (this.keyboardLocked != locked) {
|
||||
this.keyboardLocked = locked;
|
||||
if (lockStateListener != null) {
|
||||
lockStateListener.onLockStateChanged(locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
public boolean isInsertMode() { return insertMode; }
|
||||
public void setInsertMode(boolean insert) { this.insertMode = insert; }
|
||||
|
||||
/**
|
||||
* Enter a character at the current cursor position.
|
||||
*/
|
||||
public void typeCharacter(char ch) {
|
||||
if (keyboardLocked) return;
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
int baddr = screen.getCursorAddress();
|
||||
baddr = ((baddr % size) + size) % size;
|
||||
|
||||
// Check if cursor is at a field attribute or in a protected field
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
if (ea.isFieldAttribute()) {
|
||||
// Move to next position
|
||||
baddr = (baddr + 1) % size;
|
||||
ea = screen.getCell(baddr);
|
||||
}
|
||||
|
||||
byte faVal = screen.getFieldAttributeAt(baddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
// Protected field — can't type here
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate character to EBCDIC
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
if (ebc < 0) return;
|
||||
|
||||
if (insertMode) {
|
||||
// Insert mode: shift characters right from cursor to end of field
|
||||
// Find end of field
|
||||
int endAddr = baddr;
|
||||
int count = 0;
|
||||
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
|
||||
endAddr = screen.incrementAddress(endAddr);
|
||||
count++;
|
||||
if (endAddr == baddr) break; // wrapped around (unformatted)
|
||||
}
|
||||
// Check if last position is non-null (field overflow)
|
||||
if (screen.getCell(endAddr).ec != 0) {
|
||||
// Field overflow — can't insert
|
||||
return;
|
||||
}
|
||||
// Shift right from endAddr-1 down to baddr
|
||||
int dst = endAddr;
|
||||
int shiftCount = 0;
|
||||
while (dst != baddr && shiftCount < size) {
|
||||
int src = screen.decrementAddress(dst);
|
||||
screen.getCell(dst).ec = screen.getCell(src).ec;
|
||||
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
|
||||
dst = src;
|
||||
shiftCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Write character — preserve existing fg/bg/gr/cs attributes
|
||||
// so the character inherits the field's color scheme
|
||||
ea = screen.getCell(baddr);
|
||||
ea.ec = (byte) ebc;
|
||||
ea.ucs4 = ch;
|
||||
|
||||
// Set MDT on field attribute
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
ExtendedAttribute faEa = screen.getCell(faAddr);
|
||||
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
|
||||
}
|
||||
|
||||
// Advance cursor
|
||||
int startAdvance = baddr;
|
||||
baddr = (baddr + 1) % size;
|
||||
int advCount = 0;
|
||||
// Skip over field attributes safely
|
||||
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) {
|
||||
baddr = (baddr + 1) % size;
|
||||
advCount++;
|
||||
}
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
|
||||
*/
|
||||
public void sendAid(int aidCode) {
|
||||
System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
|
||||
if (keyboardLocked && aidCode != AID_CLEAR) {
|
||||
System.err.println("Keyboard locked, dropping AID");
|
||||
return;
|
||||
}
|
||||
|
||||
lastAid = aidCode;
|
||||
setKeyboardLocked(true);
|
||||
|
||||
if (aidCode == AID_CLEAR) {
|
||||
screen.clear();
|
||||
screen.markAllChanged();
|
||||
if (graphicsPlane != null) {
|
||||
graphicsPlane.clear();
|
||||
}
|
||||
// Send just the AID
|
||||
byte[] data = new byte[] { (byte) aidCode };
|
||||
sendAidResponse(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||
// SSCP-LU Mode (e.g. VM line mode / CP console before conmode 3270):
|
||||
// Per RFC 2355: Send raw EBCDIC line data without AID or 3270 cursor address.
|
||||
int cols = screen.getCols();
|
||||
int curAddr = screen.getCursorAddress();
|
||||
int row = curAddr / cols;
|
||||
int rowStart = row * cols;
|
||||
int rowEnd = rowStart + cols;
|
||||
|
||||
// Find last non-null, non-blank character in the current row
|
||||
int lastChar = rowStart - 1;
|
||||
for (int i = rowEnd - 1; i >= rowStart; i--) {
|
||||
int ec = screen.getCell(i).ec & 0xFF;
|
||||
if (ec != 0x00 && ec != 0x40) {
|
||||
lastChar = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ByteArrayOutputStream sscpData = new ByteArrayOutputStream();
|
||||
for (int i = rowStart; i <= lastChar; i++) {
|
||||
int ec = screen.getCell(i).ec & 0xFF;
|
||||
sscpData.write(ec != 0x00 ? ec : 0x40);
|
||||
}
|
||||
|
||||
fsm.sendSscpLuData(sscpData.toByteArray());
|
||||
|
||||
// Advance cursor to beginning of next row
|
||||
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
||||
screen.setCursorAddress(nextRowAddr);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
setKeyboardLocked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
||||
// PA keys: send AID + optional PID + cursor address only (no modified data)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
sendAidResponse(out.toByteArray());
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
out.write(aidCode);
|
||||
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
|
||||
if (screen.isFormatted()) {
|
||||
// Send modified fields with SBA
|
||||
// Per 3270 Data Stream Architecture: suppress NULLs (0x00) from field data
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
||||
int fieldStart = (i + 1) % size;
|
||||
|
||||
// Always send SBA and address of first character in field
|
||||
out.write(ORDER_SBA);
|
||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||
out.write(addr[0] & 0xFF);
|
||||
out.write(addr[1] & 0xFF);
|
||||
|
||||
// Send all non-null characters in field (suppressing 0x00)
|
||||
int pos = fieldStart;
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
int b = screen.getCell(pos).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
out.write(b);
|
||||
}
|
||||
pos = (pos + 1) % size;
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unformatted screen in 3270 mode:
|
||||
// Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
|
||||
// or we can just send everything up to the last non-null on the screen.
|
||||
// IBM spec: "all alphanumeric characters... Nulls are suppressed."
|
||||
// Actually, the simplest is to send everything, but suppress nulls.
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = 0; i < size; i++) {
|
||||
int b = screen.getCell(i).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
out.write(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
private void sendAidResponse(byte[] data) {
|
||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||
fsm.sendSscpLuData(data);
|
||||
} else if (fsm != null) {
|
||||
fsm.send3270Data(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G.
|
||||
*/
|
||||
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||
if (fsm == null || !fsm.getConnectionState().isFullSession()) {
|
||||
return;
|
||||
}
|
||||
if (isKeyboardLocked()) {
|
||||
return;
|
||||
}
|
||||
setKeyboardLocked(true);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
|
||||
int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
gx, gy, aidCode, true, isShift, isCtrl, pickedSeg, pickTag
|
||||
);
|
||||
System.err.println(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
|
||||
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl
|
||||
));
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
out.write(sf);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
} else {
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
}
|
||||
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
// ========== Cursor movement ==========
|
||||
|
||||
/**
|
||||
* Simulate a Text Light Pen selection.
|
||||
*/
|
||||
public boolean lightPenSelect(int address) {
|
||||
if (screen == null || !screen.isFormatted()) {
|
||||
System.out.println("LP: screen null or unformatted");
|
||||
return false;
|
||||
}
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return false;
|
||||
address = ((address % size) + size) % size;
|
||||
int faPos = screen.findFieldAttribute(address);
|
||||
if (faPos < 0) {
|
||||
System.out.println("LP: no FA found for addr=" + address);
|
||||
return false;
|
||||
}
|
||||
|
||||
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||
int fa = faCell.fa & 0xFF;
|
||||
System.out.println("LP: addr=" + address + " faPos=" + faPos + " fa=0x" + String.format("%02X", fa)
|
||||
+ " selectable=" + haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa));
|
||||
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int designatorPos = (faPos + 1) % size;
|
||||
ExtendedAttribute desCell = screen.getCell(designatorPos);
|
||||
int ebcdic = desCell.ec & 0xFF;
|
||||
char ascii = (char) desCell.ucs4;
|
||||
|
||||
screen.setCursorAddress(designatorPos);
|
||||
|
||||
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||
return true;
|
||||
} else if (ebcdic == 0x50 || ascii == '&') {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
return true;
|
||||
} else if (ebcdic == 0x6F || ascii == '?') {
|
||||
desCell.ec = (byte) 0x6E;
|
||||
desCell.ucs4 = '>';
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
} else if (ebcdic == 0x6E || ascii == '>') {
|
||||
desCell.ec = (byte) 0x6F;
|
||||
desCell.ucs4 = '?';
|
||||
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
} else {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void cursorUp() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr -= screen.getCols();
|
||||
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorDown() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr += screen.getCols();
|
||||
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorLeft() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = screen.decrementAddress(addr);
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorRight() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = screen.incrementAddress(addr);
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorHome() {
|
||||
if (screen.isFormatted()) {
|
||||
screen.setCursorAddress(screen.findNextUnprotected(0));
|
||||
} else {
|
||||
screen.setCursorAddress(0);
|
||||
}
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void tab() {
|
||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public int getLastAid() { return lastAid; }
|
||||
public void setLastAid(int aid) { this.lastAid = aid; }
|
||||
|
||||
public void setCursorAddress(int baddr) {
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void backTab() {
|
||||
if (!screen.isFormatted()) return;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = ((addr % size) + size) % size;
|
||||
int start = screen.decrementAddress(addr);
|
||||
addr = start;
|
||||
int count = 0;
|
||||
do {
|
||||
addr = screen.decrementAddress(addr);
|
||||
if (screen.getCell(addr).isFieldAttribute()) {
|
||||
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
||||
screen.setCursorAddress(screen.incrementAddress(addr));
|
||||
screen.updateDisplaySnapshot();
|
||||
return;
|
||||
}
|
||||
}
|
||||
count++;
|
||||
} while (addr != start && count < size);
|
||||
}
|
||||
|
||||
public void eraseEof() {
|
||||
if (!screen.isFormatted()) {
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = addr; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return;
|
||||
}
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
addr = ((addr % size) + size) % size;
|
||||
byte faVal = screen.getFieldAttributeAt(addr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
// Erase from cursor to end of field
|
||||
int count = 0;
|
||||
while (!screen.getCell(addr).isFieldAttribute() && count < size) {
|
||||
ExtendedAttribute ea = screen.getCell(addr);
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
addr = screen.incrementAddress(addr);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Set MDT
|
||||
int faAddr = screen.findFieldAttribute(screen.getCursorAddress());
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void deleteChar() {
|
||||
if (!screen.isFormatted()) return;
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
addr = ((addr % size) + size) % size;
|
||||
|
||||
// Cannot delete a field attribute
|
||||
if (screen.getCell(addr).isFieldAttribute()) return;
|
||||
|
||||
byte faVal = screen.getFieldAttributeAt(addr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
// Shift characters left within the field
|
||||
int shiftAddr = addr;
|
||||
int count = 0;
|
||||
while (count < size) {
|
||||
int next = screen.incrementAddress(shiftAddr);
|
||||
if (screen.getCell(next).isFieldAttribute()) {
|
||||
screen.getCell(shiftAddr).ec = 0;
|
||||
screen.getCell(shiftAddr).ucs4 = 0;
|
||||
break;
|
||||
}
|
||||
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
|
||||
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
|
||||
shiftAddr = next;
|
||||
count++;
|
||||
}
|
||||
|
||||
int faAddr = screen.findFieldAttribute(addr);
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void backspace() {
|
||||
int addr = screen.getCursorAddress();
|
||||
int prev = screen.decrementAddress(addr);
|
||||
if (screen.isFormatted()) {
|
||||
// If previous position is a field attribute or in protected field, do not back up over it
|
||||
if (screen.getCell(prev).isFieldAttribute()) {
|
||||
return;
|
||||
}
|
||||
byte faVal = screen.getFieldAttributeAt(prev);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
screen.setCursorAddress(prev);
|
||||
deleteChar();
|
||||
}
|
||||
|
||||
/** Erase All Unprotected fields (Erase Input key in 3270). */
|
||||
public void eraseInput() {
|
||||
if (keyboardLocked) return;
|
||||
screen.eraseAllUnprotected();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
||||
public void newline() {
|
||||
if (keyboardLocked) return;
|
||||
int cur = screen.getCursorAddress();
|
||||
int cols = screen.getCols();
|
||||
int rows = screen.getRows();
|
||||
int curRow = cur / cols;
|
||||
int nextRow = (curRow + 1) % rows;
|
||||
int target = nextRow * cols;
|
||||
if (screen.isFormatted()) {
|
||||
target = screen.findNextUnprotected(target == 0 ? (rows * cols - 1) : (target - 1));
|
||||
}
|
||||
screen.setCursorAddress(target);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Insert Duplicate (DUP) code and advance to next field. */
|
||||
public void dup() {
|
||||
if (keyboardLocked) return;
|
||||
int baddr = screen.getCursorAddress();
|
||||
byte faVal = screen.getFieldAttributeAt(baddr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.ec = (byte) FCORDER_DUP;
|
||||
ea.ucs4 = '*';
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
tab();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Insert Field Mark (FM) code. */
|
||||
public void fieldMark() {
|
||||
if (keyboardLocked) return;
|
||||
int baddr = screen.getCursorAddress();
|
||||
byte faVal = screen.getFieldAttributeAt(baddr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.ec = (byte) FCORDER_FM;
|
||||
ea.ucs4 = ';';
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
baddr = (baddr + 1) % size;
|
||||
while (screen.getCell(baddr).isFieldAttribute()) {
|
||||
baddr = (baddr + 1) % size;
|
||||
}
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Attention key (sends Telnet IP). */
|
||||
public void attn() {
|
||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
|
||||
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP };
|
||||
fsm.sendRecord(ip);
|
||||
}
|
||||
}
|
||||
|
||||
/** SysReq key. */
|
||||
public void sysReq() {
|
||||
reset();
|
||||
}
|
||||
|
||||
/** Reset (unlock keyboard, cancel insert mode). */
|
||||
public void reset() {
|
||||
setKeyboardLocked(false);
|
||||
insertMode = false;
|
||||
}
|
||||
|
||||
// ========== File Transfer Support ==========
|
||||
|
||||
/**
|
||||
* Send an AID code, bypassing the keyboard lock.
|
||||
* Used internally by the file transfer protocol to send Enter/PF keys
|
||||
* during a transfer when the keyboard is normally locked.
|
||||
*/
|
||||
public void sendAidForFT(int aidCode) {
|
||||
boolean wasLocked = keyboardLocked;
|
||||
keyboardLocked = false;
|
||||
sendAid(aidCode);
|
||||
keyboardLocked = wasLocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type a string into the current cursor position and press Enter.
|
||||
* Used by IND$FILE to inject the transfer command.
|
||||
* Equivalent to emulate_input() in x3270.
|
||||
*/
|
||||
public void emulateInput(String text) {
|
||||
// Type each character
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char ch = text.charAt(i);
|
||||
if (ch == '\n') {
|
||||
// Newline means press Enter
|
||||
sendAid(AID_ENTER);
|
||||
return;
|
||||
}
|
||||
typeCharacter(ch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase the current field and return its capacity.
|
||||
* Used by IND$FILE to prepare the field for the transfer command.
|
||||
* Equivalent to kybd_prime() in x3270.
|
||||
*
|
||||
* Returns:
|
||||
* - The number of characters that can fit in the field, or
|
||||
* - -1 if the keyboard is locked
|
||||
* - -2 if not in 3270 mode
|
||||
* - -3 if no input field found
|
||||
*/
|
||||
public int kybdPrime() {
|
||||
if (keyboardLocked) return -1;
|
||||
|
||||
if (!screen.isFormatted()) {
|
||||
// Unformatted screen — use entire buffer
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(0);
|
||||
for (int i = 0; i < size; i++) {
|
||||
screen.getCell(i).ec = 0;
|
||||
screen.getCell(i).ucs4 = 0;
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return size;
|
||||
}
|
||||
|
||||
// Find the field at cursor and erase it
|
||||
int curAddr = screen.getCursorAddress();
|
||||
byte faVal = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
// Try to find first unprotected field
|
||||
curAddr = screen.findNextUnprotected(curAddr);
|
||||
faVal = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
return -3; // No unprotected field
|
||||
}
|
||||
}
|
||||
|
||||
// Move to start of field
|
||||
int faAddr = screen.findFieldAttribute(curAddr);
|
||||
int fieldStart = screen.incrementAddress(faAddr);
|
||||
screen.setCursorAddress(fieldStart);
|
||||
|
||||
// Count field length and erase
|
||||
int fieldLen = 0;
|
||||
int pos = fieldStart;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int count = 0;
|
||||
while (!screen.getCell(pos).isFieldAttribute() && count < size) {
|
||||
screen.getCell(pos).ec = 0;
|
||||
screen.getCell(pos).ucs4 = 0;
|
||||
fieldLen++;
|
||||
pos = screen.incrementAddress(pos);
|
||||
count++;
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
|
||||
// Set MDT
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return fieldLen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a structured field response directly.
|
||||
* Used by DFT mode file transfer.
|
||||
*/
|
||||
public void sendStructuredFieldData(byte[] data) {
|
||||
fsm.send3270Data(data);
|
||||
}
|
||||
|
||||
/** Get a reference to the TelnetFSM for direct SF operations. */
|
||||
public TelnetFSM getTelnetFSM() {
|
||||
return fsm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package haus.nightmare.lib3270j.listener;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
|
||||
/**
|
||||
* Listener for connection state changes and errors.
|
||||
*/
|
||||
public interface ConnectionListener {
|
||||
/** Called when the connection state changes. */
|
||||
void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState);
|
||||
|
||||
/** Called when a connection error occurs. */
|
||||
void onConnectionError(String message);
|
||||
|
||||
/** Called when TN3270E negotiation completes. */
|
||||
default void onTN3270ENegotiated(String deviceType, String deviceName) {}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package haus.nightmare.lib3270j.listener;
|
||||
|
||||
/**
|
||||
* Listener for screen buffer changes.
|
||||
*/
|
||||
public interface ScreenUpdateListener {
|
||||
/** Called when the screen buffer has been updated. */
|
||||
void onScreenUpdated();
|
||||
|
||||
/** Called when the cursor position changes. */
|
||||
default void onCursorMoved(int oldAddress, int newAddress) {}
|
||||
|
||||
/** Called when the host sends a sound alarm (WCC bit). */
|
||||
default void onSoundAlarm() {}
|
||||
|
||||
/** Called when the screen size changes (erase/write vs erase/write alternate). */
|
||||
default void onScreenSizeChanged(int rows, int cols) {}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
/**
|
||||
* 3270 Data Stream protocol constants.
|
||||
* Derived from 3270ds.h in x3270.
|
||||
*/
|
||||
public final class DS3270Constants {
|
||||
|
||||
private DS3270Constants() {}
|
||||
|
||||
// ========== 3270 Commands ==========
|
||||
public static final int CMD_W = 0x01; // Write
|
||||
public static final int CMD_RB = 0x02; // Read Buffer
|
||||
public static final int CMD_NOP = 0x03; // No-Op
|
||||
public static final int CMD_EW = 0x05; // Erase/Write
|
||||
public static final int CMD_RM = 0x06; // Read Modified
|
||||
public static final int CMD_EWA = 0x0d; // Erase/Write Alternate
|
||||
public static final int CMD_RMA = 0x0e; // Read Modified All
|
||||
public static final int CMD_EAU = 0x0f; // Erase All Unprotected
|
||||
public static final int CMD_WSF = 0x11; // Write Structured Field
|
||||
|
||||
// SNA 3270 Commands
|
||||
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
|
||||
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
|
||||
public static final int SNA_CMD_EWA = 0x7e; // Erase/Write Alternate
|
||||
public static final int SNA_CMD_W = 0xf1; // Write
|
||||
public static final int SNA_CMD_RB = 0xf2; // Read Buffer
|
||||
public static final int SNA_CMD_WSF = 0xf3; // Write Structured Field
|
||||
public static final int SNA_CMD_EW = 0xf5; // Erase/Write
|
||||
public static final int SNA_CMD_RM = 0xf6; // Read Modified
|
||||
|
||||
// ========== 3270 Orders ==========
|
||||
public static final int ORDER_PT = 0x05; // Program Tab
|
||||
public static final int ORDER_GE = 0x08; // Graphic Escape
|
||||
public static final int ORDER_SBA = 0x11; // Set Buffer Address
|
||||
public static final int ORDER_EUA = 0x12; // Erase Unprotected to Address
|
||||
public static final int ORDER_IC = 0x13; // Insert Cursor
|
||||
public static final int ORDER_SF = 0x1d; // Start Field
|
||||
public static final int ORDER_SA = 0x28; // Set Attribute
|
||||
public static final int ORDER_SFE = 0x29; // Start Field Extended
|
||||
public static final int ORDER_YALE = 0x2b; // Yale sub command
|
||||
public static final int ORDER_MF = 0x2c; // Modify Field
|
||||
public static final int ORDER_RA = 0x3c; // Repeat to Address
|
||||
|
||||
// Format control orders
|
||||
public static final int FCORDER_NULL = 0x00;
|
||||
public static final int FCORDER_FF = 0x0c; // Form feed
|
||||
public static final int FCORDER_CR = 0x0d; // Carriage return
|
||||
public static final int FCORDER_SO = 0x0e; // Shift out (DBCS start)
|
||||
public static final int FCORDER_SI = 0x0f; // Shift in (DBCS end)
|
||||
public static final int FCORDER_NL = 0x15; // New line
|
||||
public static final int FCORDER_EM = 0x19; // End of medium
|
||||
public static final int FCORDER_LF = 0x25; // Line feed
|
||||
public static final int FCORDER_DUP = 0x1c; // Duplicate
|
||||
public static final int FCORDER_FM = 0x1e; // Field mark
|
||||
public static final int FCORDER_SUB = 0x3f; // Substitute
|
||||
public static final int FCORDER_EO = 0xff; // Eight ones
|
||||
|
||||
// ========== Field Attributes ==========
|
||||
public static final int FA_PRINTABLE = 0xc0;
|
||||
public static final int FA_PROTECT = 0x20; // Protected (1) / Unprotected (0)
|
||||
public static final int FA_NUMERIC = 0x10; // Numeric (1) / Alphanumeric (0)
|
||||
public static final int FA_INTENSITY = 0x0c; // Display/selector pen mask
|
||||
public static final int FA_INT_NORM_NSEL = 0x00; // Normal, non-detect
|
||||
public static final int FA_INT_NORM_SEL = 0x04; // Normal, detectable
|
||||
public static final int FA_INT_HIGH_SEL = 0x08; // Intensified, detectable
|
||||
public static final int FA_INT_ZERO_NSEL = 0x0c; // Non-display, non-detect
|
||||
public static final int FA_RESERVED = 0x02;
|
||||
public static final int FA_MODIFY = 0x01; // Modified
|
||||
|
||||
public static final int FA_MASK = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC | FA_INTENSITY | FA_MODIFY;
|
||||
|
||||
public static boolean faIsModified(int fa) { return (fa & FA_MODIFY) != 0; }
|
||||
public static boolean faIsNumeric(int fa) { return (fa & FA_NUMERIC) != 0; }
|
||||
public static boolean faIsProtected(int fa) { return (fa & FA_PROTECT) != 0; }
|
||||
public static boolean faIsSkip(int fa) { return (fa & FA_PROTECT) != 0 && (fa & FA_NUMERIC) != 0; }
|
||||
public static boolean faIsZero(int fa) { return (fa & FA_INTENSITY) == FA_INT_ZERO_NSEL; }
|
||||
public static boolean faIsHigh(int fa) { return (fa & FA_INTENSITY) == FA_INT_HIGH_SEL; }
|
||||
public static boolean faIsNormal(int fa) {
|
||||
return (fa & FA_INTENSITY) == FA_INT_NORM_NSEL || (fa & FA_INTENSITY) == FA_INT_NORM_SEL;
|
||||
}
|
||||
public static boolean faIsSelectable(int fa) {
|
||||
return (fa & FA_INTENSITY) == FA_INT_NORM_SEL || (fa & FA_INTENSITY) == FA_INT_HIGH_SEL;
|
||||
}
|
||||
|
||||
// ========== Extended Attributes ==========
|
||||
public static final int XA_ALL = 0x00;
|
||||
public static final int XA_3270 = 0xc0;
|
||||
public static final int XA_VALIDATION = 0xc1;
|
||||
public static final int XA_OUTLINING = 0xc2;
|
||||
public static final int XA_HIGHLIGHTING = 0x41;
|
||||
public static final int XA_FOREGROUND = 0x42;
|
||||
public static final int XA_CHARSET = 0x43;
|
||||
public static final int XA_BACKGROUND = 0x45;
|
||||
public static final int XA_TRANSPARENCY = 0x46;
|
||||
public static final int XA_INPUT_CONTROL = 0xfe;
|
||||
|
||||
// Highlighting values
|
||||
public static final int XAH_DEFAULT = 0x00;
|
||||
public static final int XAH_NORMAL = 0xf0;
|
||||
public static final int XAH_BLINK = 0xf1;
|
||||
public static final int XAH_REVERSE = 0xf2;
|
||||
public static final int XAH_UNDERSCORE = 0xf4;
|
||||
public static final int XAH_INTENSIFY = 0xf8;
|
||||
|
||||
// Default color
|
||||
public static final int XAC_DEFAULT = 0x00;
|
||||
|
||||
// Outlining values
|
||||
public static final int XAO_UNDERLINE = 0x01;
|
||||
public static final int XAO_RIGHT = 0x02;
|
||||
public static final int XAO_OVERLINE = 0x04;
|
||||
public static final int XAO_LEFT = 0x08;
|
||||
|
||||
// Validation values
|
||||
public static final int XAV_FILL = 0x04;
|
||||
public static final int XAV_ENTRY = 0x02;
|
||||
public static final int XAV_TRIGGER = 0x01;
|
||||
|
||||
// Transparency values
|
||||
public static final int XAT_DEFAULT = 0x00;
|
||||
public static final int XAT_OR = 0xf0;
|
||||
public static final int XAT_XOR = 0xf1;
|
||||
public static final int XAT_OPAQUE = 0xff;
|
||||
|
||||
// Input control
|
||||
public static final int XAI_DISABLED = 0x00;
|
||||
public static final int XAI_ENABLED = 0x01;
|
||||
|
||||
// ========== WCC (Write Control Character) ==========
|
||||
public static final int WCC_RESET_BIT = 0x40;
|
||||
public static final int WCC_START_PRINTER_BIT = 0x08;
|
||||
public static final int WCC_SOUND_ALARM_BIT = 0x04;
|
||||
public static final int WCC_KEYBOARD_RESTORE_BIT = 0x02;
|
||||
public static final int WCC_RESET_MDT_BIT = 0x01;
|
||||
|
||||
public static boolean wccReset(int wcc) { return (wcc & WCC_RESET_BIT) != 0; }
|
||||
public static boolean wccStartPrinter(int wcc) { return (wcc & WCC_START_PRINTER_BIT) != 0; }
|
||||
public static boolean wccSoundAlarm(int wcc) { return (wcc & WCC_SOUND_ALARM_BIT) != 0; }
|
||||
public static boolean wccKeyboardRestore(int wcc) { return (wcc & WCC_KEYBOARD_RESTORE_BIT) != 0; }
|
||||
public static boolean wccResetMDT(int wcc) { return (wcc & WCC_RESET_MDT_BIT) != 0; }
|
||||
|
||||
// ========== AID (Attention Identifier) ==========
|
||||
public static final int AID_NO = 0x60;
|
||||
public static final int AID_QREPLY = 0x61;
|
||||
public static final int AID_ENTER = 0x7d;
|
||||
public static final int AID_PF1 = 0xf1;
|
||||
public static final int AID_PF2 = 0xf2;
|
||||
public static final int AID_PF3 = 0xf3;
|
||||
public static final int AID_PF4 = 0xf4;
|
||||
public static final int AID_PF5 = 0xf5;
|
||||
public static final int AID_PF6 = 0xf6;
|
||||
public static final int AID_PF7 = 0xf7;
|
||||
public static final int AID_PF8 = 0xf8;
|
||||
public static final int AID_PF9 = 0xf9;
|
||||
public static final int AID_PF10 = 0x7a;
|
||||
public static final int AID_PF11 = 0x7b;
|
||||
public static final int AID_PF12 = 0x7c;
|
||||
public static final int AID_PF13 = 0xc1;
|
||||
public static final int AID_PF14 = 0xc2;
|
||||
public static final int AID_PF15 = 0xc3;
|
||||
public static final int AID_PF16 = 0xc4;
|
||||
public static final int AID_PF17 = 0xc5;
|
||||
public static final int AID_PF18 = 0xc6;
|
||||
public static final int AID_PF19 = 0xc7;
|
||||
public static final int AID_PF20 = 0xc8;
|
||||
public static final int AID_PF21 = 0xc9;
|
||||
public static final int AID_PF22 = 0x4a;
|
||||
public static final int AID_PF23 = 0x4b;
|
||||
public static final int AID_PF24 = 0x4c;
|
||||
public static final int AID_OICR = 0xe6;
|
||||
public static final int AID_MSR_MHS = 0xe7;
|
||||
public static final int AID_SELECT = 0x7e;
|
||||
public static final int AID_PA1 = 0x6c;
|
||||
public static final int AID_PA2 = 0x6e;
|
||||
public static final int AID_PA3 = 0x6b;
|
||||
public static final int AID_CLEAR = 0x6d;
|
||||
public static final int AID_SYSREQ = 0xf0;
|
||||
public static final int AID_SF = 0x88;
|
||||
|
||||
public static final int SFID_QREPLY = 0x81;
|
||||
|
||||
// ========== Structured Field IDs ==========
|
||||
public static final int SF_READ_PART = 0x01;
|
||||
public static final int SF_RP_QUERY = 0x02;
|
||||
public static final int SF_RP_QLIST = 0x03;
|
||||
public static final int SF_RPQ_LIST = 0x00;
|
||||
public static final int SF_RPQ_EQUIV = 0x40;
|
||||
public static final int SF_RPQ_ALL = 0x80;
|
||||
public static final int SF_ERASE_RESET = 0x03;
|
||||
public static final int SF_ER_DEFAULT = 0x00;
|
||||
public static final int SF_ER_ALT = 0x80;
|
||||
public static final int SF_SET_REPLY_MODE = 0x09;
|
||||
public static final int SF_SRM_FIELD = 0x00;
|
||||
public static final int SF_SRM_XFIELD = 0x01;
|
||||
public static final int SF_SRM_CHAR = 0x02;
|
||||
public static final int SF_CREATE_PART = 0x0c;
|
||||
public static final int SF_OUTBOUND_DS = 0x40;
|
||||
public static final int SF_TRANSFER_DATA = 0xd0;
|
||||
|
||||
// ========== Query Reply codes ==========
|
||||
public static final int QR_SUMMARY = 0x80;
|
||||
public static final int QR_USABLE_AREA = 0x81;
|
||||
public static final int QR_IMAGE = 0x82;
|
||||
public static final int QR_TEXT_PART = 0x83;
|
||||
public static final int QR_ALPHA_PART = 0x84;
|
||||
public static final int QR_CHARSETS = 0x85;
|
||||
public static final int QR_COLOR = 0x86;
|
||||
public static final int QR_HIGHLIGHTING = 0x87;
|
||||
public static final int QR_REPLY_MODES = 0x88;
|
||||
public static final int QR_SAVE_RESTORE = 0x8c;
|
||||
public static final int QR_DBCS_ASIA = 0x91;
|
||||
public static final int QR_DDM = 0x95;
|
||||
public static final int QR_TRANSPARENCY = 0x99;
|
||||
public static final int QR_RPQNAMES = 0xa1;
|
||||
public static final int QR_IMP_PART = 0xa6;
|
||||
public static final int QR_RPQ_NAMES = 0xa8;
|
||||
public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics
|
||||
public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes
|
||||
public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device
|
||||
public static final int QR_OEM_FMT = 0xb3; // OEM Format
|
||||
public static final int QR_GCOLOR = 0xb4; // Graphic Color Table
|
||||
public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets
|
||||
public static final int QR_NULL = 0xff;
|
||||
|
||||
// ========== Screen model sizes ==========
|
||||
public static final int MODEL_2_ROWS = 24;
|
||||
public static final int MODEL_2_COLS = 80;
|
||||
public static final int MODEL_3_ROWS = 32;
|
||||
public static final int MODEL_3_COLS = 80;
|
||||
public static final int MODEL_4_ROWS = 43;
|
||||
public static final int MODEL_4_COLS = 80;
|
||||
public static final int MODEL_5_ROWS = 27;
|
||||
public static final int MODEL_5_COLS = 132;
|
||||
|
||||
public static final int MAX_ROWS_COLS = 0x3fff;
|
||||
|
||||
// ========== Host colors ==========
|
||||
public static final int HOST_COLOR_NEUTRAL_BLACK = 0;
|
||||
public static final int HOST_COLOR_BLUE = 1;
|
||||
public static final int HOST_COLOR_RED = 2;
|
||||
public static final int HOST_COLOR_PINK = 3;
|
||||
public static final int HOST_COLOR_GREEN = 4;
|
||||
public static final int HOST_COLOR_TURQUOISE = 5;
|
||||
public static final int HOST_COLOR_YELLOW = 6;
|
||||
public static final int HOST_COLOR_NEUTRAL_WHITE = 7;
|
||||
public static final int HOST_COLOR_BLACK = 8;
|
||||
public static final int HOST_COLOR_DEEP_BLUE = 9;
|
||||
public static final int HOST_COLOR_ORANGE = 10;
|
||||
public static final int HOST_COLOR_PURPLE = 11;
|
||||
public static final int HOST_COLOR_PALE_GREEN = 12;
|
||||
public static final int HOST_COLOR_PALE_TURQUOISE = 13;
|
||||
public static final int HOST_COLOR_GREY = 14;
|
||||
public static final int HOST_COLOR_WHITE = 15;
|
||||
|
||||
// ========== Graphics rendition bits ==========
|
||||
public static final int GR_BLINK = 0x01;
|
||||
public static final int GR_REVERSE = 0x02;
|
||||
public static final int GR_UNDERLINE = 0x04;
|
||||
public static final int GR_INTENSIFY = 0x08;
|
||||
|
||||
// ========== Character set codes ==========
|
||||
public static final int CS_MASK = 0x03;
|
||||
public static final int CS_BASE = 0x00;
|
||||
public static final int CS_APL = 0x01;
|
||||
public static final int CS_LINEDRAW = 0x02;
|
||||
public static final int CS_DBCS = 0x03;
|
||||
public static final int CS_GE = 0x04;
|
||||
|
||||
// ========== BIND definitions ==========
|
||||
public static final int BIND_RU = 0x31;
|
||||
public static final int BIND_OFF_MAXRU_SEC = 10;
|
||||
public static final int BIND_OFF_MAXRU_PRI = 11;
|
||||
public static final int BIND_OFF_RD = 20;
|
||||
public static final int BIND_OFF_CD = 21;
|
||||
public static final int BIND_OFF_RA = 22;
|
||||
public static final int BIND_OFF_CA = 23;
|
||||
public static final int BIND_OFF_SSIZE = 24;
|
||||
public static final int BIND_OFF_PLU_NAME_LEN = 27;
|
||||
public static final int BIND_PLU_NAME_MAX = 8;
|
||||
public static final int BIND_OFF_PLU_NAME = 28;
|
||||
|
||||
// BIND dimension flags
|
||||
public static final int BIND_DIMS_PRESENT = 0x1;
|
||||
public static final int BIND_DIMS_ALT = 0x2;
|
||||
public static final int BIND_DIMS_VALID = 0x4;
|
||||
|
||||
// ========== EBCDIC common characters ==========
|
||||
public static final int EBC_NULL = 0x00;
|
||||
public static final int EBC_SPACE = 0x40;
|
||||
public static final int EBC_AMPERSAND = 0x50;
|
||||
public static final int EBC_MINUS = 0x60;
|
||||
public static final int EBC_PERIOD = 0x4b;
|
||||
public static final int EBC_COMMA = 0x6b;
|
||||
public static final int EBC_DUP = 0x1c;
|
||||
public static final int EBC_FM = 0x1e;
|
||||
public static final int EBC_FF = 0x0c;
|
||||
public static final int EBC_CR = 0x0d;
|
||||
public static final int EBC_NL = 0x15;
|
||||
public static final int EBC_EM = 0x19;
|
||||
public static final int EBC_SUB = 0x3f;
|
||||
public static final int EBC_EO = 0xff;
|
||||
|
||||
/**
|
||||
* 6-bit code table for 12-bit buffer address encoding.
|
||||
*/
|
||||
public static final int[] CODE_TABLE = {
|
||||
0x40, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
|
||||
0xC8, 0xC9, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
|
||||
0x50, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
|
||||
0xD8, 0xD9, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
|
||||
0x60, 0x61, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
|
||||
0xE8, 0xE9, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
|
||||
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7,
|
||||
0xF8, 0xF9, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode a 2-byte buffer address.
|
||||
* Handles both 12-bit and 14-bit addressing.
|
||||
*/
|
||||
public static int decodeAddress(int b1, int b2) {
|
||||
if ((b1 & 0xC0) == 0x00) {
|
||||
// 14-bit format
|
||||
return ((b1 & 0x3F) << 8) | b2;
|
||||
} else {
|
||||
// 12-bit format
|
||||
return ((b1 & 0x3F) << 6) | (b2 & 0x3F);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a buffer address into a 2-byte array.
|
||||
* Uses 14-bit format if screen > 4096 positions, otherwise 12-bit.
|
||||
*/
|
||||
public static byte[] encodeAddress(int addr, int rows, int cols) {
|
||||
byte[] result = new byte[2];
|
||||
if (rows * cols > 0x1000) {
|
||||
// 14-bit format
|
||||
result[0] = (byte) ((addr >> 8) & 0x3F);
|
||||
result[1] = (byte) (addr & 0xFF);
|
||||
} else {
|
||||
// 12-bit format
|
||||
result[0] = (byte) CODE_TABLE[(addr >> 6) & 0x3F];
|
||||
result[1] = (byte) CODE_TABLE[addr & 0x3F];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Check if a byte is a 3270 order. */
|
||||
public static boolean isOrder(int b) {
|
||||
switch (b) {
|
||||
case ORDER_PT:
|
||||
case ORDER_GE:
|
||||
case ORDER_SBA:
|
||||
case ORDER_EUA:
|
||||
case ORDER_IC:
|
||||
case ORDER_SF:
|
||||
case ORDER_SA:
|
||||
case ORDER_SFE:
|
||||
case ORDER_YALE:
|
||||
case ORDER_MF:
|
||||
case ORDER_RA:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get human-readable command name. */
|
||||
public static String commandName(int cmd) {
|
||||
switch (cmd) {
|
||||
case CMD_W: case SNA_CMD_W: return "Write";
|
||||
case CMD_EW: case SNA_CMD_EW: return "EraseWrite";
|
||||
case CMD_EWA: case SNA_CMD_EWA: return "EraseWriteAlternate";
|
||||
case CMD_RB: case SNA_CMD_RB: return "ReadBuffer";
|
||||
case CMD_RM: case SNA_CMD_RM: return "ReadModified";
|
||||
case CMD_RMA: case SNA_CMD_RMA: return "ReadModifiedAll";
|
||||
case CMD_EAU: case SNA_CMD_EAU: return "EraseAllUnprotected";
|
||||
case CMD_WSF: case SNA_CMD_WSF: return "WriteStructuredField";
|
||||
case CMD_NOP: return "NoOp";
|
||||
default: return String.format("Unknown(0x%02x)", cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
/**
|
||||
* TN3270E protocol constants per RFC 2355.
|
||||
* Derived from tn3270e.h in x3270.
|
||||
*/
|
||||
public final class TN3270EConstants {
|
||||
|
||||
private TN3270EConstants() {}
|
||||
|
||||
// Negotiation operations
|
||||
public static final int OP_ASSOCIATE = 0;
|
||||
public static final int OP_CONNECT = 1;
|
||||
public static final int OP_DEVICE_TYPE = 2;
|
||||
public static final int OP_FUNCTIONS = 3;
|
||||
public static final int OP_IS = 4;
|
||||
public static final int OP_REASON = 5;
|
||||
public static final int OP_REJECT = 6;
|
||||
public static final int OP_REQUEST = 7;
|
||||
public static final int OP_SEND = 8;
|
||||
|
||||
// Reason codes
|
||||
public static final int REASON_CONN_PARTNER = 0;
|
||||
public static final int REASON_DEVICE_IN_USE = 1;
|
||||
public static final int REASON_INV_ASSOCIATE = 2;
|
||||
public static final int REASON_INV_DEVICE_NAME = 3;
|
||||
public static final int REASON_INV_DEVICE_TYPE = 4;
|
||||
public static final int REASON_TYPE_NAME_ERROR = 5;
|
||||
public static final int REASON_UNKNOWN_ERROR = 6;
|
||||
public static final int REASON_UNSUPPORTED_REQ = 7;
|
||||
|
||||
// Function names
|
||||
public static final int FUNC_BIND_IMAGE = 0;
|
||||
public static final int FUNC_DATA_STREAM_CTL = 1;
|
||||
public static final int FUNC_RESPONSES = 2;
|
||||
public static final int FUNC_SCS_CTL_CODES = 3;
|
||||
public static final int FUNC_SYSREQ = 4;
|
||||
public static final int FUNC_CONTENTION_RESOLUTION = 5;
|
||||
public static final int FUNC_SNA_SENSE = 6;
|
||||
|
||||
// Data type names
|
||||
public static final int DT_3270_DATA = 0x00;
|
||||
public static final int DT_SCS_DATA = 0x01;
|
||||
public static final int DT_RESPONSE = 0x02;
|
||||
public static final int DT_BIND_IMAGE = 0x03;
|
||||
public static final int DT_UNBIND = 0x04;
|
||||
public static final int DT_NVT_DATA = 0x05;
|
||||
public static final int DT_REQUEST = 0x06;
|
||||
public static final int DT_SSCP_LU_DATA = 0x07;
|
||||
public static final int DT_PRINT_EOJ = 0x08;
|
||||
public static final int DT_BID = 0x09;
|
||||
|
||||
// Request flags
|
||||
public static final int RQF_ERR_COND_CLEARED = 0x00;
|
||||
public static final int RQF_SEND_DATA = 0x01;
|
||||
public static final int RQF_KEYBOARD_RESTORE = 0x02;
|
||||
public static final int RQF_SIGNAL = 0x04;
|
||||
|
||||
// Response flags (header)
|
||||
public static final int RSF_NO_RESPONSE = 0x00;
|
||||
public static final int RSF_ERROR_RESPONSE = 0x01;
|
||||
public static final int RSF_ALWAYS_RESPONSE = 0x02;
|
||||
|
||||
// Response flags (trailer)
|
||||
public static final int RSF_POSITIVE_RESPONSE = 0x00;
|
||||
public static final int RSF_NEGATIVE_RESPONSE = 0x01;
|
||||
public static final int RSF_SNA_SENSE = 0x02;
|
||||
|
||||
// Positive response data
|
||||
public static final int POS_DEVICE_END = 0x00;
|
||||
|
||||
// Negative response data
|
||||
public static final int NEG_COMMAND_REJECT = 0x00;
|
||||
public static final int NEG_INTERVENTION_REQUIRED = 0x01;
|
||||
public static final int NEG_OPERATION_CHECK = 0x02;
|
||||
public static final int NEG_COMPONENT_DISCONNECTED = 0x03;
|
||||
|
||||
// TN3270E header size
|
||||
public static final int EH_SIZE = 5;
|
||||
|
||||
// UNBIND types
|
||||
public static final int UNBIND_NORMAL = 0x01;
|
||||
public static final int UNBIND_BIND_FORTHCOMING = 0x02;
|
||||
public static final int UNBIND_VR_INOPERATIVE = 0x07;
|
||||
public static final int UNBIND_RX_INOPERATIVE = 0x08;
|
||||
public static final int UNBIND_HRESET = 0x09;
|
||||
public static final int UNBIND_SSCP_GONE = 0x0a;
|
||||
public static final int UNBIND_VR_DEACTIVATED = 0x0b;
|
||||
public static final int UNBIND_LU_FAILURE_PERM = 0x0c;
|
||||
public static final int UNBIND_LU_FAILURE_TEMP = 0x0e;
|
||||
public static final int UNBIND_CLEANUP = 0x0f;
|
||||
public static final int UNBIND_BAD_SENSE = 0xfe;
|
||||
|
||||
// Name lookups for tracing
|
||||
|
||||
private static final String[] REASON_NAMES = {
|
||||
"CONN-PARTNER", "DEVICE-IN-USE", "INV-ASSOCIATE", "INV-NAME",
|
||||
"INV-DEVICE-TYPE", "TYPE-NAME-ERROR", "UNKNOWN-ERROR", "UNSUPPORTED-REQ"
|
||||
};
|
||||
|
||||
private static final String[] FUNCTION_NAMES = {
|
||||
"BIND-IMAGE", "DATA-STREAM-CTL", "RESPONSES", "SCS-CTL-CODES",
|
||||
"SYSREQ", "CONTENTION-RESOLUTION", "SNA-SENSE"
|
||||
};
|
||||
|
||||
private static final String[] DATA_TYPE_NAMES = {
|
||||
"3270-DATA", "SCS-DATA", "RESPONSE", "BIND-IMAGE", "UNBIND",
|
||||
"NVT-DATA", "REQUEST", "SSCP-LU-DATA", "PRINT-EOJ", "BID"
|
||||
};
|
||||
|
||||
private static final String[] HRSP_FLAG_NAMES = {
|
||||
"NO-RESPONSE", "ERROR-RESPONSE", "ALWAYS-RESPONSE"
|
||||
};
|
||||
|
||||
public static String reasonName(int code) {
|
||||
return code >= 0 && code < REASON_NAMES.length ? REASON_NAMES[code] : "??";
|
||||
}
|
||||
|
||||
public static String functionName(int code) {
|
||||
return code >= 0 && code < FUNCTION_NAMES.length ? FUNCTION_NAMES[code] : "??";
|
||||
}
|
||||
|
||||
public static String dataTypeName(int code) {
|
||||
return code >= 0 && code < DATA_TYPE_NAMES.length ? DATA_TYPE_NAMES[code] : "??";
|
||||
}
|
||||
|
||||
public static String responseHeaderFlagName(int code) {
|
||||
return code >= 0 && code < HRSP_FLAG_NAMES.length ? HRSP_FLAG_NAMES[code] : "??";
|
||||
}
|
||||
|
||||
/** Format a function set as a human-readable string. */
|
||||
public static String functionNames(boolean[] funcs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < funcs.length && i <= FUNC_SNA_SENSE; i++) {
|
||||
if (funcs[i]) {
|
||||
if (sb.length() > 0) sb.append(", ");
|
||||
sb.append(functionName(i));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
/**
|
||||
* Telnet protocol constants from RFC 854 and extensions.
|
||||
* Derived from arpa_telnet.h in x3270.
|
||||
*/
|
||||
public final class TelnetConstants {
|
||||
|
||||
private TelnetConstants() {}
|
||||
|
||||
// Telnet commands
|
||||
public static final int IAC = 255; // Interpret As Command
|
||||
public static final int DONT = 254; // You are not to use option
|
||||
public static final int DO = 253; // Please, you use option
|
||||
public static final int WONT = 252; // I won't use option
|
||||
public static final int WILL = 251; // I will use option
|
||||
public static final int SB = 250; // Interpret as subnegotiation
|
||||
public static final int GA = 249; // You may reverse the line
|
||||
public static final int EL = 248; // Erase the current line
|
||||
public static final int EC = 247; // Erase the current character
|
||||
public static final int AYT = 246; // Are you there
|
||||
public static final int AO = 245; // Abort output
|
||||
public static final int IP = 244; // Interrupt process
|
||||
public static final int BREAK = 243; // Break
|
||||
public static final int DM = 242; // Data mark
|
||||
public static final int NOP = 241; // No operation
|
||||
public static final int SE = 240; // End sub negotiation
|
||||
public static final int EOR = 239; // End of record
|
||||
public static final int SUSP = 237; // Suspend process
|
||||
public static final int xEOF = 236; // End of file
|
||||
|
||||
// Telnet options
|
||||
public static final int TELOPT_BINARY = 0; // 8-bit data path
|
||||
public static final int TELOPT_ECHO = 1; // Echo
|
||||
public static final int TELOPT_RCP = 2; // Prepare to reconnect
|
||||
public static final int TELOPT_SGA = 3; // Suppress go ahead
|
||||
public static final int TELOPT_NAMS = 4; // Approximate message size
|
||||
public static final int TELOPT_STATUS = 5; // Give status
|
||||
public static final int TELOPT_TM = 6; // Timing mark
|
||||
public static final int TELOPT_TTYPE = 24; // Terminal type
|
||||
public static final int TELOPT_EOR = 25; // End of record
|
||||
public static final int TELOPT_NAWS = 31; // Window size
|
||||
public static final int TELOPT_TSPEED = 32; // Terminal speed
|
||||
public static final int TELOPT_LFLOW = 33; // Remote flow control
|
||||
public static final int TELOPT_LINEMODE = 34; // Linemode option
|
||||
public static final int TELOPT_XDISPLOC = 35; // X Display Location
|
||||
public static final int TELOPT_OLD_ENVIRON = 36; // Old environment variables
|
||||
public static final int TELOPT_AUTHENTICATION = 37; // Authenticate
|
||||
public static final int TELOPT_ENCRYPT = 38; // Encryption option
|
||||
public static final int TELOPT_NEW_ENVIRON = 39; // New environment variables
|
||||
public static final int TELOPT_TN3270E = 40; // Extended 3270 regime
|
||||
public static final int TELOPT_STARTTLS = 46; // Start TLS
|
||||
public static final int TELOPT_EXOPL = 255; // Extended options list
|
||||
|
||||
// Sub-option qualifiers
|
||||
public static final int TELQUAL_IS = 0; // Option is...
|
||||
public static final int TELQUAL_SEND = 1; // Send option
|
||||
public static final int TELQUAL_INFO = 2; // Info
|
||||
|
||||
// New-environ sub-option objects
|
||||
public static final int TELOBJ_VAR = 0;
|
||||
public static final int TELOBJ_VALUE = 1;
|
||||
public static final int TELOBJ_ESC = 2;
|
||||
public static final int TELOBJ_USERVAR = 3;
|
||||
|
||||
// STARTTLS sub-option
|
||||
public static final int TLS_FOLLOWS = 1;
|
||||
|
||||
// Standard ports
|
||||
public static final int TELNET_PORT = 23;
|
||||
public static final int TELNETS_PORT = 992;
|
||||
|
||||
/** Telnet option name lookup for tracing. */
|
||||
public static String optionName(int opt) {
|
||||
switch (opt) {
|
||||
case TELOPT_BINARY: return "BINARY";
|
||||
case TELOPT_ECHO: return "ECHO";
|
||||
case TELOPT_SGA: return "SGA";
|
||||
case TELOPT_TM: return "TIMING-MARK";
|
||||
case TELOPT_TTYPE: return "TTYPE";
|
||||
case TELOPT_EOR: return "EOR";
|
||||
case TELOPT_NAWS: return "NAWS";
|
||||
case TELOPT_NEW_ENVIRON: return "NEW-ENVIRON";
|
||||
case TELOPT_TN3270E: return "TN3270E";
|
||||
case TELOPT_STARTTLS: return "STARTTLS";
|
||||
case TELOPT_TSPEED: return "TSPEED";
|
||||
case TELOPT_LFLOW: return "LFLOW";
|
||||
case TELOPT_LINEMODE: return "LINEMODE";
|
||||
case TELOPT_XDISPLOC: return "XDISPLOC";
|
||||
case TELOPT_OLD_ENVIRON: return "OLD-ENVIRON";
|
||||
default: return "OPT-" + opt;
|
||||
}
|
||||
}
|
||||
|
||||
/** Telnet command name lookup for tracing. */
|
||||
public static String commandName(int cmd) {
|
||||
switch (cmd) {
|
||||
case IAC: return "IAC";
|
||||
case DONT: return "DONT";
|
||||
case DO: return "DO";
|
||||
case WONT: return "WONT";
|
||||
case WILL: return "WILL";
|
||||
case SB: return "SB";
|
||||
case GA: return "GA";
|
||||
case EL: return "EL";
|
||||
case EC: return "EC";
|
||||
case AYT: return "AYT";
|
||||
case AO: return "AO";
|
||||
case IP: return "IP";
|
||||
case BREAK: return "BRK";
|
||||
case DM: return "DMARK";
|
||||
case NOP: return "NOP";
|
||||
case SE: return "SE";
|
||||
case EOR: return "EOR";
|
||||
default: return "CMD-" + cmd;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
/**
|
||||
* Extended attribute structure for a single screen buffer position.
|
||||
* Mirrors struct ea from globals.h in x3270.
|
||||
*/
|
||||
public class ExtendedAttribute {
|
||||
|
||||
/** EBCDIC character code at this position. */
|
||||
public byte ec;
|
||||
|
||||
/** Field attribute byte (non-zero if this position IS a field attribute). */
|
||||
public byte fa;
|
||||
|
||||
/** Foreground color (0x00 for default, or 0xf0-0xff for explicit). */
|
||||
public byte fg;
|
||||
|
||||
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */
|
||||
public byte bg;
|
||||
|
||||
/**
|
||||
* Graphics rendition bits.
|
||||
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
|
||||
*/
|
||||
public byte gr;
|
||||
|
||||
/** Character set (CS_BASE=0, CS_APL=1, CS_LINEDRAW=2, CS_DBCS=3; CS_GE=0x04 flag). */
|
||||
public byte cs;
|
||||
|
||||
/** Input control (DBCS). */
|
||||
public byte ic;
|
||||
|
||||
/** DBCS state. */
|
||||
public byte db;
|
||||
|
||||
/**
|
||||
* Unicode character for display (set by translation from ec, or directly in NVT mode).
|
||||
*/
|
||||
public char ucs4;
|
||||
|
||||
/** Clear all attributes. */
|
||||
public void clear() {
|
||||
ec = 0;
|
||||
fa = 0;
|
||||
fg = 0;
|
||||
bg = 0;
|
||||
gr = 0;
|
||||
cs = 0;
|
||||
ic = 0;
|
||||
db = 0;
|
||||
ucs4 = 0;
|
||||
}
|
||||
|
||||
/** Copy all values from another ExtendedAttribute. */
|
||||
public void copyFrom(ExtendedAttribute other) {
|
||||
this.ec = other.ec;
|
||||
this.fa = other.fa;
|
||||
this.fg = other.fg;
|
||||
this.bg = other.bg;
|
||||
this.gr = other.gr;
|
||||
this.cs = other.cs;
|
||||
this.ic = other.ic;
|
||||
this.db = other.db;
|
||||
this.ucs4 = other.ucs4;
|
||||
}
|
||||
|
||||
/** Check if this position is a field attribute. */
|
||||
public boolean isFieldAttribute() {
|
||||
return fa != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("EA[ec=%02x fa=%02x fg=%02x bg=%02x gr=%02x cs=%02x u=%04x]",
|
||||
ec & 0xFF, fa & 0xFF, fg & 0xFF, bg & 0xFF, gr & 0xFF, cs & 0xFF, (int) ucs4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* The 3270 screen buffer.
|
||||
* Manages the display buffer, cursor, field attributes, and screen dimensions.
|
||||
* Equivalent to ea_buf[] / aea_buf[] and related state in ctlr.c.
|
||||
*/
|
||||
public class ScreenBuffer {
|
||||
|
||||
private ExtendedAttribute[] buffer; // Main screen buffer
|
||||
private ExtendedAttribute[] altBuffer; // Alternate screen buffer
|
||||
private final ExtendedAttribute defaultFA; // Default field attribute (ea_buf[-1])
|
||||
|
||||
private int maxRows, maxCols; // Maximum (alternate) dimensions
|
||||
private int defRows, defCols; // Default dimensions (24x80)
|
||||
private int altRows, altCols; // Alternate dimensions
|
||||
private int rows, cols; // Current dimensions
|
||||
private int cursorAddress;
|
||||
private int bufferAddress;
|
||||
private boolean screenAlt; // Using alternate screen?
|
||||
private boolean formatted; // Screen has at least one field attribute?
|
||||
private byte replyMode = SF_SRM_FIELD;
|
||||
|
||||
private int activePartition = 0; // 0 = implicit partition
|
||||
private boolean explicitPartitionActive = false;
|
||||
|
||||
// Change tracking
|
||||
private boolean screenChanged;
|
||||
private int firstChanged = -1;
|
||||
private int lastChanged = -1;
|
||||
|
||||
// Default attribute values
|
||||
private byte defaultFg = 0x00;
|
||||
private byte defaultBg = 0x00;
|
||||
private byte defaultGr = 0x00;
|
||||
private byte defaultCs = 0x00;
|
||||
private byte defaultIc = 0x00;
|
||||
private final EbcdicTranslator translator;
|
||||
private final Object renderLock = new Object();
|
||||
|
||||
public Object getRenderLock() {
|
||||
return renderLock;
|
||||
}
|
||||
|
||||
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
|
||||
this.translator = translator;
|
||||
this.defRows = MODEL_2_ROWS;
|
||||
this.defCols = MODEL_2_COLS;
|
||||
this.altRows = model.getAlternateRows();
|
||||
this.altCols = model.getAlternateCols();
|
||||
this.maxRows = altRows;
|
||||
this.maxCols = altCols;
|
||||
this.rows = defRows;
|
||||
this.cols = defCols;
|
||||
|
||||
// Default field attribute (like ea_buf[-1])
|
||||
defaultFA = new ExtendedAttribute();
|
||||
defaultFA.fa = (byte) (FA_PRINTABLE | FA_MODIFY);
|
||||
defaultFA.ic = 1;
|
||||
|
||||
allocateBuffers();
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
private void allocateBuffers() {
|
||||
int size = maxRows * maxCols;
|
||||
buffer = new ExtendedAttribute[size];
|
||||
altBuffer = new ExtendedAttribute[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
buffer[i] = new ExtendedAttribute();
|
||||
altBuffer[i] = new ExtendedAttribute();
|
||||
}
|
||||
cursorAddress = 0;
|
||||
bufferAddress = 0;
|
||||
}
|
||||
|
||||
/** Get the current screen buffer. */
|
||||
public ExtendedAttribute[] getBuffer() { return buffer; }
|
||||
|
||||
/** Get a cell at the given buffer address. */
|
||||
public ExtendedAttribute getCell(int addr) {
|
||||
if (addr < 0 || addr >= rows * cols) return defaultFA;
|
||||
return buffer[addr];
|
||||
}
|
||||
|
||||
private ExtendedAttribute[] displaySnapshot;
|
||||
private int displayRows;
|
||||
private int displayCols;
|
||||
private int displayCursorAddress;
|
||||
|
||||
/**
|
||||
* Atomically creates a snapshot of the current presentation buffer for tear-free rendering.
|
||||
* Takes ~2 microseconds and eliminates mutual thread contention with the UI thread.
|
||||
*/
|
||||
public synchronized void updateDisplaySnapshot() {
|
||||
int size = rows * cols;
|
||||
if (displaySnapshot == null || displaySnapshot.length < size) {
|
||||
displaySnapshot = new ExtendedAttribute[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
displaySnapshot[i] = new ExtendedAttribute();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < size; i++) {
|
||||
displaySnapshot[i].copyFrom(buffer[i]);
|
||||
}
|
||||
this.displayRows = rows;
|
||||
this.displayCols = cols;
|
||||
this.displayCursorAddress = cursorAddress;
|
||||
}
|
||||
|
||||
public synchronized ExtendedAttribute getDisplayCell(int addr) {
|
||||
if (displaySnapshot == null || addr < 0 || addr >= displayRows * displayCols) {
|
||||
return getCell(addr);
|
||||
}
|
||||
return displaySnapshot[addr];
|
||||
}
|
||||
|
||||
public synchronized int getDisplayRows() {
|
||||
return displayRows > 0 ? displayRows : rows;
|
||||
}
|
||||
|
||||
public synchronized int getDisplayCols() {
|
||||
return displayCols > 0 ? displayCols : cols;
|
||||
}
|
||||
|
||||
public synchronized int getDisplayCursorAddress() {
|
||||
return displayRows > 0 ? displayCursorAddress : cursorAddress;
|
||||
}
|
||||
|
||||
// ========== Dimension accessors ==========
|
||||
public int getRows() { return rows; }
|
||||
public int getCols() { return cols; }
|
||||
public int getMaxRows() { return maxRows; }
|
||||
public int getMaxCols() { return maxCols; }
|
||||
public int getDefRows() { return defRows; }
|
||||
public int getDefCols() { return defCols; }
|
||||
public int getAltRows() { return altRows; }
|
||||
public int getAltCols() { return altCols; }
|
||||
public boolean isScreenAlt() { return screenAlt; }
|
||||
|
||||
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
|
||||
public synchronized void setAlternateDimensions(int newAltRows, int newAltCols) {
|
||||
if (newAltRows == altRows && newAltCols == altCols) return;
|
||||
this.altRows = newAltRows;
|
||||
this.altCols = newAltCols;
|
||||
// maxRows/maxCols should be the larger of alt vs current max
|
||||
if (newAltRows > maxRows || newAltCols > maxCols) {
|
||||
this.maxRows = Math.max(maxRows, newAltRows);
|
||||
this.maxCols = Math.max(maxCols, newAltCols);
|
||||
allocateBuffers();
|
||||
}
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
// ========== Cursor ==========
|
||||
public int getCursorAddress() { return cursorAddress; }
|
||||
public synchronized void setCursorAddress(int addr) {
|
||||
this.cursorAddress = addr;
|
||||
this.displayCursorAddress = addr;
|
||||
}
|
||||
public int getCursorRow() { return cursorAddress / cols; }
|
||||
public int getCursorCol() { return cursorAddress % cols; }
|
||||
|
||||
public int getBufferAddress() { return bufferAddress; }
|
||||
public void setBufferAddress(int addr) { this.bufferAddress = addr; }
|
||||
|
||||
public byte getReplyMode() { return replyMode; }
|
||||
public void setReplyMode(byte mode) { this.replyMode = mode; }
|
||||
|
||||
public int getActivePartition() { return activePartition; }
|
||||
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = true; }
|
||||
public boolean isExplicitPartitionActive() { return explicitPartitionActive; }
|
||||
|
||||
// ========== Screen erase ==========
|
||||
|
||||
/**
|
||||
* Perform an erase, optionally using the alternate screen size.
|
||||
*/
|
||||
public synchronized void erase(boolean alt) {
|
||||
clear();
|
||||
int newRows = alt ? altRows : defRows;
|
||||
int newCols = alt ? altCols : defCols;
|
||||
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
||||
updateDisplaySnapshot();
|
||||
return;
|
||||
}
|
||||
rows = newRows;
|
||||
cols = newCols;
|
||||
screenAlt = alt;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void setFieldAttribute(int pos, byte fa) {
|
||||
ExtendedAttribute ea = buffer[pos];
|
||||
ea.clear();
|
||||
ea.fa = fa;
|
||||
if (!formatted) {
|
||||
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
|
||||
}
|
||||
formatted = true;
|
||||
screenChanged = true;
|
||||
// The display logic needs to render this attribute character
|
||||
ea.ec = 0x00; // Typically null character for the attribute space itself
|
||||
}
|
||||
|
||||
/** Clear the entire buffer. */
|
||||
public synchronized void clear() {
|
||||
for (ExtendedAttribute ea : buffer) {
|
||||
ea.clear();
|
||||
}
|
||||
cursorAddress = 0;
|
||||
bufferAddress = 0;
|
||||
formatted = false;
|
||||
replyMode = SF_SRM_FIELD;
|
||||
activePartition = 0;
|
||||
explicitPartitionActive = false;
|
||||
screenChanged = true;
|
||||
|
||||
defaultFg = 0x00;
|
||||
defaultBg = 0x00;
|
||||
defaultGr = 0x00;
|
||||
defaultCs = 0x00;
|
||||
defaultIc = 0x00;
|
||||
replyMode = SF_SRM_FIELD;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all unprotected fields.
|
||||
*/
|
||||
public synchronized void eraseAllUnprotected() {
|
||||
int size = rows * cols;
|
||||
boolean inUnprotected = false;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = buffer[i];
|
||||
if (ea.isFieldAttribute()) {
|
||||
if (!faIsProtected(ea.fa & 0xFF)) {
|
||||
inUnprotected = true;
|
||||
// Clear modified bit
|
||||
ea.fa = (byte) (ea.fa & ~FA_MODIFY);
|
||||
} else {
|
||||
inUnprotected = false;
|
||||
}
|
||||
} else if (inUnprotected) {
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
ea.cs = 0;
|
||||
ea.fg = 0;
|
||||
ea.bg = 0;
|
||||
ea.gr = 0;
|
||||
ea.ic = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Move cursor to first unprotected field
|
||||
cursorAddress = findNextUnprotected(0);
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
// ========== Field attribute navigation ==========
|
||||
|
||||
/**
|
||||
* Find the field attribute for a given buffer address.
|
||||
* Returns -1 if screen is not formatted.
|
||||
*/
|
||||
public int findFieldAttribute(int baddr) {
|
||||
if (!formatted) return -1;
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return -1;
|
||||
baddr = ((baddr % size) + size) % size;
|
||||
int start = baddr;
|
||||
int count = 0;
|
||||
do {
|
||||
if (buffer[baddr].isFieldAttribute()) {
|
||||
return baddr;
|
||||
}
|
||||
baddr = (baddr > 0) ? baddr - 1 : size - 1;
|
||||
count++;
|
||||
} while (baddr != start && count < size);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field attribute byte for a given position.
|
||||
*/
|
||||
public byte getFieldAttributeAt(int baddr) {
|
||||
int fa_addr = findFieldAttribute(baddr);
|
||||
if (fa_addr < 0) return defaultFA.fa;
|
||||
return buffer[fa_addr].fa;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next unprotected field after the given address.
|
||||
* Returns 0 if none found.
|
||||
*/
|
||||
public int findNextUnprotected(int baddr) {
|
||||
if (!formatted) return 0;
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return 0;
|
||||
baddr = ((baddr % size) + size) % size;
|
||||
int start = baddr;
|
||||
int count = 0;
|
||||
do {
|
||||
int next = (baddr + 1) % size;
|
||||
if (buffer[baddr].isFieldAttribute()
|
||||
&& !faIsProtected(buffer[baddr].fa & 0xFF)
|
||||
&& !buffer[next].isFieldAttribute()) {
|
||||
return next;
|
||||
}
|
||||
baddr = next;
|
||||
count++;
|
||||
} while (baddr != start && count < size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Refresh the formatted flag by scanning for any field attributes. */
|
||||
public void updateFormatted() {
|
||||
formatted = false;
|
||||
int size = rows * cols;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (buffer[i].isFieldAttribute()) {
|
||||
formatted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isFormatted() { return formatted; }
|
||||
public void setFormatted(boolean f) { this.formatted = f; }
|
||||
|
||||
// ========== Buffer address arithmetic ==========
|
||||
|
||||
/** Increment buffer address (wrapping). */
|
||||
public int incrementAddress(int addr) {
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return 0;
|
||||
return ((addr % size) + size + 1) % size;
|
||||
}
|
||||
|
||||
/** Decrement buffer address (wrapping). */
|
||||
public int decrementAddress(int addr) {
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return 0;
|
||||
int norm = ((addr % size) + size) % size;
|
||||
return (norm > 0) ? norm - 1 : size - 1;
|
||||
}
|
||||
|
||||
/** Convert buffer address to row. */
|
||||
public int addressToRow(int addr) { return addr / cols; }
|
||||
|
||||
/** Convert buffer address to column. */
|
||||
public int addressToCol(int addr) { return addr % cols; }
|
||||
|
||||
/** Convert row/col to buffer address. */
|
||||
public int rowColToAddress(int row, int col) { return row * cols + col; }
|
||||
|
||||
// ========== Change tracking ==========
|
||||
|
||||
public boolean isScreenChanged() { return screenChanged; }
|
||||
public void clearChanged() { screenChanged = false; firstChanged = -1; lastChanged = -1; }
|
||||
public void markAllChanged() { screenChanged = true; }
|
||||
|
||||
// ========== Setters for model reconfiguration ==========
|
||||
|
||||
public synchronized void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
|
||||
int altRows, int altCols) {
|
||||
this.maxRows = maxRows;
|
||||
this.maxCols = maxCols;
|
||||
this.defRows = defRows;
|
||||
this.defCols = defCols;
|
||||
this.altRows = altRows;
|
||||
this.altCols = altCols;
|
||||
this.rows = defRows;
|
||||
this.cols = defCols;
|
||||
allocateBuffers();
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void translateToUnicode() {
|
||||
int size = rows * cols;
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = buffer[i];
|
||||
if (!ea.isFieldAttribute()) {
|
||||
if (ea.ec == 0) {
|
||||
ea.ucs4 = 0;
|
||||
} else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE
|
||||
ea.ucs4 = getAplGraphic(ea.ec & 0xFF);
|
||||
} else {
|
||||
ea.ucs4 = translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private char getAplGraphic(int ec) {
|
||||
switch (ec) {
|
||||
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
|
||||
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
|
||||
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
|
||||
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
|
||||
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
|
||||
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
|
||||
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
|
||||
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
|
||||
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
|
||||
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
|
||||
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
|
||||
case 0xCB: return '\u253C'; // Cross -> '┼'
|
||||
|
||||
// Special math and APL symbols (matching x3270 cg.c / apl.c)
|
||||
case 0x8C: return '\u2264'; // Less-than or equal '≤'
|
||||
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
|
||||
case 0xBE: return '\u2260'; // Not equal '≠'
|
||||
case 0xAD: return '['; // Left bracket
|
||||
case 0xBD: return ']'; // Right bracket
|
||||
case 0x8D: return '{'; // Left brace
|
||||
case 0x9D: return '}'; // Right brace
|
||||
case 0xB0: return '\u00B0'; // Degree '°'
|
||||
case 0xB1: return '\u00B1'; // Plus-minus '±'
|
||||
case 0xB2: return '\u00B2'; // Superscript 2 '²'
|
||||
case 0xB3: return '\u00B3'; // Superscript 3 '³'
|
||||
case 0xAF: return '\u00AF'; // Overbar '¯'
|
||||
case 0xBA: return '\u03A9'; // Omega 'Ω'
|
||||
case 0xBF: return '\u00B5'; // Micro 'µ'
|
||||
case 0x5F: return '\u00AC'; // Not sign '¬'
|
||||
|
||||
default: return translator.ebcdicToUnicode(ec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default field attribute.
|
||||
*/
|
||||
public ExtendedAttribute getDefaultFieldAttribute() {
|
||||
return defaultFA;
|
||||
}
|
||||
|
||||
// ========== Low-level cell access for file transfer (CUT mode) ==========
|
||||
|
||||
/**
|
||||
* Write an EBCDIC byte to a cell at the given buffer address.
|
||||
* Used by CUT mode file transfer to place data into the screen buffer.
|
||||
*/
|
||||
public void setCell(int baddr, int ebcdicByte) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return;
|
||||
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
|
||||
buffer[baddr].ucs4 = 0; // will be set on display refresh
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an EBCDIC byte with extended attribute flags (cs=0).
|
||||
* Equivalent to ctlr_add() in x3270.
|
||||
*/
|
||||
public void setCellWithCS(int baddr, int ebcdicByte, int cs) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return;
|
||||
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
|
||||
buffer[baddr].cs = (byte) (cs & 0xFF);
|
||||
buffer[baddr].ucs4 = 0;
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the field attribute byte at a given buffer address.
|
||||
* Used by CUT mode to hide data fields during transfer.
|
||||
*/
|
||||
public void setCellFA(int baddr, byte fa) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return;
|
||||
buffer[baddr].fa = fa;
|
||||
buffer[baddr].ic = 1; // mark as field attribute
|
||||
formatted = true;
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get just the EBCDIC code byte from a cell.
|
||||
* Convenience method for file transfer data extraction.
|
||||
*/
|
||||
public int getCellEC(int baddr) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
|
||||
return buffer[baddr].ec & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field attribute byte at a cell (the fa byte, not the ic field).
|
||||
* For CUT mode checking of field attribute modifications.
|
||||
*/
|
||||
public byte getCellFAByte(int baddr) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
|
||||
return buffer[baddr].fa;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
|
||||
|
||||
/**
|
||||
* Manages the raw TCP socket connection to a TN3270 host.
|
||||
* Handles connect/disconnect, raw byte I/O, and spawns a reader thread.
|
||||
*/
|
||||
public class TelnetConnection {
|
||||
|
||||
private static final Logger log = Logger.getLogger(TelnetConnection.class.getName());
|
||||
private static final int READ_BUFFER_SIZE = 32768;
|
||||
|
||||
private Socket socket;
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private Thread readerThread;
|
||||
private volatile boolean running;
|
||||
private final TelnetFSM fsm;
|
||||
private final ConnectionConfig config;
|
||||
|
||||
private javax.net.ssl.SSLSession sslSession;
|
||||
|
||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
this.config = config;
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return sslSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the host. Blocks until connection is established or fails.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
if (config.isUseTls()) {
|
||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
||||
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
||||
try {
|
||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
sslSocket.setKeepAlive(true);
|
||||
sslSocket.setTcpNoDelay(true);
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
sslSession = sslSocket.getSession();
|
||||
log.info("TLS session active: protocol=" + sslSession.getProtocol() +
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("TLS setup failure: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||
socket = new Socket();
|
||||
socket.setKeepAlive(true);
|
||||
socket.setOOBInline(true);
|
||||
socket.setTcpNoDelay(true);
|
||||
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
}
|
||||
|
||||
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
|
||||
outputStream = new BufferedOutputStream(socket.getOutputStream());
|
||||
|
||||
log.info("Connected to " + socket.getRemoteSocketAddress());
|
||||
|
||||
running = true;
|
||||
readerThread = new Thread(this::readLoop, "TN3270-Reader");
|
||||
readerThread.setDaemon(true);
|
||||
readerThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send raw bytes to the host.
|
||||
*/
|
||||
public synchronized void sendRaw(byte[] data) throws IOException {
|
||||
sendRaw(data, 0, data.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send raw bytes to the host with offset and length.
|
||||
*/
|
||||
public synchronized void sendRaw(byte[] data, int offset, int length) throws IOException {
|
||||
if (outputStream == null) return;
|
||||
outputStream.write(data, offset, length);
|
||||
outputStream.flush();
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
running = false;
|
||||
try {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.shutdownInput();
|
||||
socket.shutdownOutput();
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.log(Level.FINE, "Error during disconnect", e);
|
||||
}
|
||||
socket = null;
|
||||
inputStream = null;
|
||||
outputStream = null;
|
||||
log.info("Disconnected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the socket is connected.
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return socket != null && socket.isConnected() && !socket.isClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main reader loop. Reads from socket and feeds bytes to the telnet FSM.
|
||||
*/
|
||||
private void readLoop() {
|
||||
byte[] buf = new byte[READ_BUFFER_SIZE];
|
||||
try {
|
||||
while (running && isConnected()) {
|
||||
int n = inputStream.read(buf);
|
||||
if (n < 0) {
|
||||
log.info("Host disconnected (EOF)");
|
||||
fsm.onDisconnect();
|
||||
break;
|
||||
}
|
||||
if (n > 0) {
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
||||
}
|
||||
try {
|
||||
fsm.feedBytes(buf, 0, n);
|
||||
fsm.endOfNetworkData();
|
||||
} catch (Throwable t) {
|
||||
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
if (running) {
|
||||
log.info("Socket closed: " + e.getMessage());
|
||||
fsm.onDisconnect();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
log.log(Level.WARNING, "Read error", e);
|
||||
fsm.onError("Read error: " + e.getMessage());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
if (running) {
|
||||
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
|
||||
fsm.onError("Network loop error: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Format bytes as hex string for logging. */
|
||||
static String formatHex(byte[] data, int offset, int length) {
|
||||
StringBuilder sb = new StringBuilder(length * 3);
|
||||
for (int i = 0; i < length && i < 128; i++) {
|
||||
if (i > 0) sb.append(' ');
|
||||
sb.append(String.format("%02x", data[offset + i] & 0xFF));
|
||||
}
|
||||
if (length > 128) sb.append("...");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
package haus.nightmare.lib3270j.tls;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* Callback interface for validating TLS server certificates.
|
||||
* Used when standard certificate path validation fails or when custom verification is needed.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TlsCertificateVerifier {
|
||||
|
||||
/**
|
||||
* Determine whether to trust an unverified server certificate chain.
|
||||
*
|
||||
* @param chain The peer certificate chain presented by the server.
|
||||
* @param authType The key exchange algorithm (e.g., "RSA", "ECDHE_RSA").
|
||||
* @param exception The CertificateException thrown by standard validation (or null if called proactively).
|
||||
* @return true to trust the certificate and proceed with the connection; false to abort.
|
||||
*/
|
||||
boolean shouldTrust(X509Certificate[] chain, String authType, CertificateException exception);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package haus.nightmare.lib3270j.tls;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Custom X509TrustManager that supports:
|
||||
* 1. Standard certificate verification using the JVM default TrustManager.
|
||||
* 2. Unverified / trust-all mode when tlsVerifyCert is false.
|
||||
* 3. Interactive/custom certificate verifier callbacks (e.g. GUI prompts for self-signed certificates).
|
||||
*/
|
||||
public class TlsTrustManager implements X509TrustManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(TlsTrustManager.class.getName());
|
||||
|
||||
private final ConnectionConfig config;
|
||||
private X509TrustManager defaultTrustManager;
|
||||
|
||||
public TlsTrustManager(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
try {
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
this.defaultTrustManager = (X509TrustManager) tm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to initialize default TrustManagerFactory", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (defaultTrustManager != null) {
|
||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (config != null && !config.isTlsVerifyCert()) {
|
||||
log.fine("Certificate verification bypassed (tlsVerifyCert=false)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chain == null || chain.length == 0) {
|
||||
CertificateException ex = new CertificateException("null or zero-length certificate chain");
|
||||
if (config != null && config.getCertificateVerifier() != null) {
|
||||
if (config.getCertificateVerifier().shouldTrust(chain, authType, ex)) {
|
||||
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
try {
|
||||
if (defaultTrustManager != null) {
|
||||
defaultTrustManager.checkServerTrusted(chain, authType);
|
||||
} else {
|
||||
throw new CertificateException("No default X509TrustManager available");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
CertificateException certEx = (ex instanceof CertificateException)
|
||||
? (CertificateException) ex
|
||||
: new CertificateException("Certificate validation failed: " + ex.getMessage(), ex);
|
||||
|
||||
log.log(Level.FINE, "Standard certificate validation failed: " + certEx.getMessage(), certEx);
|
||||
|
||||
if (config != null && config.getCertificateVerifier() != null) {
|
||||
boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, certEx);
|
||||
if (accepted) {
|
||||
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||
return;
|
||||
} else {
|
||||
log.warning("Server certificate rejected by TlsCertificateVerifier callback");
|
||||
throw new CertificateException("Certificate rejected by user/verifier: " + certEx.getMessage(), certEx);
|
||||
}
|
||||
}
|
||||
throw certEx;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
if (defaultTrustManager != null) {
|
||||
return defaultTrustManager.getAcceptedIssuers();
|
||||
}
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an initialized SSLContext configured for the given ConnectionConfig.
|
||||
*/
|
||||
public static SSLContext createSSLContext(ConnectionConfig config) throws Exception {
|
||||
String protocol = (config != null && config.getSslProtocol() != null)
|
||||
? config.getSslProtocol()
|
||||
: "TLS";
|
||||
SSLContext sslContext = SSLContext.getInstance(protocol);
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
|
||||
return sslContext;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user