Initial Commit

This commit is contained in:
2026-04-14 09:18:03 -04:00
commit 64d713a283
91 changed files with 6518 additions and 0 deletions
@@ -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;
}
}
}