Initial Commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package org.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 int connectTimeoutMs = 15000;
|
||||
private int nopIntervalSeconds = 0;
|
||||
private String terminalName = null; // override terminal type string
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
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 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 String getTerminalName() { return terminalName; }
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
/**
|
||||
* 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 org.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 org.lib3270j;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.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("org.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_2);
|
||||
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,192 @@
|
||||
package org.lib3270j;
|
||||
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.datastream.DataStreamProcessor;
|
||||
import org.lib3270j.input.InputProcessor;
|
||||
import org.lib3270j.listener.ConnectionListener;
|
||||
import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.telnet.TelnetConnection;
|
||||
import org.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.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 connection config. */
|
||||
public ConnectionConfig getConfig() { return config; }
|
||||
|
||||
// ========== 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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Send Enter key. */
|
||||
public void sendEnter() {
|
||||
inputProcessor.sendAid(org.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(org.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(); }
|
||||
/** Reset (unlock keyboard). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.lib3270j;
|
||||
|
||||
import static org.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,128 @@
|
||||
package org.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];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
package org.lib3270j.datastream;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Processes inbound 3270 data stream records.
|
||||
* Handles all 3270 commands: Write, Erase/Write, Read Buffer, WSF, etc.
|
||||
* Equivalent to process_ds() and ctlr_write() in ctlr.c.
|
||||
*/
|
||||
public class DataStreamProcessor {
|
||||
|
||||
private static final Logger log = Logger.getLogger(DataStreamProcessor.class.getName());
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private final EbcdicTranslator translator;
|
||||
private final QueryReplyBuilder qrBuilder;
|
||||
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Output buffer for responses (read buffer, query replies, etc.)
|
||||
private byte[] outputBuffer;
|
||||
private int outputPos;
|
||||
|
||||
// Callback for sending data back to host
|
||||
private OutputSender outputSender;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
void send3270Data(byte[] data);
|
||||
}
|
||||
|
||||
public DataStreamProcessor(ScreenBuffer screen, EbcdicTranslator translator) {
|
||||
this.screen = screen;
|
||||
this.translator = translator;
|
||||
this.qrBuilder = new QueryReplyBuilder(screen);
|
||||
this.outputBuffer = new byte[32768];
|
||||
this.outputPos = 0;
|
||||
}
|
||||
|
||||
public void setOutputSender(OutputSender sender) {
|
||||
this.outputSender = sender;
|
||||
}
|
||||
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.add(l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a 3270 data stream record.
|
||||
*
|
||||
* @param data Raw data buffer
|
||||
* @param offset Offset of the first byte (command byte)
|
||||
* @param length Number of bytes
|
||||
* @param keyboardRestore Whether to restore keyboard after processing
|
||||
*/
|
||||
public void processRecord(byte[] data, int offset, int length, boolean keyboardRestore) {
|
||||
if (length == 0)
|
||||
return;
|
||||
|
||||
int cmd = data[offset] & 0xFF;
|
||||
log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
|
||||
|
||||
switch (cmd) {
|
||||
case CMD_W:
|
||||
case SNA_CMD_W:
|
||||
processWrite(data, offset, length, false);
|
||||
break;
|
||||
case CMD_EW:
|
||||
case SNA_CMD_EW:
|
||||
log.info(">>> ERASE/WRITE: clearing screen (default size)"); {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_EWA:
|
||||
case SNA_CMD_EWA:
|
||||
log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_RB:
|
||||
case SNA_CMD_RB:
|
||||
processReadBuffer();
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
processReadModified(false);
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
processReadModified(true);
|
||||
break;
|
||||
case CMD_EAU:
|
||||
case SNA_CMD_EAU:
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
processWriteStructuredField(data, offset, length);
|
||||
break;
|
||||
case CMD_NOP:
|
||||
log.info(">>> NOP command");
|
||||
break;
|
||||
default:
|
||||
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
|
||||
break;
|
||||
}
|
||||
|
||||
// Translate EBCDIC to Unicode for display
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
|
||||
// Debug: dump non-empty screen lines
|
||||
if (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW) {
|
||||
int r = screen.getRows();
|
||||
int c = screen.getCols();
|
||||
StringBuilder dump = new StringBuilder();
|
||||
int linesShown = 0;
|
||||
for (int row = 0; row < r && linesShown < 5; row++) {
|
||||
StringBuilder line = new StringBuilder();
|
||||
boolean hasContent = false;
|
||||
for (int col = 0; col < c; col++) {
|
||||
int addr = row * c + col;
|
||||
ExtendedAttribute ea = screen.getCell(addr);
|
||||
byte fa = screen.getFieldAttributeAt(addr);
|
||||
|
||||
if ((fa & org.lib3270j.protocol.DS3270Constants.FA_MASK) == org.lib3270j.protocol.DS3270Constants.FA_INT_ZERO_NSEL) {
|
||||
if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
|
||||
line.append('*');
|
||||
hasContent = true;
|
||||
} else {
|
||||
line.append(' ');
|
||||
}
|
||||
} else if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
|
||||
line.append(ea.ucs4);
|
||||
hasContent = true;
|
||||
} else {
|
||||
line.append(' ');
|
||||
}
|
||||
}
|
||||
if (hasContent) {
|
||||
dump.append(String.format(" %02d: %s\n", row, line.toString().stripTrailing()));
|
||||
linesShown++;
|
||||
}
|
||||
}
|
||||
if (dump.length() > 0) {
|
||||
log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process SSCP-LU data (TN3270E SSCP-LU mode).
|
||||
* Data is character data placed starting at cursor position.
|
||||
* Unlike 3270 mode, SSCP-LU data accumulates — we do NOT clear the screen.
|
||||
*/
|
||||
public void processSscpLuData(byte[] data, int offset, int length) {
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int baddr = screen.getCursorAddress();
|
||||
int cols = screen.getCols();
|
||||
|
||||
for (int i = offset; i < offset + length; i++) {
|
||||
int c = data[i] & 0xFF;
|
||||
switch (c) {
|
||||
case 0x15: // NL (new line) — move to column 0 of next row
|
||||
baddr = ((baddr / cols) + 1) * cols;
|
||||
if (baddr >= size)
|
||||
baddr = 0;
|
||||
break;
|
||||
case 0x0d: // CR (carriage return) — move to column 0 of current row
|
||||
baddr = (baddr / cols) * cols;
|
||||
break;
|
||||
case 0x0c: // FF (form feed) — clear screen and home cursor
|
||||
screen.clear();
|
||||
baddr = 0;
|
||||
break;
|
||||
case 0x00: // NULL — skip
|
||||
break;
|
||||
default:
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.ec = (byte) c;
|
||||
ea.ucs4 = translator.ebcdicToUnicode(c);
|
||||
baddr = (baddr + 1) % size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
}
|
||||
|
||||
// ========== Write processing ==========
|
||||
|
||||
private void processWrite(byte[] data, int offset, int length, boolean eraseFirst) {
|
||||
if (length < 2)
|
||||
return;
|
||||
|
||||
// WCC is the second byte
|
||||
int wcc = data[offset + 1] & 0xFF;
|
||||
boolean alarm = wccSoundAlarm(wcc);
|
||||
boolean kbdRestore = wccKeyboardRestore(wcc);
|
||||
boolean resetMdt = wccResetMDT(wcc);
|
||||
|
||||
log.fine("WCC: " + String.format("0x%02x", wcc) +
|
||||
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
||||
|
||||
if (resetMdt) {
|
||||
resetAllMDT();
|
||||
}
|
||||
|
||||
// WCC reset bit — reset partition characteristics
|
||||
if (wccReset(wcc)) {
|
||||
// Reset all character attributes to defaults
|
||||
log.fine("WCC reset: clearing default attributes");
|
||||
}
|
||||
|
||||
if (eraseFirst) {
|
||||
screen.clear();
|
||||
log.fine("Cleared screen for Erase/Write");
|
||||
}
|
||||
|
||||
// Process orders and data starting at byte 2
|
||||
int pos = offset + 2;
|
||||
int end = offset + length;
|
||||
int baddr = screen.getBufferAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
// Current SA (set attribute) values for character-mode
|
||||
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
|
||||
boolean lastWasOrder = false;
|
||||
|
||||
while (pos < end) {
|
||||
int b = data[pos] & 0xFF;
|
||||
|
||||
switch (b) {
|
||||
case ORDER_SBA: { // Set Buffer Address
|
||||
if (pos + 2 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
baddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF);
|
||||
if (baddr >= size)
|
||||
baddr = baddr % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
log.finest("SBA " + baddr + " (row=" + (baddr / screen.getCols()) + " col="
|
||||
+ (baddr % screen.getCols()) + ")");
|
||||
pos += 3;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_SF: { // Start Field
|
||||
if (pos + 1 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
int fa = data[pos + 1] & 0xFF;
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.clear();
|
||||
ea.fa = (byte) (fa & FA_MASK);
|
||||
// FA position is a display position that shows as blank
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
screen.setFormatted(true);
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
log.finest("SF fa=" + String.format("0x%02x", fa));
|
||||
pos += 2;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_SFE: { // Start Field Extended
|
||||
if (pos + 1 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
int nPairs = data[pos + 1] & 0xFF;
|
||||
if (pos + 2 + nPairs * 2 > end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.clear();
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
|
||||
for (int i = 0; i < nPairs; i++) {
|
||||
int attrType = data[pos + 2 + i * 2] & 0xFF;
|
||||
int attrValue = data[pos + 2 + i * 2 + 1] & 0xFF;
|
||||
applyExtendedAttribute(ea, attrType, attrValue);
|
||||
}
|
||||
if (ea.fa == 0) {
|
||||
// If no 3270 FA was among the pairs, set default
|
||||
ea.fa = (byte) (FA_PRINTABLE);
|
||||
}
|
||||
screen.setFormatted(true);
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 2 + nPairs * 2;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_SA: { // Set Attribute
|
||||
if (pos + 2 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
int attrType = data[pos + 1] & 0xFF;
|
||||
int attrValue = data[pos + 2] & 0xFF;
|
||||
switch (attrType) {
|
||||
case XA_FOREGROUND:
|
||||
currentFg = (byte) attrValue;
|
||||
break;
|
||||
case XA_BACKGROUND:
|
||||
currentBg = (byte) attrValue;
|
||||
break;
|
||||
case XA_HIGHLIGHTING:
|
||||
if (attrValue == XAH_DEFAULT)
|
||||
currentGr = 0;
|
||||
else if (attrValue == XAH_BLINK)
|
||||
currentGr = GR_BLINK;
|
||||
else if (attrValue == XAH_REVERSE)
|
||||
currentGr = GR_REVERSE;
|
||||
else if (attrValue == XAH_UNDERSCORE)
|
||||
currentGr = GR_UNDERLINE;
|
||||
else if (attrValue == XAH_INTENSIFY)
|
||||
currentGr = GR_INTENSIFY;
|
||||
else if (attrValue == XAH_NORMAL)
|
||||
currentGr = 0;
|
||||
break;
|
||||
case XA_CHARSET:
|
||||
currentCs = (byte) attrValue;
|
||||
break;
|
||||
}
|
||||
pos += 3;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_MF: { // Modify Field
|
||||
if (pos + 1 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
int nPairs = data[pos + 1] & 0xFF;
|
||||
if (pos + 2 + nPairs * 2 > end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
|
||||
// Find the field attribute at or before current position
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
ExtendedAttribute ea = screen.getCell(faAddr);
|
||||
for (int i = 0; i < nPairs; i++) {
|
||||
int attrType = data[pos + 2 + i * 2] & 0xFF;
|
||||
int attrValue = data[pos + 2 + i * 2 + 1] & 0xFF;
|
||||
applyExtendedAttribute(ea, attrType, attrValue);
|
||||
}
|
||||
}
|
||||
pos += 2 + nPairs * 2;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_IC: { // Insert Cursor
|
||||
screen.setCursorAddress(baddr);
|
||||
log.finest("IC at " + baddr);
|
||||
pos += 1;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_PT: { // Program Tab
|
||||
// Skip to next unprotected field
|
||||
baddr = screen.findNextUnprotected(baddr);
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 1;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_RA: { // Repeat to Address
|
||||
if (pos + 3 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
int toAddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF);
|
||||
int fillChar = data[pos + 3] & 0xFF;
|
||||
if (toAddr >= size)
|
||||
toAddr = toAddr % size;
|
||||
|
||||
// Handle GE (graphic escape) prefix
|
||||
byte fillCs = currentCs;
|
||||
if (pos + 4 < end && fillChar == ORDER_GE) {
|
||||
fillChar = data[pos + 4] & 0xFF;
|
||||
fillCs = CS_GE;
|
||||
pos += 5;
|
||||
} else {
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// Fill from current position to target
|
||||
while (baddr != toAddr) {
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0; // Destroy previous field attribute if any
|
||||
ea.ec = (byte) fillChar;
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = fillCs;
|
||||
baddr = (baddr + 1) % size;
|
||||
}
|
||||
screen.setBufferAddress(baddr);
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_EUA: { // Erase Unprotected to Address
|
||||
if (pos + 2 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
int toAddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF);
|
||||
if (toAddr >= size)
|
||||
toAddr = toAddr % size;
|
||||
|
||||
while (baddr != toAddr) {
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
if (!ea.isFieldAttribute()) {
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
byte faVal = faAddr >= 0 ? screen.getCell(faAddr).fa : 0;
|
||||
if (!faIsProtected(faVal & 0xFF)) {
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
}
|
||||
}
|
||||
baddr = (baddr + 1) % size;
|
||||
}
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 3;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ORDER_GE: { // Graphic Escape
|
||||
if (pos + 1 >= end) {
|
||||
pos = end;
|
||||
break;
|
||||
}
|
||||
int geChar = data[pos + 1] & 0xFF;
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0; // Destroy previous field attribute if any
|
||||
ea.ec = (byte) geChar;
|
||||
ea.cs = CS_GE;
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 2;
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Regular data byte (EBCDIC character)
|
||||
// Format control codes display as nulls/blanks per 3270 spec
|
||||
boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF ||
|
||||
b == FCORDER_CR || b == FCORDER_NL || b == FCORDER_EM ||
|
||||
b == FCORDER_DUP || b == FCORDER_FM || b == FCORDER_SUB ||
|
||||
b == FCORDER_EO);
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0; // Destroy previous field attribute if any
|
||||
|
||||
if (isFCOrder) {
|
||||
ea.ec = 0; // Display as null/blank
|
||||
} else {
|
||||
ea.ec = (byte) b;
|
||||
}
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = currentCs;
|
||||
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 1;
|
||||
lastWasOrder = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (alarm) {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onSoundAlarm();
|
||||
}
|
||||
}
|
||||
|
||||
screen.updateFormatted();
|
||||
}
|
||||
|
||||
private void applyExtendedAttribute(ExtendedAttribute ea, int type, int value) {
|
||||
switch (type) {
|
||||
case XA_3270:
|
||||
ea.fa = (byte) (value & FA_MASK);
|
||||
break;
|
||||
case XA_FOREGROUND:
|
||||
ea.fg = (byte) value;
|
||||
break;
|
||||
case XA_BACKGROUND:
|
||||
ea.bg = (byte) value;
|
||||
break;
|
||||
case XA_HIGHLIGHTING:
|
||||
if (value == XAH_DEFAULT || value == XAH_NORMAL)
|
||||
ea.gr = 0;
|
||||
else if (value == XAH_BLINK)
|
||||
ea.gr = GR_BLINK;
|
||||
else if (value == XAH_REVERSE)
|
||||
ea.gr = GR_REVERSE;
|
||||
else if (value == XAH_UNDERSCORE)
|
||||
ea.gr = GR_UNDERLINE;
|
||||
else if (value == XAH_INTENSIFY)
|
||||
ea.gr = GR_INTENSIFY;
|
||||
break;
|
||||
case XA_CHARSET:
|
||||
ea.cs = (byte) value;
|
||||
break;
|
||||
case XA_VALIDATION:
|
||||
case XA_OUTLINING:
|
||||
case XA_INPUT_CONTROL:
|
||||
// Acknowledged but not visually rendered yet
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetAllMDT() {
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute()) {
|
||||
ea.fa = (byte) (ea.fa & ~FA_MODIFY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Read Buffer ==========
|
||||
|
||||
private void processReadBuffer() {
|
||||
outputPos = 0;
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
// AID byte
|
||||
outputWrite(AID_NO);
|
||||
// Cursor address
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
outputWrite(caddr[0] & 0xFF);
|
||||
outputWrite(caddr[1] & 0xFF);
|
||||
|
||||
// Buffer contents
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute()) {
|
||||
outputWrite(ORDER_SF);
|
||||
outputWrite(ea.fa & 0xFF);
|
||||
} else {
|
||||
outputWrite(ea.ec & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
sendOutput();
|
||||
}
|
||||
|
||||
// ========== Read Modified ==========
|
||||
|
||||
private void processReadModified(boolean all) {
|
||||
outputPos = 0;
|
||||
|
||||
int aid = AID_NO; // Last AID
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
// AID byte
|
||||
outputWrite(aid);
|
||||
|
||||
// Cursor address
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
outputWrite(caddr[0] & 0xFF);
|
||||
outputWrite(caddr[1] & 0xFF);
|
||||
|
||||
if (!screen.isFormatted()) {
|
||||
// Unformatted: send everything
|
||||
if (all) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
outputWrite(screen.getCell(i).ec & 0xFF);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Formatted: send modified fields
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
||||
int fieldStart = (i + 1) % size;
|
||||
// Send SBA for field start
|
||||
outputWrite(ORDER_SBA);
|
||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||
outputWrite(addr[0] & 0xFF);
|
||||
outputWrite(addr[1] & 0xFF);
|
||||
|
||||
// Send field contents until next FA
|
||||
int pos = fieldStart;
|
||||
while (pos < size && !screen.getCell(pos).isFieldAttribute()) {
|
||||
outputWrite(screen.getCell(pos).ec & 0xFF);
|
||||
pos = (pos + 1) % size;
|
||||
if (pos == fieldStart)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendOutput();
|
||||
}
|
||||
|
||||
// ========== Write Structured Field ==========
|
||||
|
||||
private void processWriteStructuredField(byte[] data, int offset, int length) {
|
||||
int pos = offset + 1; // Skip WSF command byte
|
||||
int end = offset + length;
|
||||
|
||||
while (pos < end) {
|
||||
// Field length (2 bytes)
|
||||
if (pos + 2 > end)
|
||||
break;
|
||||
int fieldLen = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
|
||||
if (fieldLen == 0)
|
||||
fieldLen = end - pos;
|
||||
if (fieldLen < 3 || pos + fieldLen > end)
|
||||
break;
|
||||
|
||||
int sfId = data[pos + 2] & 0xFF;
|
||||
log.fine("SF id=" + String.format("0x%02x", sfId) + " len=" + fieldLen);
|
||||
|
||||
switch (sfId) {
|
||||
case SF_READ_PART:
|
||||
processSFReadPartition(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_ERASE_RESET:
|
||||
if (fieldLen >= 4) {
|
||||
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
|
||||
screen.erase(alt);
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
break;
|
||||
case SF_SET_REPLY_MODE:
|
||||
if (fieldLen >= 5) {
|
||||
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
|
||||
}
|
||||
break;
|
||||
case SF_CREATE_PART:
|
||||
// Acknowledged — we use implicit partition
|
||||
break;
|
||||
case SF_OUTBOUND_DS:
|
||||
if (fieldLen > 5) {
|
||||
// Outbound DS contains another 3270 command
|
||||
processRecord(data, pos + 4, fieldLen - 4, false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
|
||||
break;
|
||||
}
|
||||
|
||||
pos += fieldLen;
|
||||
}
|
||||
}
|
||||
|
||||
private void processSFReadPartition(byte[] data, int offset, int fieldLen) {
|
||||
if (fieldLen < 5)
|
||||
return;
|
||||
|
||||
int partition = data[offset + 3] & 0xFF;
|
||||
int type = data[offset + 4] & 0xFF;
|
||||
|
||||
// Log the incoming ReadPartition request
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < fieldLen && i < 32; i++) {
|
||||
sb.append(String.format("%02x ", data[offset + i] & 0xFF));
|
||||
}
|
||||
log.info("ReadPartition raw: " + sb.toString().trim() +
|
||||
" partition=" + String.format("0x%02x", partition) +
|
||||
" type=" + String.format("0x%02x", type));
|
||||
|
||||
switch (type) {
|
||||
case SF_RP_QUERY:
|
||||
log.info("ReadPartition Query — sending all query replies");
|
||||
sendAllQueryReplies();
|
||||
break;
|
||||
case SF_RP_QLIST:
|
||||
if (fieldLen >= 6) {
|
||||
int listType = data[offset + 5] & 0xFF;
|
||||
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
|
||||
if (listType == SF_RPQ_ALL || listType == SF_RPQ_EQUIV) {
|
||||
sendAllQueryReplies();
|
||||
} else if (listType == SF_RPQ_LIST) {
|
||||
// Send only requested query replies
|
||||
byte[] requestedCodes = new byte[fieldLen - 6];
|
||||
System.arraycopy(data, offset + 6, requestedCodes, 0, requestedCodes.length);
|
||||
StringBuilder reqSb = new StringBuilder();
|
||||
for (byte c : requestedCodes) {
|
||||
reqSb.append(String.format("%02x ", c & 0xFF));
|
||||
}
|
||||
log.info("Requested QR codes: " + reqSb.toString().trim());
|
||||
sendRequestedQueryReplies(requestedCodes);
|
||||
}
|
||||
} else {
|
||||
sendAllQueryReplies();
|
||||
}
|
||||
break;
|
||||
case SNA_CMD_RMA:
|
||||
processReadModified(true);
|
||||
break;
|
||||
case SNA_CMD_RB:
|
||||
processReadBuffer();
|
||||
break;
|
||||
case SNA_CMD_RM:
|
||||
processReadModified(false);
|
||||
break;
|
||||
default:
|
||||
log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAllQueryReplies() {
|
||||
byte[] qr = qrBuilder.buildAllQueryReplies(screen.getMaxCols(), screen.getMaxRows(),
|
||||
screen.getMaxCols() * screen.getMaxRows());
|
||||
// Hex dump the query reply for debugging
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < qr.length; i++) {
|
||||
sb.append(String.format("%02x ", qr[i] & 0xFF));
|
||||
if ((i + 1) % 32 == 0)
|
||||
sb.append("\n ");
|
||||
}
|
||||
log.info("Query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
||||
if (outputSender != null) {
|
||||
outputSender.send3270Data(qr);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRequestedQueryReplies(byte[] codes) {
|
||||
// For now, send all — refinement can come later
|
||||
sendAllQueryReplies();
|
||||
}
|
||||
|
||||
// ========== Output helpers ==========
|
||||
|
||||
private void outputWrite(int b) {
|
||||
if (outputPos >= outputBuffer.length) {
|
||||
byte[] newBuf = new byte[outputBuffer.length * 2];
|
||||
System.arraycopy(outputBuffer, 0, newBuf, 0, outputPos);
|
||||
outputBuffer = newBuf;
|
||||
}
|
||||
outputBuffer[outputPos++] = (byte) b;
|
||||
}
|
||||
|
||||
private void sendOutput() {
|
||||
if (outputSender != null && outputPos > 0) {
|
||||
byte[] data = new byte[outputPos];
|
||||
System.arraycopy(outputBuffer, 0, data, 0, outputPos);
|
||||
outputSender.send3270Data(data);
|
||||
}
|
||||
outputPos = 0;
|
||||
}
|
||||
|
||||
private void notifyScreenSizeChanged() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenSizeChanged(screen.getRows(), screen.getCols());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package org.lib3270j.datastream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||
* Equivalent to the do_qr_* functions in sf.c.
|
||||
*/
|
||||
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;
|
||||
|
||||
// Supported query reply codes (must match what we send in summary)
|
||||
private static final int[] SUPPORTED_QR = {
|
||||
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_IMP_PART, // 0xa6
|
||||
};
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
|
||||
// Implicit Partition
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
log.info("Built " + out.size() + " bytes of query replies");
|
||||
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();
|
||||
for (int code : SUPPORTED_QR) {
|
||||
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 partition)
|
||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||
out.write(bufSize & 0xFF); // total partition storage low
|
||||
out.write(0x00); // no special features
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(32);
|
||||
out.write(0x82); // flags: GE, CGCSGID present
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW - default char width
|
||||
out.write(SH_3279_2); // SDH - default char height
|
||||
out.write(0x00); // Load PS format types supported: none
|
||||
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 (non-DBCS)
|
||||
// Descriptor 1 (SET 0): default character set
|
||||
out.write(0x00); // SET 0
|
||||
out.write(0x10); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
||||
out.write(0x00); // LCID 0
|
||||
out.write(0x02); // CGCSGID (4 bytes) = 0x02b90025 (CGEN|CSET)
|
||||
out.write(0xb9);
|
||||
out.write(0x00);
|
||||
out.write(0x25);
|
||||
// Descriptor 2 (SET 1): APL/GE character set
|
||||
out.write(0x01); // SET 1
|
||||
out.write(0x00); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
||||
out.write(0xf1); // LCID 0xf1
|
||||
out.write(0x03); // CGCSGID: 3179-style APL2 = 0x03c30136
|
||||
out.write(0xc3);
|
||||
out.write(0x01);
|
||||
out.write(0x36);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildColor() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(36);
|
||||
int colorMax = 16;
|
||||
out.write(0x00); // no options
|
||||
out.write(colorMax); // number of colors
|
||||
out.write(0x00); // default color pair: attribute
|
||||
out.write(0xf0 + HOST_COLOR_GREEN); // default color: green
|
||||
for (int i = 0xf1; i < 0xf1 + colorMax - 1; i++) {
|
||||
out.write(i); // color attribute value
|
||||
out.write(i); // maps to itself (color mode)
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildHighlighting() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(11);
|
||||
out.write(5); // 5 pairs
|
||||
out.write(XAH_DEFAULT); out.write(XAH_NORMAL);
|
||||
out.write(XAH_BLINK); out.write(XAH_BLINK);
|
||||
out.write(XAH_REVERSE); out.write(XAH_REVERSE);
|
||||
out.write(XAH_UNDERSCORE); out.write(XAH_UNDERSCORE);
|
||||
out.write(XAH_INTENSIFY); out.write(XAH_INTENSIFY);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildReplyModes() {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
}
|
||||
|
||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||
// Implicit partition sizes, 2 self-defining parameters
|
||||
|
||||
// SDP 1: Default screen size
|
||||
out.write(0x00); // flags
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x0b); // SDP length
|
||||
out.write(0x01); // SDP type: implicit partition sizes
|
||||
out.write(0x00); // reserved
|
||||
// Default
|
||||
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
|
||||
out.write((maxCols >> 8) & 0xFF);
|
||||
out.write(maxCols & 0xFF);
|
||||
out.write((maxRows >> 8) & 0xFF);
|
||||
out.write(maxRows & 0xFF);
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package org.lib3270j.input;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.telnet.TelnetFSM;
|
||||
import static org.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;
|
||||
}
|
||||
|
||||
public boolean isKeyboardLocked() { return keyboardLocked; }
|
||||
public void setKeyboardLocked(boolean locked) { this.keyboardLocked = 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 baddr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
// 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;
|
||||
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute()) {
|
||||
endAddr = screen.incrementAddress(endAddr);
|
||||
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;
|
||||
while (dst != baddr) {
|
||||
int src = screen.decrementAddress(dst);
|
||||
screen.getCell(dst).ec = screen.getCell(src).ec;
|
||||
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
|
||||
dst = src;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
baddr = (baddr + 1) % size;
|
||||
// Skip over field attributes
|
||||
while (screen.getCell(baddr).isFieldAttribute()) {
|
||||
baddr = (baddr + 1) % size;
|
||||
}
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.markAllChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
|
||||
*/
|
||||
public void sendAid(int aidCode) {
|
||||
if (keyboardLocked && aidCode != AID_CLEAR) return;
|
||||
|
||||
lastAid = aidCode;
|
||||
keyboardLocked = true;
|
||||
|
||||
if (aidCode == AID_CLEAR) {
|
||||
screen.clear();
|
||||
screen.markAllChanged();
|
||||
// Send just the AID
|
||||
byte[] data = new byte[] { (byte) aidCode };
|
||||
sendAidResponse(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
||||
// PA keys: send AID + cursor address only (no modified data)
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] };
|
||||
sendAidResponse(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter, PF keys: send AID + 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: strip trailing 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)) {
|
||||
if (faIsProtected(ea.fa & 0xFF)) continue;
|
||||
|
||||
int fieldStart = (i + 1) % size;
|
||||
|
||||
// First, collect field data and find last non-null byte
|
||||
ByteArrayOutputStream fieldData = new ByteArrayOutputStream();
|
||||
int pos = fieldStart;
|
||||
int lastNonNull = -1;
|
||||
int fieldLen = 0;
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
int b = screen.getCell(pos).ec & 0xFF;
|
||||
fieldData.write(b);
|
||||
if (b != 0x00) {
|
||||
lastNonNull = fieldLen;
|
||||
}
|
||||
fieldLen++;
|
||||
pos = (pos + 1) % size;
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
|
||||
// Only send if there's actual data (strip trailing nulls)
|
||||
if (lastNonNull >= 0) {
|
||||
out.write(ORDER_SBA);
|
||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||
out.write(addr[0] & 0xFF);
|
||||
out.write(addr[1] & 0xFF);
|
||||
|
||||
// Write only up to the last non-null byte
|
||||
byte[] allData = fieldData.toByteArray();
|
||||
out.write(allData, 0, lastNonNull + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unformatted screen: send all data
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = 0; i < size; i++) {
|
||||
out.write(screen.getCell(i).ec & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
private void sendAidResponse(byte[] data) {
|
||||
if (fsm.getConnectionState().isSscp()) {
|
||||
fsm.sendSscpLuData(data);
|
||||
} else {
|
||||
fsm.send3270Data(data);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Cursor movement ==========
|
||||
|
||||
public void cursorUp() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr -= screen.getCols();
|
||||
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(addr);
|
||||
}
|
||||
|
||||
public void cursorDown() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr += screen.getCols();
|
||||
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(addr);
|
||||
}
|
||||
|
||||
public void cursorLeft() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = screen.decrementAddress(addr);
|
||||
screen.setCursorAddress(addr);
|
||||
}
|
||||
|
||||
public void cursorRight() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = screen.incrementAddress(addr);
|
||||
screen.setCursorAddress(addr);
|
||||
}
|
||||
|
||||
public void cursorHome() {
|
||||
if (screen.isFormatted()) {
|
||||
screen.setCursorAddress(screen.findNextUnprotected(0));
|
||||
} else {
|
||||
screen.setCursorAddress(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void tab() {
|
||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||
screen.setCursorAddress(addr);
|
||||
}
|
||||
|
||||
public void backTab() {
|
||||
// Find previous unprotected field
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int start = screen.decrementAddress(addr);
|
||||
addr = start;
|
||||
do {
|
||||
addr = screen.decrementAddress(addr);
|
||||
if (screen.getCell(addr).isFieldAttribute()) {
|
||||
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
||||
screen.setCursorAddress(screen.incrementAddress(addr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} while (addr != start);
|
||||
}
|
||||
|
||||
public void eraseEof() {
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
byte faVal = screen.getFieldAttributeAt(addr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
// Erase from cursor to end of field
|
||||
while (!screen.getCell(addr).isFieldAttribute()) {
|
||||
ExtendedAttribute ea = screen.getCell(addr);
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
addr = screen.incrementAddress(addr);
|
||||
}
|
||||
|
||||
// Set MDT
|
||||
int faAddr = screen.findFieldAttribute(screen.getCursorAddress());
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
}
|
||||
|
||||
public void deleteChar() {
|
||||
int addr = screen.getCursorAddress();
|
||||
byte faVal = screen.getFieldAttributeAt(addr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
// Shift characters left within the field
|
||||
int shiftAddr = addr;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
while (true) {
|
||||
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;
|
||||
}
|
||||
|
||||
int faAddr = screen.findFieldAttribute(addr);
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
}
|
||||
|
||||
public void backspace() {
|
||||
if (screen.getCursorAddress() == 0) return;
|
||||
cursorLeft();
|
||||
deleteChar();
|
||||
}
|
||||
|
||||
/** Reset (unlock keyboard, cancel insert mode). */
|
||||
public void reset() {
|
||||
keyboardLocked = false;
|
||||
insertMode = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.lib3270j.listener;
|
||||
|
||||
import org.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 org.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,376 @@
|
||||
package org.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_DBCS_ASIA = 0x91;
|
||||
public static final int QR_DDM = 0x95;
|
||||
public static final int QR_RPQNAMES = 0xa1;
|
||||
public static final int QR_IMP_PART = 0xa6;
|
||||
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 org.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,117 @@
|
||||
package org.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_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 org.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,329 @@
|
||||
package org.lib3270j.screen;
|
||||
|
||||
import org.lib3270j.TerminalModel;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import static org.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;
|
||||
|
||||
// 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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// ========== 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 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();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Cursor ==========
|
||||
public int getCursorAddress() { return cursorAddress; }
|
||||
public void setCursorAddress(int addr) { this.cursorAddress = 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; }
|
||||
|
||||
// ========== Screen erase ==========
|
||||
|
||||
/**
|
||||
* Perform an erase, optionally using the alternate screen size.
|
||||
*/
|
||||
public void erase(boolean alt) {
|
||||
clear();
|
||||
int newRows = alt ? altRows : defRows;
|
||||
int newCols = alt ? altCols : defCols;
|
||||
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
||||
return;
|
||||
}
|
||||
rows = newRows;
|
||||
cols = newCols;
|
||||
screenAlt = alt;
|
||||
}
|
||||
|
||||
/** Clear the entire buffer. */
|
||||
public void clear() {
|
||||
for (ExtendedAttribute ea : buffer) {
|
||||
ea.clear();
|
||||
}
|
||||
cursorAddress = 0;
|
||||
bufferAddress = 0;
|
||||
formatted = false;
|
||||
screenChanged = true;
|
||||
|
||||
defaultFg = 0x00;
|
||||
defaultBg = 0x00;
|
||||
defaultGr = 0x00;
|
||||
defaultCs = 0x00;
|
||||
defaultIc = 0x00;
|
||||
replyMode = SF_SRM_FIELD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all unprotected fields.
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
|
||||
// ========== 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;
|
||||
int start = baddr;
|
||||
do {
|
||||
if (buffer[baddr].isFieldAttribute()) {
|
||||
return baddr;
|
||||
}
|
||||
baddr = (baddr > 0) ? baddr - 1 : size - 1;
|
||||
} while (baddr != start);
|
||||
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) {
|
||||
int size = rows * cols;
|
||||
int start = baddr;
|
||||
do {
|
||||
int next = (baddr + 1) % size;
|
||||
if (buffer[baddr].isFieldAttribute()
|
||||
&& !faIsProtected(buffer[baddr].fa & 0xFF)
|
||||
&& !buffer[next].isFieldAttribute()) {
|
||||
return next;
|
||||
}
|
||||
baddr = next;
|
||||
} while (baddr != start);
|
||||
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) {
|
||||
return (addr + 1) % (rows * cols);
|
||||
}
|
||||
|
||||
/** Decrement buffer address (wrapping). */
|
||||
public int decrementAddress(int addr) {
|
||||
return (addr > 0) ? addr - 1 : (rows * cols) - 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 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();
|
||||
}
|
||||
|
||||
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) {
|
||||
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 -> '┼'
|
||||
default: return translator.ebcdicToUnicode(ec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default field attribute.
|
||||
*/
|
||||
public ExtendedAttribute getDefaultFieldAttribute() {
|
||||
return defaultFA;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.lib3270j.telnet;
|
||||
|
||||
import org.lib3270j.ConnectionState;
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.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;
|
||||
|
||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
this.config = config;
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the host. Blocks until connection is established or fails.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
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));
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
fsm.feedByte(buf[i] & 0xFF);
|
||||
}
|
||||
fsm.endOfNetworkData();
|
||||
}
|
||||
}
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,954 @@
|
||||
package org.lib3270j.telnet;
|
||||
|
||||
import org.lib3270j.*;
|
||||
import org.lib3270j.datastream.DataStreamProcessor;
|
||||
import org.lib3270j.protocol.*;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.listener.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.lib3270j.protocol.TelnetConstants.*;
|
||||
import static org.lib3270j.protocol.TN3270EConstants.*;
|
||||
|
||||
/**
|
||||
* Telnet finite state machine.
|
||||
* Processes incoming bytes one at a time, handling telnet commands,
|
||||
* option negotiation, sub-negotiation, and 3270 data stream framing.
|
||||
*
|
||||
* Eight states matching telnet.c:
|
||||
* TNS_DATA, TNS_IAC, TNS_WILL, TNS_WONT, TNS_DO, TNS_DONT, TNS_SB, TNS_SB_IAC
|
||||
*/
|
||||
public class TelnetFSM {
|
||||
|
||||
private static final Logger log = Logger.getLogger(TelnetFSM.class.getName());
|
||||
|
||||
// Telnet FSM states
|
||||
private static final int TNS_DATA = 0;
|
||||
private static final int TNS_IAC = 1;
|
||||
private static final int TNS_WILL = 2;
|
||||
private static final int TNS_WONT = 3;
|
||||
private static final int TNS_DO = 4;
|
||||
private static final int TNS_DONT = 5;
|
||||
private static final int TNS_SB = 6;
|
||||
private static final int TNS_SB_IAC = 7;
|
||||
|
||||
private int state = TNS_DATA;
|
||||
|
||||
// Option state tracking
|
||||
private final boolean[] myOpts = new boolean[256]; // options we have enabled
|
||||
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
|
||||
|
||||
// 3270 input buffer (accumulated between telnet framing)
|
||||
private final ByteArrayOutputStream ibuf = new ByteArrayOutputStream(32768);
|
||||
|
||||
// Sub-negotiation buffer
|
||||
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
|
||||
|
||||
// TN3270E state
|
||||
private boolean tn3270eNegotiated;
|
||||
private TN3270ESubmode tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
private boolean tn3270eBound;
|
||||
private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions
|
||||
private int eXmitSeq;
|
||||
private int responseRequired = RSF_NO_RESPONSE;
|
||||
private boolean deferredWillTtype;
|
||||
|
||||
// Connection references
|
||||
private TelnetConnection connection;
|
||||
private final ConnectionConfig config;
|
||||
private final ScreenBuffer screenBuffer;
|
||||
private final DataStreamProcessor dsProcessor;
|
||||
private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
|
||||
|
||||
// Listeners
|
||||
private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Connected LU info
|
||||
private String connectedLu;
|
||||
private String connectedType;
|
||||
|
||||
enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
|
||||
|
||||
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
|
||||
this.config = config;
|
||||
this.screenBuffer = screenBuffer;
|
||||
this.dsProcessor = dsProcessor;
|
||||
}
|
||||
|
||||
public void setConnection(TelnetConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) { screenListeners.add(l); }
|
||||
|
||||
public ConnectionState getConnectionState() { return connectionState; }
|
||||
public boolean[] getMyOpts() { return myOpts; }
|
||||
public boolean[] getHisOpts() { return hisOpts; }
|
||||
|
||||
/**
|
||||
* Called when the TCP connection is established.
|
||||
* Initialize telnet state and prepare for negotiation.
|
||||
*/
|
||||
public void onConnected() {
|
||||
state = TNS_DATA;
|
||||
java.util.Arrays.fill(myOpts, false);
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
tn3270eNegotiated = false;
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
tn3270eBound = false;
|
||||
eXmitSeq = 0;
|
||||
deferredWillTtype = false;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
|
||||
// Initial TN3270E function requests
|
||||
eFuncs[FUNC_BIND_IMAGE] = true;
|
||||
eFuncs[FUNC_RESPONSES] = true;
|
||||
eFuncs[FUNC_SYSREQ] = true;
|
||||
|
||||
changeState(ConnectionState.TELNET_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed a single byte from the network into the FSM.
|
||||
*/
|
||||
public void feedByte(int c) {
|
||||
switch (state) {
|
||||
case TNS_DATA:
|
||||
processData(c);
|
||||
break;
|
||||
case TNS_IAC:
|
||||
processIAC(c);
|
||||
break;
|
||||
case TNS_WILL:
|
||||
processWill(c);
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case TNS_WONT:
|
||||
processWont(c);
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case TNS_DO:
|
||||
processDo(c);
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case TNS_DONT:
|
||||
processDont(c);
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case TNS_SB:
|
||||
processSB(c);
|
||||
break;
|
||||
case TNS_SB_IAC:
|
||||
processSBIAC(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called at the end of a network read batch. */
|
||||
public void endOfNetworkData() {
|
||||
// Opportunity to flush any pending NVT data
|
||||
}
|
||||
|
||||
// ========== TNS_DATA ==========
|
||||
|
||||
private void processData(int c) {
|
||||
if (c == IAC) {
|
||||
state = TNS_IAC;
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionState == ConnectionState.TELNET_PENDING) {
|
||||
// Got data before any telnet commands — assume NVT mode
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
||||
ibuf.write(c);
|
||||
}
|
||||
// NVT data would go to NVT processor (not implemented in initial version)
|
||||
}
|
||||
|
||||
// ========== TNS_IAC ==========
|
||||
|
||||
private void processIAC(int c) {
|
||||
switch (c) {
|
||||
case IAC: // Escaped IAC — literal 0xFF
|
||||
ibuf.write(c);
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case EOR: // End of record — process accumulated 3270 data
|
||||
log.fine("RCVD EOR");
|
||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
||||
processEndOfRecord();
|
||||
}
|
||||
ibuf.reset();
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case WILL: state = TNS_WILL; break;
|
||||
case WONT: state = TNS_WONT; break;
|
||||
case DO: state = TNS_DO; break;
|
||||
case DONT: state = TNS_DONT; break;
|
||||
case SB:
|
||||
sbbuf.reset();
|
||||
state = TNS_SB;
|
||||
break;
|
||||
case GA:
|
||||
log.fine("RCVD GA");
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case NOP:
|
||||
log.fine("RCVD NOP");
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
default:
|
||||
log.fine("RCVD IAC " + TelnetConstants.commandName(c));
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TNS_WILL (host sends WILL option) ==========
|
||||
|
||||
private void processWill(int opt) {
|
||||
log.info("RCVD WILL " + TelnetConstants.optionName(opt));
|
||||
|
||||
switch (opt) {
|
||||
case TELOPT_SGA:
|
||||
case TELOPT_BINARY:
|
||||
case TELOPT_EOR:
|
||||
case TELOPT_ECHO:
|
||||
if (!hisOpts[opt]) {
|
||||
hisOpts[opt] = true;
|
||||
sendCommand(DO, opt);
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_TN3270E:
|
||||
if (!hisOpts[opt]) {
|
||||
hisOpts[opt] = true;
|
||||
sendCommand(DO, opt);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
sendCommand(DONT, opt);
|
||||
break;
|
||||
}
|
||||
|
||||
checkIn3270();
|
||||
}
|
||||
|
||||
// ========== TNS_WONT ==========
|
||||
|
||||
private void processWont(int opt) {
|
||||
log.info("RCVD WONT " + TelnetConstants.optionName(opt));
|
||||
if (hisOpts[opt]) {
|
||||
hisOpts[opt] = false;
|
||||
sendCommand(DONT, opt);
|
||||
}
|
||||
checkIn3270();
|
||||
}
|
||||
|
||||
// ========== TNS_DO (host requests we enable option) ==========
|
||||
|
||||
private void processDo(int opt) {
|
||||
log.info("RCVD DO " + TelnetConstants.optionName(opt));
|
||||
|
||||
switch (opt) {
|
||||
case TELOPT_BINARY:
|
||||
case TELOPT_EOR:
|
||||
case TELOPT_SGA:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
sendCommand(WILL, opt);
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_TTYPE:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
if (hisOpts[TELOPT_TN3270E]) {
|
||||
// Defer TTYPE response until TN3270E negotiation completes
|
||||
deferredWillTtype = true;
|
||||
} else {
|
||||
sendCommand(WILL, opt);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_TN3270E:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
sendCommand(WILL, opt);
|
||||
// Start TN3270E sub-negotiation: send device type request
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_NAWS:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
sendCommand(WILL, opt);
|
||||
}
|
||||
sendNaws();
|
||||
break;
|
||||
|
||||
case TELOPT_NEW_ENVIRON:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
sendCommand(WILL, opt);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
sendCommand(WONT, opt);
|
||||
break;
|
||||
}
|
||||
|
||||
checkIn3270();
|
||||
}
|
||||
|
||||
// ========== TNS_DONT ==========
|
||||
|
||||
private void processDont(int opt) {
|
||||
log.info("RCVD DONT " + TelnetConstants.optionName(opt));
|
||||
if (myOpts[opt]) {
|
||||
myOpts[opt] = false;
|
||||
sendCommand(WONT, opt);
|
||||
}
|
||||
checkIn3270();
|
||||
}
|
||||
|
||||
// ========== TNS_SB (accumulating sub-negotiation data) ==========
|
||||
|
||||
private void processSB(int c) {
|
||||
if (c == IAC) {
|
||||
state = TNS_SB_IAC;
|
||||
} else {
|
||||
sbbuf.write(c);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TNS_SB_IAC (IAC seen during sub-negotiation) ==========
|
||||
|
||||
private void processSBIAC(int c) {
|
||||
if (c == SE) {
|
||||
// Sub-negotiation complete
|
||||
processSubNegotiation(sbbuf.toByteArray());
|
||||
state = TNS_DATA;
|
||||
} else if (c == IAC) {
|
||||
// Escaped IAC within sub-negotiation
|
||||
sbbuf.write(IAC);
|
||||
state = TNS_SB;
|
||||
} else {
|
||||
// Shouldn't happen, but recover
|
||||
log.warning("Unexpected byte " + c + " after IAC in SB");
|
||||
state = TNS_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Sub-negotiation processing ==========
|
||||
|
||||
private void processSubNegotiation(byte[] data) {
|
||||
if (data.length < 1) return;
|
||||
|
||||
int opt = data[0] & 0xFF;
|
||||
log.info("RCVD SB " + TelnetConstants.optionName(opt) + " (" + data.length + " bytes)");
|
||||
|
||||
switch (opt) {
|
||||
case TELOPT_TTYPE:
|
||||
handleTTypeSB(data);
|
||||
break;
|
||||
case TELOPT_TN3270E:
|
||||
handleTN3270ESB(data);
|
||||
break;
|
||||
case TELOPT_NEW_ENVIRON:
|
||||
handleNewEnvironSB(data);
|
||||
break;
|
||||
default:
|
||||
log.info("Ignoring SB for option " + opt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TTYPE sub-negotiation ==========
|
||||
|
||||
private void handleTTypeSB(byte[] data) {
|
||||
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
|
||||
// Host asks for terminal type
|
||||
String termType = config.getEffectiveTerminalType();
|
||||
log.info("RCVD SB TTYPE SEND - Responding with: " + termType);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(IAC);
|
||||
out.write(SB);
|
||||
out.write(TELOPT_TTYPE);
|
||||
out.write(TELQUAL_IS);
|
||||
for (char ch : termType.toCharArray()) {
|
||||
out.write((byte) ch);
|
||||
}
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.info("SENT SB TTYPE IS " + termType + " SE");
|
||||
}
|
||||
}
|
||||
|
||||
// ========== NEW_ENVIRON sub-negotiation ==========
|
||||
|
||||
private void handleNewEnvironSB(byte[] data) {
|
||||
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
|
||||
log.info("RCVD SB NEW-ENVIRON SEND - Responding with empty IS");
|
||||
byte[] response = { (byte) IAC, (byte) SB, (byte) TELOPT_NEW_ENVIRON,
|
||||
(byte) TELQUAL_IS, (byte) IAC, (byte) SE };
|
||||
sendBytes(response);
|
||||
log.info("SENT SB NEW-ENVIRON IS SE");
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TN3270E sub-negotiation ==========
|
||||
|
||||
private void handleTN3270ESB(byte[] data) {
|
||||
if (data.length < 2) return;
|
||||
|
||||
int op = data[1] & 0xFF;
|
||||
log.info("TN3270E SB op=" + op + " (" + tn3270eOpName(op) + ")");
|
||||
|
||||
switch (op) {
|
||||
case OP_SEND:
|
||||
// Host asks us to send device-type request
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
break;
|
||||
|
||||
case OP_DEVICE_TYPE:
|
||||
handleTN3270EDeviceType(data);
|
||||
break;
|
||||
|
||||
case OP_FUNCTIONS:
|
||||
handleTN3270EFunctions(data);
|
||||
break;
|
||||
|
||||
case OP_IS:
|
||||
// Could be device-type IS or functions IS, check context
|
||||
if (data.length >= 3 && data[2] == OP_DEVICE_TYPE) {
|
||||
handleTN3270EDeviceType(data);
|
||||
} else if (data.length >= 3 && data[2] == OP_FUNCTIONS) {
|
||||
handleTN3270EFunctions(data);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
log.info("Unhandled TN3270E op: " + op);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendTN3270EDeviceTypeRequest() {
|
||||
String termType = config.getEffectiveTerminalType();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(IAC);
|
||||
out.write(SB);
|
||||
out.write(TELOPT_TN3270E);
|
||||
out.write(OP_DEVICE_TYPE);
|
||||
out.write(OP_REQUEST);
|
||||
for (char ch : termType.toCharArray()) {
|
||||
out.write((byte) ch);
|
||||
}
|
||||
// Add LU name if specified
|
||||
if (config.getLuName() != null && !config.getLuName().isEmpty()) {
|
||||
out.write(OP_CONNECT);
|
||||
for (char ch : config.getLuName().toCharArray()) {
|
||||
out.write((byte) ch);
|
||||
}
|
||||
}
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.info("SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
|
||||
(config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE");
|
||||
}
|
||||
|
||||
private void handleTN3270EDeviceType(byte[] data) {
|
||||
// Parse: TN3270E DEVICE-TYPE IS <type> CONNECT <name>
|
||||
// or: TN3270E DEVICE-TYPE REJECT REASON <code>
|
||||
int pos = 2; // Skip TN3270E and DEVICE-TYPE
|
||||
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
||||
// Rejection
|
||||
pos++;
|
||||
int reason = -1;
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
|
||||
pos++;
|
||||
if (pos < data.length) {
|
||||
reason = data[pos] & 0xFF;
|
||||
}
|
||||
}
|
||||
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
|
||||
|
||||
// Fall back to plain TN3270
|
||||
myOpts[TELOPT_TN3270E] = false;
|
||||
hisOpts[TELOPT_TN3270E] = false;
|
||||
|
||||
// Send deferred WILL TTYPE if needed
|
||||
if (deferredWillTtype) {
|
||||
sendCommand(WILL, TELOPT_TTYPE);
|
||||
deferredWillTtype = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse device type name
|
||||
StringBuilder deviceType = new StringBuilder();
|
||||
while (pos < data.length && (data[pos] & 0xFF) != OP_CONNECT) {
|
||||
deviceType.append((char) (data[pos] & 0xFF));
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Parse device name (after CONNECT)
|
||||
String deviceName = null;
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_CONNECT) {
|
||||
pos++;
|
||||
StringBuilder name = new StringBuilder();
|
||||
while (pos < data.length) {
|
||||
name.append((char) (data[pos] & 0xFF));
|
||||
pos++;
|
||||
}
|
||||
deviceName = name.toString();
|
||||
}
|
||||
|
||||
connectedType = deviceType.toString();
|
||||
connectedLu = deviceName;
|
||||
log.info("TN3270E device-type IS " + connectedType +
|
||||
(connectedLu != null ? " CONNECT " + connectedLu : ""));
|
||||
|
||||
// Now send functions request
|
||||
sendTN3270EFunctionsRequest();
|
||||
}
|
||||
|
||||
private void sendTN3270EFunctionsRequest() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(IAC);
|
||||
out.write(SB);
|
||||
out.write(TELOPT_TN3270E);
|
||||
out.write(OP_FUNCTIONS);
|
||||
out.write(OP_REQUEST);
|
||||
|
||||
StringBuilder funcNames = new StringBuilder();
|
||||
for (int i = 0; i < eFuncs.length; i++) {
|
||||
if (eFuncs[i]) {
|
||||
out.write(i);
|
||||
if (funcNames.length() > 0) funcNames.append(" ");
|
||||
funcNames.append(TN3270EConstants.functionName(i));
|
||||
}
|
||||
}
|
||||
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
|
||||
}
|
||||
|
||||
private void handleTN3270EFunctions(byte[] data) {
|
||||
// Parse: TN3270E FUNCTIONS IS [func...]
|
||||
int pos = 2; // Skip TN3270E, FUNCTIONS
|
||||
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
|
||||
pos++; // Skip IS
|
||||
}
|
||||
|
||||
// The remaining bytes are the agreed-upon functions
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
StringBuilder funcNames = new StringBuilder();
|
||||
while (pos < data.length) {
|
||||
int func = data[pos] & 0xFF;
|
||||
if (func <= FUNC_SNA_SENSE) {
|
||||
eFuncs[func] = true;
|
||||
if (funcNames.length() > 0) funcNames.append(" ");
|
||||
funcNames.append(TN3270EConstants.functionName(func));
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
tn3270eNegotiated = true;
|
||||
log.info("TN3270E functions IS: " + funcNames);
|
||||
log.info("TN3270E negotiation complete");
|
||||
|
||||
// Move to CONNECTED_UNBOUND or CONNECTED_SSCP
|
||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||
|
||||
// Notify listeners
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
l.onTN3270ENegotiated(connectedType, connectedLu);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== End of Record processing ==========
|
||||
|
||||
private void processEndOfRecord() {
|
||||
byte[] data = ibuf.toByteArray();
|
||||
if (data.length == 0) return;
|
||||
|
||||
if (tn3270eNegotiated) {
|
||||
// TN3270E mode: data starts with 5-byte header
|
||||
processTN3270ERecord(data);
|
||||
} else {
|
||||
// Plain TN3270 mode: data is raw 3270 data stream
|
||||
dsProcessor.processRecord(data, 0, data.length, true);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private void processTN3270ERecord(byte[] data) {
|
||||
if (data.length < EH_SIZE) {
|
||||
log.warning("TN3270E record too short: " + data.length);
|
||||
return;
|
||||
}
|
||||
|
||||
int dataType = data[0] & 0xFF;
|
||||
int requestFlag = data[1] & 0xFF;
|
||||
int responseFlag = data[2] & 0xFF;
|
||||
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
|
||||
|
||||
log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) +
|
||||
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
|
||||
|
||||
responseRequired = responseFlag;
|
||||
|
||||
switch (dataType) {
|
||||
case DT_3270_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
// Transition to 3270 mode
|
||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
|
||||
connectionState == ConnectionState.CONNECTED_SSCP) {
|
||||
// Clear screen on transition to 3270 mode from unbound/SSCP
|
||||
// This ensures old SSCP-LU data or stale content doesn't persist
|
||||
screenBuffer.erase(false);
|
||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
}
|
||||
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
// Send positive response if required
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_SSCP_LU_DATA:
|
||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND) {
|
||||
// Clear screen on first SSCP-LU transition to remove stale data
|
||||
screenBuffer.clear();
|
||||
changeState(ConnectionState.CONNECTED_SSCP);
|
||||
tn3270eSubmode = TN3270ESubmode.E_SSCP;
|
||||
}
|
||||
if (data.length > EH_SIZE) {
|
||||
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_BIND_IMAGE:
|
||||
tn3270eBound = true;
|
||||
// Parse BIND image for screen dimensions (SNA BIND format)
|
||||
{
|
||||
int bindLen = data.length - EH_SIZE;
|
||||
StringBuilder bindHex = new StringBuilder();
|
||||
for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) {
|
||||
bindHex.append(String.format("%02x ", data[bi] & 0xFF));
|
||||
}
|
||||
log.info("Received BIND image (" + bindLen + " bytes)" +
|
||||
" responseFlag=" + responseFlag + " raw: " + bindHex.toString().trim());
|
||||
|
||||
// SNA BIND RU offsets (from 3270ds.h):
|
||||
// Byte 20 = RD (default rows), Byte 21 = CD (default cols)
|
||||
// Byte 22 = RA (alternate rows), Byte 23 = CA (alternate cols)
|
||||
// Byte 24 = SSIZE (screen size indicator)
|
||||
final int BIND_OFF_RD = 20, BIND_OFF_CD = 21;
|
||||
final int BIND_OFF_RA = 22, BIND_OFF_CA = 23;
|
||||
final int BIND_OFF_SSIZE = 24;
|
||||
|
||||
if (bindLen > BIND_OFF_SSIZE) {
|
||||
int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF;
|
||||
int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF;
|
||||
int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF;
|
||||
int bindRa, bindCa;
|
||||
|
||||
switch (ssize) {
|
||||
case 0x00: case 0x02:
|
||||
// Default model 2 dimensions for both default and alt
|
||||
bindRd = 24; bindCd = 80;
|
||||
bindRa = 24; bindCa = 80;
|
||||
break;
|
||||
case 0x03:
|
||||
// Default = 24x80, alternate = configured model max
|
||||
bindRd = 24; bindCd = 80;
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
case 0x7e:
|
||||
// Both default and alternate = specified values
|
||||
bindRa = bindRd; bindCa = bindCd;
|
||||
break;
|
||||
case 0x7f:
|
||||
// Default and alternate are both specified separately
|
||||
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
|
||||
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
|
||||
break;
|
||||
default:
|
||||
// Unknown SSIZE - use model defaults
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
}
|
||||
log.info("BIND SSIZE=0x" + String.format("%02x", ssize) +
|
||||
" default=" + bindRd + "x" + bindCd +
|
||||
" alt=" + bindRa + "x" + bindCa);
|
||||
|
||||
// Apply dimensions — constrain to model max
|
||||
int maxR = screenBuffer.getMaxRows();
|
||||
int maxC = screenBuffer.getMaxCols();
|
||||
if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) {
|
||||
screenBuffer.setAlternateDimensions(bindRa, bindCa);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear and reset screen for new session
|
||||
screenBuffer.erase(false);
|
||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
notifyScreenUpdate();
|
||||
// Send positive response if required (critical for ISPF NEWAPPL)
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_UNBIND:
|
||||
log.info("Received UNBIND responseFlag=" + responseFlag);
|
||||
tn3270eBound = false;
|
||||
// Restore alternate dimensions to configured model max (per x3270)
|
||||
screenBuffer.setAlternateDimensions(
|
||||
screenBuffer.getMaxRows(), screenBuffer.getMaxCols());
|
||||
// Clear screen on UNBIND — essential for ISPF NEWAPPL transitions
|
||||
screenBuffer.clear();
|
||||
// Send positive response BEFORE changing state (host expects it)
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
notifyScreenUpdate();
|
||||
break;
|
||||
|
||||
case DT_NVT_DATA:
|
||||
// NVT data in TN3270E mode
|
||||
changeState(ConnectionState.CONNECTED_E_NVT);
|
||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||
break;
|
||||
|
||||
case DT_RESPONSE:
|
||||
log.fine("Received response, seq=" + seqNumber);
|
||||
break;
|
||||
|
||||
default:
|
||||
log.info("Unhandled TN3270E data type: " + dataType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendTN3270EPositiveResponse(int seqNumber) {
|
||||
byte[] resp = new byte[EH_SIZE + 1];
|
||||
resp[0] = (byte) DT_RESPONSE;
|
||||
resp[1] = 0;
|
||||
resp[2] = (byte) RSF_POSITIVE_RESPONSE;
|
||||
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seqNumber & 0xFF);
|
||||
resp[5] = (byte) POS_DEVICE_END;
|
||||
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
// ========== Check if we should transition to 3270 mode ==========
|
||||
|
||||
private void checkIn3270() {
|
||||
if (connectionState != ConnectionState.TELNET_PENDING) return;
|
||||
|
||||
// For TN3270E, we wait for TN3270E negotiation to complete
|
||||
if (myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
|
||||
return; // TN3270E in progress
|
||||
}
|
||||
|
||||
// For plain TN3270: need BINARY and EOR in both directions
|
||||
if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] &&
|
||||
myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) {
|
||||
log.info("Transitioning to plain TN3270 mode");
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Sending helpers ==========
|
||||
|
||||
private void sendCommand(int cmd, int opt) {
|
||||
byte[] msg = { (byte) IAC, (byte) cmd, (byte) opt };
|
||||
sendBytes(msg);
|
||||
log.info("SENT " + TelnetConstants.commandName(cmd) + " " + TelnetConstants.optionName(opt));
|
||||
}
|
||||
|
||||
private void sendNaws() {
|
||||
int cols = screenBuffer.getMaxCols();
|
||||
int rows = screenBuffer.getMaxRows();
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(IAC);
|
||||
out.write(SB);
|
||||
out.write(TELOPT_NAWS);
|
||||
writeNawsValue(out, cols);
|
||||
writeNawsValue(out, rows);
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.info("SENT SB NAWS " + cols + " " + rows + " SE");
|
||||
}
|
||||
|
||||
private void writeNawsValue(ByteArrayOutputStream out, int value) {
|
||||
int hi = (value >> 8) & 0xFF;
|
||||
int lo = value & 0xFF;
|
||||
out.write(hi);
|
||||
if (hi == IAC) out.write(IAC);
|
||||
out.write(lo);
|
||||
if (lo == IAC) out.write(IAC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a 3270 record (with EOR framing, and TN3270E header if applicable).
|
||||
*/
|
||||
public void sendRecord(byte[] data) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + 10);
|
||||
|
||||
// Escape any IAC bytes in the payload
|
||||
for (byte b : data) {
|
||||
out.write(b & 0xFF);
|
||||
if ((b & 0xFF) == IAC) {
|
||||
out.write(IAC);
|
||||
}
|
||||
}
|
||||
|
||||
out.write(IAC);
|
||||
out.write(EOR);
|
||||
sendBytes(out.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a 3270 data record, with TN3270E header if in TN3270E mode.
|
||||
*/
|
||||
public void send3270Data(byte[] data) {
|
||||
if (tn3270eNegotiated) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
|
||||
// TN3270E header
|
||||
out.write(DT_3270_DATA); // data type
|
||||
out.write(0); // request flag
|
||||
out.write(0); // response flag
|
||||
out.write((eXmitSeq >> 8) & 0xFF); // seq high
|
||||
out.write(eXmitSeq & 0xFF); // seq low
|
||||
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
|
||||
// 3270 data
|
||||
for (byte b : data) {
|
||||
out.write(b & 0xFF);
|
||||
}
|
||||
sendRecord(out.toByteArray());
|
||||
} else {
|
||||
sendRecord(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SSCP-LU data (in TN3270E SSCP-LU mode).
|
||||
*/
|
||||
public void sendSscpLuData(byte[] data) {
|
||||
if (tn3270eNegotiated) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
|
||||
out.write(DT_SSCP_LU_DATA);
|
||||
out.write(0);
|
||||
out.write(0);
|
||||
out.write((eXmitSeq >> 8) & 0xFF);
|
||||
out.write(eXmitSeq & 0xFF);
|
||||
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
|
||||
for (byte b : data) {
|
||||
out.write(b & 0xFF);
|
||||
}
|
||||
sendRecord(out.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBytes(byte[] data) {
|
||||
try {
|
||||
connection.sendRaw(data);
|
||||
} catch (IOException e) {
|
||||
log.log(Level.WARNING, "Send error", e);
|
||||
onError("Send error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== State management ==========
|
||||
|
||||
private void changeState(ConnectionState newState) {
|
||||
ConnectionState old = this.connectionState;
|
||||
this.connectionState = newState;
|
||||
log.info("State: " + old + " -> " + newState);
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
l.onConnectionStateChanged(old, newState);
|
||||
}
|
||||
}
|
||||
|
||||
public void onDisconnect() {
|
||||
changeState(ConnectionState.NOT_CONNECTED);
|
||||
}
|
||||
|
||||
public void onError(String message) {
|
||||
log.warning("Error: " + message);
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
l.onConnectionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyScreenUpdate() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
|
||||
public String getConnectedLu() { return connectedLu; }
|
||||
public String getConnectedType() { return connectedType; }
|
||||
|
||||
private static String tn3270eOpName(int op) {
|
||||
switch (op) {
|
||||
case OP_ASSOCIATE: return "ASSOCIATE";
|
||||
case OP_CONNECT: return "CONNECT";
|
||||
case OP_DEVICE_TYPE: return "DEVICE-TYPE";
|
||||
case OP_FUNCTIONS: return "FUNCTIONS";
|
||||
case OP_IS: return "IS";
|
||||
case OP_REASON: return "REASON";
|
||||
case OP_REJECT: return "REJECT";
|
||||
case OP_REQUEST: return "REQUEST";
|
||||
case OP_SEND: return "SEND";
|
||||
default: return "OP-" + op;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user