More NVT stuff
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m24s

This commit is contained in:
2026-08-31 15:54:26 -04:00
parent 7b5bc7ec11
commit a56aa5f059
82 changed files with 7319 additions and 203 deletions
@@ -129,14 +129,27 @@ public class ConnectDialog extends JDialog {
"870 - Eastern Europe / Latin-2", "870 - Eastern Europe / Latin-2",
"871 - Iceland", "871 - Iceland",
"875 - Greece (Greek)", "875 - Greece (Greek)",
"1026 - Turkey (Turkish)", "1026 - Turkey (Turkish Latin-5)",
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual",
"424 - Hebrew (with Lowercase)",
"803 - Hebrew Old / Standard",
"838 - Thai",
"1160 - Thai (Euro \u20AC)",
"1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine",
"1154 - Cyrillic Multilingual (Euro \u20AC)",
"880 - Cyrillic Russian",
"1140 - US / Canada (Euro \u20AC)", "1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)", "1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)", "1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS", "930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS", "939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS", "935 - Simplified Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS", "937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended Mixed DBCS",
"933 - Korean Mixed DBCS" "933 - Korean Mixed DBCS"
}; };
codePageCombo = new JComboBox<>(commonCodePages); codePageCombo = new JComboBox<>(commonCodePages);
@@ -730,6 +730,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
} else if (e.getID() == KeyEvent.KEY_PRESSED) { } else if (e.getID() == KeyEvent.KEY_PRESSED) {
if (client.getNvtProcessor() != null && client.getNvtProcessor().isApplicationKeypad()) {
String kp = client.getNvtProcessor().mapKeypadKey(e.getKeyCode());
if (kp != null) {
try {
client.sendNVTString(kp);
} catch (Exception ignored) {}
e.consume();
return;
}
}
if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) { if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
int code = e.getKeyCode(); int code = e.getKeyCode();
if (code >= KeyEvent.VK_A && code <= KeyEvent.VK_Z) { if (code >= KeyEvent.VK_A && code <= KeyEvent.VK_Z) {
@@ -838,12 +848,20 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) { if (client.getConnectionState().isNvt()) {
try { try {
switch (dir) { int vk = KeyEvent.VK_UP;
case "up": client.sendNVTString("\u001B[A"); break; if ("down".equals(dir)) vk = KeyEvent.VK_DOWN;
case "down": client.sendNVTString("\u001B[B"); break; else if ("left".equals(dir)) vk = KeyEvent.VK_LEFT;
case "left": client.sendNVTString("\u001B[D"); break; else if ("right".equals(dir)) vk = KeyEvent.VK_RIGHT;
case "right": client.sendNVTString("\u001B[C"); break; else if ("home".equals(dir)) vk = KeyEvent.VK_HOME;
case "home": client.sendNVTString("\u001B[H"); break; boolean handled = client.sendNVTKey(vk, '\0', false, false, false);
if (!handled) {
switch (dir) {
case "up": client.sendNVTString("\u001B[A"); break;
case "down": client.sendNVTString("\u001B[B"); break;
case "left": client.sendNVTString("\u001B[D"); break;
case "right": client.sendNVTString("\u001B[C"); break;
case "home": client.sendNVTString("\u001B[H"); break;
}
} }
} catch (Exception ignored) {} } catch (Exception ignored) {}
return; return;
@@ -863,7 +881,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) { if (client.getConnectionState().isNvt()) {
try { try {
client.sendNVTString(getNvtFunctionKeySequence(n)); if (client.getNvtProcessor() != null) {
client.sendNVTString(client.getNvtProcessor().getFunctionKeySequence(n));
} else {
client.sendNVTString(getNvtFunctionKeySequence(n));
}
} catch (Exception ignored) {} } catch (Exception ignored) {}
return; return;
} }
@@ -1093,6 +1115,37 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (client != null) { if (client != null) {
setupGraphicsPlaneRenderer(); setupGraphicsPlaneRenderer();
updateCellSize(); updateCellSize();
client.setNvtClipboardHandler(new haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler() {
@Override
public String getClipboardText() {
try {
return (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor);
} catch (Exception e) {
return "";
}
}
@Override
public void setClipboardText(String text) {
if (text != null) {
try {
StringSelection ss = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
} catch (Exception ignored) {}
}
}
});
client.addNvtTitleListener(title -> {
SwingUtilities.invokeLater(() -> {
Container top = getTopLevelAncestor();
if (top instanceof JFrame) {
((JFrame) top).setTitle(title != null && !title.isEmpty() ? ("j3270 - " + title) : "j3270");
}
});
});
} }
} }
@@ -59,4 +59,32 @@ public enum ConnectionState {
public boolean isFullSession() { public boolean isFullSession() {
return isNvt() || is3270(); return isNvt() || is3270();
} }
/**
* Map to IBM Host On-Demand ECL connection state integer codes.
* 0 = Disconnected, 1 = Connecting/Resolving, 2 = Connected (NVT/Unbound), 3 = Bound (Full 3270 session).
*/
public int toHoDStateCode() {
switch (this) {
case NOT_CONNECTED:
return 0;
case RECONNECTING:
case RESOLVING:
case TCP_PENDING:
case TLS_PENDING:
case PROXY_PENDING:
case TELNET_PENDING:
return 1;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_UNBOUND:
case CONNECTED_E_NVT:
case CONNECTED_SSCP:
return 2;
case CONNECTED_3270:
case CONNECTED_TN3270E:
default:
return 3;
}
}
} }
@@ -63,11 +63,21 @@ public class Telnet3270Client {
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane()); inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder()); inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
// Wire screen update to ECLXfer for CUT mode screen tracking // Wire screen update to ECLXfer for CUT mode screen tracking and ECLPS for event dispatching
addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() { addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
@Override public void onScreenUpdated() { xfer.onScreenUpdated(); } @Override public void onScreenUpdated() {
@Override public void onScreenSizeChanged(int rows, int cols) {} xfer.onScreenUpdated();
@Override public void onSoundAlarm() {} ps.notifyPSUpdate(0, 0, screenBuffer.getRows() - 1, screenBuffer.getCols() - 1, true);
}
@Override public void onCursorMoved(int oldAddress, int newAddress) {
ps.notifyCursorMoved(oldAddress, newAddress);
}
@Override public void onScreenSizeChanged(int rows, int cols) {
ps.notifyScreenResized(rows, cols);
}
@Override public void onSoundAlarm() {
ps.notifyAlarm();
}
}); });
} }
@@ -90,6 +100,52 @@ public class Telnet3270Client {
fsm.onConnected(); fsm.onConnected();
} }
/**
* Connect to the configured host synchronously, blocking until the full data session is established
* (or timeout expires).
* @param timeoutMs maximum time to wait in milliseconds
* @return true if successfully connected, false if timed out
* @throws IOException if network connection fails
*/
public boolean connect(long timeoutMs) throws IOException {
connect();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
ConnectionState state = getConnectionState();
if (state.isFullSession() || (state.isFullyConnected() && isConnected())) {
return true;
}
if (state == ConnectionState.NOT_CONNECTED && !isConnected()) {
return false;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getConnectionState().isFullSession() || isConnected();
}
/**
* Connect to the configured host synchronously and block until the presentation space matches
* the specified screen descriptor.
* @param timeoutMs maximum time to wait in milliseconds
* @param desc ECLScreenDesc descriptor to match against
* @return true if successfully connected and screen matched, false otherwise
* @throws IOException if network connection fails
*/
public boolean connect(long timeoutMs, haus.nightmare.lib3270j.ecl.ECLScreenDesc desc) throws IOException {
long start = System.currentTimeMillis();
if (!connect(timeoutMs)) {
return false;
}
long elapsed = System.currentTimeMillis() - start;
long remaining = Math.max(1, timeoutMs - elapsed);
return ps.waitForScreen(desc, remaining);
}
/** /**
* Disconnect from the host. * Disconnect from the host.
*/ */
@@ -101,6 +157,35 @@ public class Telnet3270Client {
fsm.onDisconnect(); fsm.onDisconnect();
} }
/**
* Disconnect from host with synchronous teardown.
* @param timeoutMs maximum time to wait for graceful disconnect in milliseconds
*/
public void disconnect(long timeoutMs) {
disconnect();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getConnectionState() == ConnectionState.NOT_CONNECTED && !isConnected()) {
break;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
/** HoD StopCommunication compatibility alias. */
public void stopCommunication() {
disconnect();
}
public void stopCommunication(long timeoutMs) {
disconnect(timeoutMs);
}
/** /**
* Check if connected (any state past TCP pending). * Check if connected (any state past TCP pending).
*/ */
@@ -114,11 +199,20 @@ public class Telnet3270Client {
fsm.addConnectionListener(l); fsm.addConnectionListener(l);
} }
public void removeConnectionListener(ConnectionListener l) {
fsm.removeConnectionListener(l);
}
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
fsm.addScreenUpdateListener(l); fsm.addScreenUpdateListener(l);
dsProcessor.addScreenUpdateListener(l); dsProcessor.addScreenUpdateListener(l);
} }
public void removeScreenUpdateListener(ScreenUpdateListener l) {
fsm.removeScreenUpdateListener(l);
dsProcessor.removeScreenUpdateListener(l);
}
public void addSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) { public void addSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
fsm.addSCSInboundListener(l); fsm.addSCSInboundListener(l);
} }
@@ -198,6 +292,37 @@ public class Telnet3270Client {
fsm.sendNVTString(s); fsm.sendNVTString(s);
} }
/** Send an NVT key event mapped through NVT processor. */
public boolean sendNVTKey(int keyCode, char keyChar, boolean shift, boolean ctrl, boolean alt) throws IOException {
haus.nightmare.lib3270j.nvt.NvtProcessor nvt = getNvtProcessor();
if (nvt != null) {
String seq = nvt.mapKey(keyCode, keyChar, shift, ctrl, alt);
if (seq != null && !seq.isEmpty()) {
fsm.sendNvtData(seq.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
return true;
}
}
return false;
}
public void addNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
if (getNvtProcessor() != null) {
getNvtProcessor().addTitleListener(l);
}
}
public void removeNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
if (getNvtProcessor() != null) {
getNvtProcessor().removeTitleListener(l);
}
}
public void setNvtClipboardHandler(haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler h) {
if (getNvtProcessor() != null) {
getNvtProcessor().setClipboardHandler(h);
}
}
// ========== Convenience input methods & HoD compatibility ========== // ========== Convenience input methods & HoD compatibility ==========
/** Type a character at the cursor position. */ /** Type a character at the cursor position. */
@@ -106,6 +106,19 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override @Override
public String ebcdicToString(byte[] ebcdic, int offset, int length) { public String ebcdicToString(byte[] ebcdic, int offset, int length) {
return ebcdicToString(ebcdic, offset, length, false);
}
/**
* Translate an EBCDIC byte array slice to a Unicode String with optional SO/SI escape preservation.
*
* @param ebcdic source byte array
* @param offset start offset
* @param length number of bytes
* @param preserveSOSI if true, preserves Shift-Out (\u000E) and Shift-In (\u000F) control characters
* @return translated Unicode String
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length, boolean preserveSOSI) {
if (ebcdic == null || length <= 0) return ""; if (ebcdic == null || length <= 0) return "";
StringBuilder sb = new StringBuilder(length); StringBuilder sb = new StringBuilder(length);
@@ -117,9 +130,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int b = ebcdic[i] & 0xFF; int b = ebcdic[i] & 0xFF;
if (b == SO) { if (b == SO) {
inDBCS = true; inDBCS = true;
if (preserveSOSI) {
sb.append((char) SO);
}
i++; i++;
} else if (b == SI) { } else if (b == SI) {
inDBCS = false; inDBCS = false;
if (preserveSOSI) {
sb.append((char) SI);
}
i++; i++;
} else if (inDBCS) { } else if (inDBCS) {
if (i + 1 < end) { if (i + 1 < end) {
@@ -127,13 +146,22 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
if (b2 == SI) { if (b2 == SI) {
// Orphaned single byte before SI // Orphaned single byte before SI
inDBCS = false; inDBCS = false;
if (preserveSOSI) {
sb.append(ebcdicToUnicode(b));
sb.append((char) SI);
}
i += 2; i += 2;
} else if (b2 == SO) {
// Unmatched byte followed by another SO
sb.append(ebcdicToUnicode(b));
i++;
} else { } else {
sb.append(dbcsToUnicode(b, b2)); sb.append(dbcsToUnicode(b, b2));
i += 2; i += 2;
} }
} else { } else {
// Trailing byte // Trailing single byte inside unclosed DBCS shift
sb.append(ebcdicToUnicode(b));
i++; i++;
} }
} else { } else {
@@ -147,14 +175,39 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override @Override
public byte[] stringToEbcdic(String s) { public byte[] stringToEbcdic(String s) {
return stringToEbcdic(s, true);
}
/**
* Translate a Unicode String to EBCDIC byte array with optional parsing of embedded SO/SI markers.
*
* @param s input Unicode string
* @param parseSOSIMarkers whether to interpret embedded \u000E (SO) and \u000F (SI) characters
* @return EBCDIC byte array with balanced SO/SI framing
*/
public byte[] stringToEbcdic(String s, boolean parseSOSIMarkers) {
if (s == null || s.isEmpty()) return new byte[0]; if (s == null || s.isEmpty()) return new byte[0];
// Manual state machine encoding
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2); ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
boolean inDBCS = false; boolean inDBCS = false;
for (int i = 0; i < s.length(); i++) { for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i); char c = s.charAt(i);
if (parseSOSIMarkers && c == (char) SO) {
if (!inDBCS) {
out.write(SO);
inDBCS = true;
}
continue;
} else if (parseSOSIMarkers && c == (char) SI) {
if (inDBCS) {
out.write(SI);
inDBCS = false;
}
continue;
}
int dbcsCode = unicodeToDbcs(c); int dbcsCode = unicodeToDbcs(c);
if (dbcsCode >= 0) { if (dbcsCode >= 0) {
@@ -175,10 +228,26 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
} }
} }
// Auto-close unclosed DBCS shift sequence
if (inDBCS) { if (inDBCS) {
out.write(SI); out.write(SI);
} }
return out.toByteArray(); return out.toByteArray();
} }
/**
* Helper to decode an EBCDIC byte array to UTF-8 String preserving SO/SI control codes.
*/
public String ebcdicToUtf8WithSOSI(byte[] ebcdic) {
if (ebcdic == null) return "";
return ebcdicToString(ebcdic, 0, ebcdic.length, true);
}
/**
* Helper to encode a UTF-8/Unicode String with embedded SO/SI markers to EBCDIC.
*/
public byte[] utf8WithSOSIToEbcdic(String s) {
return stringToEbcdic(s, true);
}
} }
@@ -53,6 +53,9 @@ public class CodePageRegistry {
register(new Cp937.Cp1371()); register(new Cp937.Cp1371());
register(new Cp933()); register(new Cp933());
// Register Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS)
registerExtendedCodepages();
// Setup common aliases // Setup common aliases
addAlias("us", "037"); addAlias("us", "037");
addAlias("usa", "037"); addAlias("usa", "037");
@@ -218,6 +221,100 @@ public class CodePageRegistry {
return CODE_PAGES.get("037"); return CODE_PAGES.get("037");
} }
/**
* Register extended EBCDIC codepages from HoD v14 converters:
* - Arabic: Cp420, Cp424
* - Hebrew: Cp424, Cp803
* - Thai: Cp838, Cp1160
* - Cyrillic: Cp1025, Cp1123, Cp1154, Cp880
* - Turkish: Cp1155, Cp905
* - Extended Chinese DBCS: Cp1388, Cp1371
*/
public static void registerExtendedCodepages() {
// Arabic & Hebrew
register(new Cp420());
register(new Cp424());
register(new Cp803());
// Thai
register(new Cp838());
register(new Cp1160());
// Cyrillic / Russian / Ukrainian
register(new Cp1025());
register(new Cp1123());
register(new Cp1154());
register(new Cp880());
// Turkish
register(new Cp1155());
register(new Cp905());
// Extended Chinese DBCS
register(new Cp1388());
register(new Cp1371());
// Setup Extended Aliases
addAlias("ar", "420");
addAlias("arabic", "420");
addAlias("ebcdic-cp-ar", "420");
addAlias("he", "424");
addAlias("hebrew", "424");
addAlias("hebrew-lowercase", "424");
addAlias("ebcdic-cp-he", "424");
addAlias("hebrew-old", "803");
addAlias("israel", "803");
addAlias("iw", "803");
addAlias("ebcdic-cp-he-old", "803");
addAlias("th", "838");
addAlias("thai", "838");
addAlias("ebcdic-cp-th", "838");
addAlias("thai-euro", "1160");
addAlias("ru", "1025");
addAlias("russian", "1025");
addAlias("cyrillic", "1025");
addAlias("ebcdic-cp-ru", "1025");
addAlias("ukraine", "1123");
addAlias("ukrainian", "1123");
addAlias("ebcdic-cp-ua", "1123");
addAlias("cyrillic-euro", "1154");
addAlias("ru-euro", "1154");
addAlias("russian-euro", "1154");
addAlias("cyrillic-russian", "880");
addAlias("ru-old", "880");
addAlias("turkish-euro", "1155");
addAlias("tr-euro", "1155");
addAlias("turkish-latin3", "905");
addAlias("tr-latin3", "905");
addAlias("chinese-ext-simplified", "1388");
addAlias("zh-simplified-ext", "1388");
addAlias("zh-ext", "1388");
addAlias("chinese-ext-traditional", "1371");
addAlias("zh-traditional-ext", "1371");
addAlias("zh-tw-ext", "1371");
}
/**
* Check if a code page is registered.
*/
public static boolean hasCodePage(String name) {
if (name == null || name.trim().isEmpty()) return false;
String raw = name.trim();
if (CODE_PAGES.containsKey(raw)) return true;
String norm = normalizeKey(raw);
if (CODE_PAGES.containsKey(norm)) return true;
return ALIASES.containsKey(norm);
}
/** /**
* Get an unmodifiable list of all registered built-in CodePages. * Get an unmodifiable list of all registered built-in CodePages.
*/ */
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1025 (Cyrillic Multilingual EBCDIC - Russian, Bulgarian, Belarusian, Serbian, Macedonian).
* CCSID / CPGID: 1025, GCSGID: 1150.
*/
public class Cp1025 extends AbstractCodePage {
public static final String ID = "1025";
public static final String DESCRIPTION = "Cyrillic Multilingual EBCDIC";
public static final int CPGID = 1025;
public static final int GCSGID = 1150;
public static final int[] MAPPING = {
// 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
0x0020, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0453,
0x0452, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0454, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A,
// 90-9F
0x045B, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x045C, 0x045E, 0x045F, 0x0401, 0x0402, 0x0403,
// A0-AF
0x0404, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A,
// B0-BF
0x040B, 0x00A3, 0x040C, 0x00B7, 0x00A9, 0x040E, 0x040F, 0x0410,
0x0411, 0x0412, 0x00AC, 0x007C, 0x0413, 0x0414, 0x0415, 0x0416,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
};
public Cp1025() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1025", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1123 (Cyrillic Ukraine EBCDIC).
* CCSID / CPGID: 1123, GCSGID: 1399.
*/
public class Cp1123 extends AbstractCodePage {
public static final String ID = "1123";
public static final String DESCRIPTION = "Cyrillic Ukraine EBCDIC";
public static final int CPGID = 1123;
public static final int GCSGID = 1399;
public static final int[] MAPPING = {
// 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
0x0020, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0491,
0x0454, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0456, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0457, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
};
public Cp1123() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1123", MAPPING);
}
}
@@ -0,0 +1,24 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1154 (Cyrillic Multilingual with Euro \u20AC).
* CCSID / CPGID: 1154, GCSGID: 1305.
*/
public class Cp1154 extends AbstractCodePage {
public static final String ID = "1154";
public static final String DESCRIPTION = "Cyrillic Multilingual EBCDIC (with Euro \u20AC)";
public static final int CPGID = 1154;
public static final int GCSGID = 1305;
public static final int[] MAPPING = new int[256];
static {
System.arraycopy(Cp1025.MAPPING, 0, MAPPING, 0, 256);
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
}
public Cp1154() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1154", MAPPING);
}
}
@@ -0,0 +1,24 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1155 (Turkey - Turkish Latin-5 with Euro \u20AC).
* CCSID / CPGID: 1155, GCSGID: 1306.
*/
public class Cp1155 extends AbstractCodePage {
public static final String ID = "1155";
public static final String DESCRIPTION = "Turkey - Turkish Latin-5 (with Euro \u20AC)";
public static final int CPGID = 1155;
public static final int GCSGID = 1306;
public static final int[] MAPPING = new int[256];
static {
System.arraycopy(Cp1026.MAPPING, 0, MAPPING, 0, 256);
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
}
public Cp1155() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1155", MAPPING);
}
}
@@ -0,0 +1,24 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1160 (Thai EBCDIC with Euro \u20AC).
* CCSID / CPGID: 1160, GCSGID: 1176.
*/
public class Cp1160 extends AbstractCodePage {
public static final String ID = "1160";
public static final String DESCRIPTION = "Thai EBCDIC (with Euro \u20AC)";
public static final int CPGID = 1160;
public static final int GCSGID = 1176;
public static final int[] MAPPING = new int[256];
static {
System.arraycopy(Cp838.MAPPING, 0, MAPPING, 0, 256);
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
}
public Cp1160() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1160", MAPPING);
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1371 (Traditional Chinese Extended Mixed DBCS).
* CCSID / CPGID: 1371, GCSGID: 1174.
*/
public class Cp1371 extends AbstractDBCSCodePage {
public static final String ID = "1371";
public static final String DESCRIPTION = "Traditional Chinese Extended Mixed DBCS";
public static final int CPGID = 1371;
public static final int GCSGID = 1174;
public Cp1371() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1371");
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1388 (Simplified Chinese Extended Mixed DBCS).
* CCSID / CPGID: 1388, GCSGID: 1175.
*/
public class Cp1388 extends AbstractDBCSCodePage {
public static final String ID = "1388";
public static final String DESCRIPTION = "Simplified Chinese Extended Mixed DBCS";
public static final int CPGID = 1388;
public static final int GCSGID = 1175;
public Cp1388() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1388");
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 420 (Arabic Bilingual EBCDIC).
* CCSID / CPGID: 420, GCSGID: 235.
*/
public class Cp420 extends AbstractCodePage {
public static final String ID = "420";
public static final String DESCRIPTION = "Arabic Bilingual EBCDIC";
public static final int CPGID = 420;
public static final int GCSGID = 235;
public static final int[] MAPPING = {
// 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
0x0020, 0x0640, 0xFE83, 0xFE87, 0xFE85, 0xFE81, 0xFE80, 0x0621,
0xFE8D, 0xFE8B, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0xFE8E, 0xFE8F, 0xFE91, 0xFE93, 0xFE95, 0xFE97, 0xFE99,
0xFE9B, 0xFE9D, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0xFE9F, 0xFEA1, 0xFEA3, 0xFEA5, 0xFEA7, 0xFEA9,
0xFEAB, 0xFEAD, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0xFEAF, 0xFEB1, 0xFEB3, 0xFEB5, 0xFEB7, 0xFEB9, 0xFEBB, 0xFEBD,
0xFEBF, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0xFEC1, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0xFEC3, 0xFEC5, 0xFEC7, 0xFEC9, 0xFECB, 0xFECD,
// 90-9F
0xFECF, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0xFED1, 0xFED3, 0xFED5, 0xFED7, 0xFED9, 0xFEDB,
// A0-AF
0xFEDD, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0xFEDF, 0xFEE1, 0xFEE3, 0xFEE5, 0xFEE7, 0xFEE9,
// B0-BF
0xFEEB, 0xFEED, 0xFEEF, 0xFEF1, 0xFEF3, 0xFEF5, 0xFEF7, 0xFEF9,
0xFEFB, 0x00AE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x060C, 0x061B, 0x061F, 0x0628, 0x062A, 0x062B,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631,
// E0-EF
0x005C, 0x0632, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, 0x0638,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0639, 0x063A, 0x0641, 0x0642, 0x0643, 0x009F,
};
public Cp420() {
super(ID, DESCRIPTION, CPGID, GCSGID, "420", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 424 (Hebrew with Lowercase EBCDIC).
* CCSID / CPGID: 424, GCSGID: 941.
*/
public class Cp424 extends AbstractCodePage {
public static final String ID = "424";
public static final String DESCRIPTION = "Hebrew (with Lowercase) EBCDIC";
public static final int CPGID = 424;
public static final int GCSGID = 941;
public static final int[] MAPPING = {
// 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
0x0020, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6,
0x05D7, 0x05D8, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
0x05E0, 0x05E1, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7,
0x05E8, 0x05E9, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x05EA, 0x00A0, 0x2017, 0x00B8, 0x00A8, 0x00B4, 0x00AA, 0x00BA,
0x00DF, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp424() {
super(ID, DESCRIPTION, CPGID, GCSGID, "424", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 803 (Hebrew Old / Standard EBCDIC).
* CCSID / CPGID: 803, GCSGID: 1147.
*/
public class Cp803 extends AbstractCodePage {
public static final String ID = "803";
public static final String DESCRIPTION = "Hebrew Old / Standard EBCDIC";
public static final int CPGID = 803;
public static final int GCSGID = 1147;
public static final int[] MAPPING = {
// 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
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 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
0x00D8, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6,
0x05D7, 0x05D8, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
0x05E0, 0x05E1, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7,
0x05E8, 0x05E9, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x05EA, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp803() {
super(ID, DESCRIPTION, CPGID, GCSGID, "803", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 838 (Thai EBCDIC).
* CCSID / CPGID: 838, GCSGID: 1176.
*/
public class Cp838 extends AbstractCodePage {
public static final String ID = "838";
public static final String DESCRIPTION = "Thai EBCDIC";
public static final int CPGID = 838;
public static final int GCSGID = 1176;
public static final int[] MAPPING = {
// 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
0x0020, 0x0E01, 0x0E02, 0x0E03, 0x0E04, 0x0E05, 0x0E06, 0x0E07,
0x0E08, 0x0E09, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0E0A, 0x0E0B, 0x0E0C, 0x0E0D, 0x0E0E, 0x0E0F, 0x0E10,
0x0E11, 0x0E12, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x0E13, 0x0E14, 0x0E15, 0x0E16, 0x0E17, 0x0E18,
0x0E19, 0x0E1A, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0E1B, 0x0E1C, 0x0E1D, 0x0E1E, 0x0E1F, 0x0E20, 0x0E21, 0x0E22,
0x0E23, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0E24, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0E25, 0x0E26, 0x0E27, 0x0E28, 0x0E29, 0x0E2A,
// 90-9F
0x0E2B, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x0E2C, 0x0E2D, 0x0E2E, 0x0E2F, 0x0E30, 0x0E31,
// A0-AF
0x0E32, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x0E33, 0x0E34, 0x0E35, 0x0E36, 0x0E37, 0x0E38,
// B0-BF
0x0E39, 0x0E3A, 0x0E40, 0x0E41, 0x0E42, 0x0E43, 0x0E44, 0x0E45,
0x0E46, 0x0E47, 0x00AC, 0x007C, 0x0E48, 0x0E49, 0x0E4A, 0x0E4B,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0E4C, 0x0E4D, 0x0E4E, 0x0E4F, 0x0E50, 0x0E51,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x0E52, 0x0E53, 0x0E54, 0x0E55, 0x0E56, 0x0E57,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0E58, 0x0E59, 0x0E5A, 0x0E5B, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp838() {
super(ID, DESCRIPTION, CPGID, GCSGID, "838", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 880 (Cyrillic Russian EBCDIC).
* CCSID / CPGID: 880, GCSGID: 960.
*/
public class Cp880 extends AbstractCodePage {
public static final String ID = "880";
public static final String DESCRIPTION = "Cyrillic Russian EBCDIC";
public static final int CPGID = 880;
public static final int GCSGID = 960;
public static final int[] MAPPING = {
// 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
0x0020, 0x00A0, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A,
0x0449, 0x0448, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0447, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441,
0x0440, 0x043F, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043E, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439,
0x0438, 0x0437, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0436, 0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451,
0x045E, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x045F, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0456, 0x0458, 0x0459, 0x045A, 0x045C, 0x045B,
// 90-9F
0x0402, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x0403, 0x0404, 0x0405, 0x0406, 0x0408, 0x0409,
// A0-AF
0x040A, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x040C, 0x040B, 0x040F, 0x040E, 0x0401, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0410,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x009F,
};
public Cp880() {
super(ID, DESCRIPTION, CPGID, GCSGID, "880", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 905 (Turkey - Turkish Latin-3 EBCDIC).
* CCSID / CPGID: 905, GCSGID: 1151.
*/
public class Cp905 extends AbstractCodePage {
public static final String ID = "905";
public static final String DESCRIPTION = "Turkey - Turkish Latin-3 EBCDIC";
public static final int CPGID = 905;
public static final int GCSGID = 1151;
public static final int[] MAPPING = {
// 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
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x011E, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0130, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x015E, 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
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x011F, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x0131, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x015F, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp905() {
super(ID, DESCRIPTION, CPGID, GCSGID, "905", MAPPING);
}
}
@@ -1,10 +1,13 @@
package haus.nightmare.lib3270j.charset; package haus.nightmare.lib3270j.charset;
import java.util.List; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* EBCDIC Unicode character translator conforming to IBM Host On-Demand (HoD v14). * EBCDIC Unicode character translator conforming to IBM Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS). * Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
* Supports custom per-instance character translation override tables and complete
* IBM 3270 APL / Graphic Escape (GA23-0059) character mappings.
* Default: Code Page 037 (US/Canada EBCDIC). * Default: Code Page 037 (US/Canada EBCDIC).
*/ */
public class EbcdicTranslator { public class EbcdicTranslator {
@@ -15,6 +18,8 @@ public class EbcdicTranslator {
public static final int[] CP037_TO_UNICODE = Cp037.MAPPING; public static final int[] CP037_TO_UNICODE = Cp037.MAPPING;
private CodePage activeCodePage; private CodePage activeCodePage;
private final Map<Integer, Character> customEbcdicToUnicode = new ConcurrentHashMap<>();
private final Map<Character, Integer> customUnicodeToEbcdic = new ConcurrentHashMap<>();
public EbcdicTranslator() { public EbcdicTranslator() {
this("037"); this("037");
@@ -85,6 +90,107 @@ public class EbcdicTranslator {
return isDBCSCodePage(); return isDBCSCodePage();
} }
// =========================================================================
// Custom Character Translation Overrides
// =========================================================================
/**
* Set a bidirectional custom character translation override.
* Maps an EBCDIC byte value to a Unicode character, and vice versa.
*/
public synchronized void setCustomOverride(int ebcdicByte, char unicodeChar) {
int b = ebcdicByte & 0xFF;
customEbcdicToUnicode.put(b, unicodeChar);
customUnicodeToEbcdic.put(unicodeChar, b);
}
/**
* Set a one-way custom EBCDIC byte to Unicode character override.
*/
public synchronized void setCustomEbcdicToUnicodeOverride(int ebcdicByte, char unicodeChar) {
customEbcdicToUnicode.put(ebcdicByte & 0xFF, unicodeChar);
}
/**
* Set a one-way custom Unicode character to EBCDIC byte override.
*/
public synchronized void setCustomUnicodeToEbcdicOverride(char unicodeChar, int ebcdicByte) {
customUnicodeToEbcdic.put(unicodeChar, ebcdicByte & 0xFF);
}
/**
* Set custom translation overrides in bulk.
*/
public synchronized void setCustomOverrides(Map<Integer, Character> ebcToUni, Map<Character, Integer> uniToEbc) {
if (ebcToUni != null) {
for (Map.Entry<Integer, Character> entry : ebcToUni.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
setCustomOverride(entry.getKey(), entry.getValue());
}
}
}
if (uniToEbc != null) {
for (Map.Entry<Character, Integer> entry : uniToEbc.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
setCustomUnicodeToEbcdicOverride(entry.getKey(), entry.getValue());
}
}
}
}
/**
* Remove custom translation override for a specific EBCDIC byte.
*/
public synchronized void removeCustomOverride(int ebcdicByte) {
Character removed = customEbcdicToUnicode.remove(ebcdicByte & 0xFF);
if (removed != null) {
customUnicodeToEbcdic.remove(removed);
}
}
/**
* Remove custom translation override for a specific Unicode character.
*/
public synchronized void removeCustomUnicodeOverride(char unicodeChar) {
Integer removed = customUnicodeToEbcdic.remove(unicodeChar);
if (removed != null) {
customEbcdicToUnicode.remove(removed);
}
}
/**
* Clear all registered custom translation overrides.
*/
public synchronized void clearCustomOverrides() {
customEbcdicToUnicode.clear();
customUnicodeToEbcdic.clear();
}
/**
* Check if any custom translation overrides are active.
*/
public boolean hasCustomOverrides() {
return !customEbcdicToUnicode.isEmpty() || !customUnicodeToEbcdic.isEmpty();
}
/**
* Get an unmodifiable view of active EBCDIC-to-Unicode custom overrides.
*/
public Map<Integer, Character> getCustomEbcdicToUnicodeOverrides() {
return Collections.unmodifiableMap(new HashMap<>(customEbcdicToUnicode));
}
/**
* Get an unmodifiable view of active Unicode-to-EBCDIC custom overrides.
*/
public Map<Character, Integer> getCustomUnicodeToEbcdicOverrides() {
return Collections.unmodifiableMap(new HashMap<>(customUnicodeToEbcdic));
}
// =========================================================================
// Translation Operations
// =========================================================================
/** /**
* Translate double-byte EBCDIC pair (b1, b2) to Unicode. * Translate double-byte EBCDIC pair (b1, b2) to Unicode.
*/ */
@@ -100,10 +206,15 @@ public class EbcdicTranslator {
} }
/** /**
* Translate EBCDIC byte to Unicode character using active code page. * Translate EBCDIC byte to Unicode character using active code page and custom overrides.
*/ */
public char ebcdicToUnicode(int ebc) { public char ebcdicToUnicode(int ebc) {
return activeCodePage.ebcdicToUnicode(ebc); int b = ebc & 0xFF;
Character custom = customEbcdicToUnicode.get(b);
if (custom != null) {
return custom;
}
return activeCodePage.ebcdicToUnicode(b);
} }
/** /**
@@ -121,10 +232,14 @@ public class EbcdicTranslator {
} }
/** /**
* Translate Unicode character to EBCDIC byte using active code page. * Translate Unicode character to EBCDIC byte using active code page and custom overrides.
* Returns -1 if the character cannot be mapped. * Returns -1 if the character cannot be mapped.
*/ */
public int unicodeToEbcdic(char unicode) { public int unicodeToEbcdic(char unicode) {
Integer custom = customUnicodeToEbcdic.get(unicode);
if (custom != null) {
return custom;
}
return activeCodePage.unicodeToEbcdic(unicode); return activeCodePage.unicodeToEbcdic(unicode);
} }
@@ -132,20 +247,37 @@ public class EbcdicTranslator {
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable. * Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/ */
public byte unicodeToEbcdicSafe(char unicode) { public byte unicodeToEbcdicSafe(char unicode) {
return activeCodePage.unicodeToEbcdicSafe(unicode); int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
} }
/** /**
* Translate a byte array from EBCDIC to a Unicode string using active code page. * Translate a byte array from EBCDIC to a Unicode string using active code page and overrides.
*/ */
public String ebcdicToString(byte[] ebcdic, int offset, int length) { public String ebcdicToString(byte[] ebcdic, int offset, int length) {
if (ebcdic == null || length <= 0) return "";
if (hasCustomOverrides() || !(activeCodePage instanceof AbstractDBCSCodePage)) {
char[] chars = new char[length];
for (int i = 0; i < length; i++) {
chars[i] = ebcdicToUnicode(ebcdic[offset + i]);
}
return new String(chars);
}
return activeCodePage.ebcdicToString(ebcdic, offset, length); return activeCodePage.ebcdicToString(ebcdic, offset, length);
} }
/** /**
* Translate a Unicode string to EBCDIC byte array using active code page. * Translate a Unicode string to EBCDIC byte array using active code page and overrides.
*/ */
public byte[] stringToEbcdic(String s) { public byte[] stringToEbcdic(String s) {
if (s == null || s.isEmpty()) return new byte[0];
if (hasCustomOverrides() || !(activeCodePage instanceof AbstractDBCSCodePage)) {
byte[] bytes = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
bytes[i] = unicodeToEbcdicSafe(s.charAt(i));
}
return bytes;
}
return activeCodePage.stringToEbcdic(s); return activeCodePage.stringToEbcdic(s);
} }
@@ -169,7 +301,7 @@ public class EbcdicTranslator {
*/ */
public char mapAPL(int ebcdicCodePoint) { public char mapAPL(int ebcdicCodePoint) {
switch (ebcdicCodePoint & 0xFF) { switch (ebcdicCodePoint & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL) // Standard Box-Drawing Lines, Corners, T-Junctions, and Crosses
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─' case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│' case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌' case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
@@ -182,7 +314,7 @@ public class EbcdicTranslator {
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴' case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼' case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c) // Mathematical Relations and Punctuation
case 0x8C: return '\u2264'; // Less-than or equal '≤' case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥' case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠' case 0xBE: return '\u2260'; // Not equal '≠'
@@ -194,11 +326,58 @@ public class EbcdicTranslator {
case 0xB1: return '\u00B1'; // Plus-minus '±' case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²' case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³' case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯' case 0xAF: return '\u00AF'; // Overbar / High Minus '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω' case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ' case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬' case 0x5F: return '\u00AC'; // Not sign '¬'
// IBM 3270 APL Operational & Structural Glyphs
case 0x80: return '\u22C4'; // Diamond '⋄'
case 0x81: return '\u237A'; // APL Alpha ''
case 0x82: return '\u22A5'; // Up Tack / Decode '⊥'
case 0x83: return '\u2229'; // Intersection '∩'
case 0x84: return '\u230A'; // Floor '⌊'
case 0x86: return '\u2286'; // Subset or equal '⊆'
case 0x87: return '\u2207'; // Del / Grad '∇'
case 0x88: return '\u2206'; // Delta '∆'
case 0x89: return '\u2373'; // Iota ''
case 0x8A: return '\u2192'; // Right Arrow '→'
case 0x8B: return '\u235E'; // Quote Quad '⍞'
case 0x8E: return '\u00D7'; // Multiply '×'
case 0x8F: return '\u00F7'; // Divide '÷'
case 0x90: return '\u235F'; // Circle Star / Log '⍟'
case 0x91: return '\u2339'; // Quad Divide / Domino '⌹'
case 0x92: return '\u22A4'; // Down Tack / Encode ''
case 0x93: return '\u222A'; // Union ''
case 0x94: return '\u2308'; // Ceiling '⌈'
case 0x95: return '\u2374'; // Rho / Shape ''
case 0x96: return '\u2375'; // APL Omega '⍵'
case 0x97: return '\u2260'; // Not equal '≠'
case 0x98: return '\u2377'; // Epsilon Underbar '⍷'
case 0x99: return '\u25CB'; // Circle '○'
case 0x9A: return '\u2190'; // Left Arrow '←'
case 0x9B: return '\u2359'; // Delta Underbar '⍙'
case 0x9C: return '\u234B'; // Grade Up '⍋'
case 0x9E: return '\u2352'; // Grade Down '⍒'
case 0x9F: return '\u235D'; // Lamp / Comment '⍝'
case 0xA1: return '\u00A8'; // Diaeresis '¨'
case 0xA4: return '\u2336'; // I-Beam '⌶'
case 0xA5: return '\u2355'; // Thorn / Format '⍕'
case 0xA6: return '\u2282'; // Left Shoe / Enclose '⊂'
case 0xA7: return '\u2283'; // Right Shoe / Disclose '⊃'
case 0xA8: return '\u2191'; // Up Arrow / Take '↑'
case 0xA9: return '\u2193'; // Down Arrow / Drop '↓'
case 0xAA: return '\u2395'; // Quad '⎕'
case 0xAB: return '\u234E'; // Execute / Hydra '⍎'
case 0xAC: return '\u2349'; // Transpose / Circle Slope '⍉'
case 0xB4: return '\u2296'; // Circle Bar / Reverse '⊖'
case 0xB5: return '\u236A'; // Comma Bar / Table '⍪'
case 0xB6: return '\u236B'; // Del Tilde '⍫'
case 0xB7: return '\u236C'; // Zilde '⍬'
case 0xB9: return '\u233F'; // Slash Bar '⌿'
case 0xBB: return '\u2340'; // Backslash Bar '⍀'
case 0xBC: return '\u2338'; // Quad Equal '⌸'
default: return ebcdicToUnicode(ebcdicCodePoint & 0xFF); default: return ebcdicToUnicode(ebcdicCodePoint & 0xFF);
} }
} }
@@ -109,6 +109,10 @@ public class DataStreamProcessor {
screenListeners.add(l); screenListeners.add(l);
} }
public void removeScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.remove(l);
}
/** /**
* Process a 3270 data stream record. * Process a 3270 data stream record.
* *
@@ -0,0 +1,13 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.Telnet3270Client;
/**
* IBM Host On-Demand ECLConnection drop-in compatibility class.
*/
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection {
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session, Telnet3270Client client) {
super(session, client);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLConstants compatibility interface.
*/
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLErrors compatibility interface.
*/
public interface ECLErrors extends haus.nightmare.lib3270j.ecl.ECLErrors {
}
@@ -0,0 +1,21 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLException compatibility class.
*/
public class ECLException extends haus.nightmare.lib3270j.ecl.ECLException {
private static final long serialVersionUID = 1L;
public ECLException(int errorCode, String message) {
super(errorCode, message);
}
public ECLException(int errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
public ECLException(String message) {
super(message);
}
}
@@ -0,0 +1,11 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLField drop-in compatibility class.
*/
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField {
public ECLField(haus.nightmare.lib3270j.ecl.ECLPS ps, int startPos, int dataStart, int endPos, int length, byte attribute) {
super(ps, startPos, dataStart, endPos, length, attribute);
}
}
@@ -0,0 +1,13 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* IBM Host On-Demand ECLFieldList drop-in compatibility class.
*/
public class ECLFieldList extends haus.nightmare.lib3270j.ecl.ECLFieldList {
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLPS ps, ScreenBuffer screen) {
super(ps, screen);
}
}
@@ -0,0 +1,15 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
/**
* IBM Host On-Demand ECLOIA drop-in compatibility class.
*/
public class ECLOIA extends haus.nightmare.lib3270j.ecl.ECLOIA {
public ECLOIA(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) {
super(screen, inputProcessor, fsm);
}
}
@@ -0,0 +1,15 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* IBM Host On-Demand ECLPS drop-in compatibility class.
*/
public class ECLPS extends haus.nightmare.lib3270j.ecl.ECLPS {
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
super(screen, inputProcessor, translator);
}
}
@@ -0,0 +1,11 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLScreenDesc drop-in compatibility class.
*/
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc {
public ECLScreenDesc() {
super();
}
}
@@ -0,0 +1,37 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import java.util.Properties;
/**
* IBM Host On-Demand ECLSession drop-in compatibility class.
*/
public class ECLSession extends haus.nightmare.lib3270j.ecl.ECLSession {
public ECLSession() {
super();
}
public ECLSession(ConnectionConfig config) {
super(config);
}
public ECLSession(String host, int port, TerminalModel model) {
super(host, port, model);
}
public ECLSession(String host, int port, TerminalModel model, boolean useTls) {
super(host, port, model, useTls);
}
public ECLSession(Properties props) {
super(props);
}
public ECLSession(Telnet3270Client client) {
super(client);
}
}
@@ -0,0 +1,16 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* IBM Host On-Demand ECLXfer drop-in compatibility class.
*/
public class ECLXfer extends haus.nightmare.lib3270j.ecl.ECLXfer {
public ECLXfer(ScreenBuffer screen, InputProcessor input, DataStreamProcessor dsProcessor, EbcdicTranslator translator) {
super(screen, input, dsProcessor, translator);
}
}
@@ -0,0 +1,16 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
import haus.nightmare.lib3270j.ConnectionState;
/**
* IBM Host On-Demand ECLCommEvent compatibility class.
*/
public class ECLCommEvent extends haus.nightmare.lib3270j.ecl.ECLCommEvent {
private static final long serialVersionUID = 1L;
public ECLCommEvent(Object source, int eventType, ConnectionState oldState, ConnectionState newState,
String message, String deviceType, String deviceName) {
super(source, eventType, oldState, newState, message, deviceType, deviceName);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLCommListener compatibility interface.
*/
public interface ECLCommListener extends haus.nightmare.lib3270j.ecl.ECLCommListener {
}
@@ -0,0 +1,8 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLCommNotify compatibility interface.
*/
@FunctionalInterface
public interface ECLCommNotify extends haus.nightmare.lib3270j.ecl.ECLCommNotify {
}
@@ -0,0 +1,14 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLOIAEvent compatibility class.
*/
public class ECLOIAEvent extends haus.nightmare.lib3270j.ecl.ECLOIAEvent {
private static final long serialVersionUID = 1L;
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source, eventType, inputInhibited, alphanumericType, insertMode, statusString);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLOIAListener compatibility interface.
*/
public interface ECLOIAListener extends haus.nightmare.lib3270j.ecl.ECLOIAListener {
}
@@ -0,0 +1,18 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLPSEvent compatibility class.
*/
public class ECLPSEvent extends haus.nightmare.lib3270j.ecl.ECLPSEvent {
private static final long serialVersionUID = 1L;
public ECLPSEvent(Object source, int eventType, int startRow, int startCol, int endRow, int endCol,
int oldCursorAddress, int newCursorAddress, int rows, int cols, boolean fullUpdate) {
super(source, eventType, startRow, startCol, endRow, endCol, oldCursorAddress, newCursorAddress, rows, cols, fullUpdate);
}
public ECLPSEvent(Object source, int eventType) {
super(source, eventType);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLPSListener compatibility interface.
*/
public interface ECLPSListener extends haus.nightmare.lib3270j.ecl.ECLPSListener {
}
@@ -0,0 +1,73 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionState;
import java.util.EventObject;
/**
* Event object dispatched on communication lifecycle and state transitions.
*/
public class ECLCommEvent extends EventObject {
private static final long serialVersionUID = 1L;
public static final int COMM_CONNECTING = 1;
public static final int COMM_CONNECTED = 2;
public static final int COMM_DISCONNECTED = 3;
public static final int COMM_STATE_CHANGED = 4;
public static final int COMM_ERROR = 5;
public static final int COMM_UNBOUND = 6;
public static final int COMM_BIND = 7;
public static final int EVENT_CONNECTING = COMM_CONNECTING;
public static final int EVENT_CONNECTED = COMM_CONNECTED;
public static final int EVENT_DISCONNECTED = COMM_DISCONNECTED;
public static final int EVENT_STATE_CHANGED = COMM_STATE_CHANGED;
public static final int EVENT_ERROR = COMM_ERROR;
public static final int EVENT_UNBOUND = COMM_UNBOUND;
public static final int EVENT_BIND = COMM_BIND;
private final int eventType;
private final ConnectionState oldState;
private final ConnectionState newState;
private final String message;
private final String deviceType;
private final String deviceName;
public ECLCommEvent(Object source, int eventType, ConnectionState oldState, ConnectionState newState,
String message, String deviceType, String deviceName) {
super(source);
this.eventType = eventType;
this.oldState = oldState;
this.newState = newState;
this.message = message;
this.deviceType = deviceType;
this.deviceName = deviceName;
}
public int getEventType() { return eventType; }
public ConnectionState getOldState() { return oldState; }
public ConnectionState getNewState() { return newState; }
public String getMessage() { return message; }
public String getErrorMessage() { return message; }
public String getDeviceType() { return deviceType; }
public String getDeviceName() { return deviceName; }
public String getLUName() { return deviceName; }
public boolean isConnected() {
return newState != null && newState.isConnected();
}
public boolean isFullSession() {
return newState != null && newState.isFullSession();
}
public ECLConnection getConnection() {
return (getSource() instanceof ECLConnection) ? (ECLConnection) getSource() : null;
}
@Override
public String toString() {
return String.format("ECLCommEvent[type=%d, oldState=%s, newState=%s, msg='%s', dev='%s', lu='%s']",
eventType, oldState, newState, message, deviceType, deviceName);
}
}
@@ -0,0 +1,31 @@
package haus.nightmare.lib3270j.ecl;
/**
* Listener interface for communication lifecycle events.
*/
public interface ECLCommListener {
/**
* Called when the communication connection state or status changes.
* @param event ECLCommEvent containing transition details
*/
void commEvent(ECLCommEvent event);
/**
* Called specifically when the connection is established.
* @param event ECLCommEvent
*/
default void commConnected(ECLCommEvent event) {}
/**
* Called specifically when the connection is disconnected.
* @param event ECLCommEvent
*/
default void commDisconnected(ECLCommEvent event) {}
/**
* Called specifically when a communication error occurs.
* @param event ECLCommEvent
*/
default void commError(ECLCommEvent event) {}
}
@@ -0,0 +1,14 @@
package haus.nightmare.lib3270j.ecl;
/**
* IBM Host On-Demand ECLCommNotify callback interface.
*/
@FunctionalInterface
public interface ECLCommNotify {
/**
* Notification callback invoked on connection state change.
* @param connected true if session is connected, false otherwise
*/
void CommNotify(boolean connected);
}
@@ -0,0 +1,230 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Communication and session connection state management conforming to IBM Host On-Demand ECL.
*/
public class ECLConnection {
private final ECLSession session;
private final Telnet3270Client client;
private final List<ECLCommListener> commListeners = new CopyOnWriteArrayList<>();
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
public ECLConnection(ECLSession session, Telnet3270Client client) {
this.session = session;
this.client = client;
if (client != null) {
client.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
int eventType = ECLCommEvent.COMM_STATE_CHANGED;
if (newState.isFullSession()) {
eventType = ECLCommEvent.COMM_CONNECTED;
} else if (newState == ConnectionState.NOT_CONNECTED) {
eventType = ECLCommEvent.COMM_DISCONNECTED;
} else if (newState.isHalfConnected()) {
eventType = ECLCommEvent.COMM_CONNECTING;
}
String devType = (client.getTelnetFSM() != null) ? client.getTelnetFSM().getConnectedType() : null;
String lu = (client.getTelnetFSM() != null) ? client.getTelnetFSM().getConnectedLu() : null;
ECLCommEvent event = new ECLCommEvent(ECLConnection.this, eventType, oldState, newState,
"State: " + newState, devType, lu);
notifyCommEvent(event);
}
@Override
public void onConnectionError(String message) {
ConnectionState state = client.getConnectionState();
ECLCommEvent event = new ECLCommEvent(ECLConnection.this, ECLCommEvent.COMM_ERROR,
state, state, message, null, null);
notifyCommEvent(event);
}
@Override
public void onTN3270ENegotiated(String deviceType, String deviceName) {
ConnectionState state = client.getConnectionState();
ECLCommEvent event = new ECLCommEvent(ECLConnection.this, ECLCommEvent.COMM_BIND,
state, state, "TN3270E Negotiated", deviceType, deviceName);
notifyCommEvent(event);
}
});
}
}
public ECLSession GetSession() { return session; }
public ECLSession getSession() { return session; }
public Telnet3270Client GetClient() { return client; }
public Telnet3270Client getClient() { return client; }
public String GetHost() {
return (client != null && client.getConfig() != null) ? client.getConfig().getHost() : "";
}
public String getHost() { return GetHost(); }
public int GetPort() {
return (client != null && client.getConfig() != null) ? client.getConfig().getPort() : 23;
}
public int getPort() { return GetPort(); }
public String GetCodePage() {
return (client != null) ? client.getCodePage() : "037";
}
public String getCodePage() { return GetCodePage(); }
public TerminalModel GetModel() {
return (client != null && client.getConfig() != null) ? client.getConfig().getModel() : TerminalModel.IBM_3279_4;
}
public TerminalModel getModel() { return GetModel(); }
public String GetLUName() {
if (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) {
return client.getTelnetFSM().getConnectedLu();
}
return (client != null && client.getConfig() != null) ? client.getConfig().getLuName() : null;
}
public String getLUName() { return GetLUName(); }
public String GetDevName() { return GetLUName(); }
public String getDevName() { return GetLUName(); }
public String GetConnType() {
if (client == null) return "UNKNOWN";
ConnectionState cs = client.getConnectionState();
if (cs.isTn3270e()) return "TN3270E";
if (cs.is3270()) return "TN3270";
if (cs.isNvt()) return "NVT";
if (client.getConfig() != null) {
return client.getConfig().isTn3270eEnabled() ? "TN3270E" : "TN3270";
}
return "UNKNOWN";
}
public String getConnType() { return GetConnType(); }
public ConnectionState GetState() {
return (client != null) ? client.getConnectionState() : ConnectionState.NOT_CONNECTED;
}
public ConnectionState getState() { return GetState(); }
public int GetStateCode() {
return GetState().toHoDStateCode();
}
public int getStateCode() { return GetStateCode(); }
public boolean IsConnected() {
return client != null && client.isConnected();
}
public boolean isConnected() { return IsConnected(); }
public boolean IsStarted() {
return IsConnected();
}
public boolean isStarted() { return IsStarted(); }
public boolean IsReady() {
return client != null && client.getConnectionState().isFullSession();
}
public boolean isReady() { return IsReady(); }
public boolean IsConnecting() {
return client != null && client.getConnectionState().isHalfConnected();
}
public boolean isConnecting() { return IsConnecting(); }
public boolean IsDisconnecting() {
return client == null || client.getConnectionState() == ConnectionState.NOT_CONNECTED;
}
public boolean isDisconnecting() { return IsDisconnecting(); }
public boolean IsSSL() {
return client != null && client.getConfig() != null && client.getConfig().isUseTls();
}
public boolean isSSL() { return IsSSL(); }
public boolean IsTLS() { return IsSSL(); }
public boolean isTLS() { return IsSSL(); }
public void Connect() throws IOException {
if (client != null) {
client.connect();
}
}
public void connect() throws IOException { Connect(); }
public void Disconnect() {
if (client != null) {
client.disconnect();
}
}
public void disconnect() { Disconnect(); }
public void StartCommunication() throws IOException { Connect(); }
public void startCommunication() throws IOException { Connect(); }
public void StopCommunication() { Disconnect(); }
public void stopCommunication() { Disconnect(); }
// ========== Event Listener Management ==========
public void RegisterCommEvent(ECLCommListener listener) {
if (listener != null && !commListeners.contains(listener)) {
commListeners.add(listener);
}
}
public void registerCommEvent(ECLCommListener listener) { RegisterCommEvent(listener); }
public void UnregisterCommEvent(ECLCommListener listener) {
commListeners.remove(listener);
}
public void unregisterCommEvent(ECLCommListener listener) { UnregisterCommEvent(listener); }
public void RegisterCommEvent(ECLCommNotify notify, boolean sync) {
if (notify != null && !commNotifies.contains(notify)) {
commNotifies.add(notify);
}
}
public void registerCommEvent(ECLCommNotify notify, boolean sync) { RegisterCommEvent(notify, sync); }
public void UnregisterCommEvent(ECLCommNotify notify) {
commNotifies.remove(notify);
}
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
private void notifyCommEvent(ECLCommEvent event) {
for (ECLCommListener l : commListeners) {
try {
l.commEvent(event);
if (event.getEventType() == ECLCommEvent.COMM_CONNECTED) {
l.commConnected(event);
} else if (event.getEventType() == ECLCommEvent.COMM_DISCONNECTED) {
l.commDisconnected(event);
} else if (event.getEventType() == ECLCommEvent.COMM_ERROR) {
l.commError(event);
}
} catch (Exception ignored) {}
}
for (ECLCommNotify n : commNotifies) {
try {
n.CommNotify(event.isConnected());
} catch (Exception ignored) {}
}
}
@Override
public String toString() {
return String.format("ECLConnection[host=%s, port=%d, state=%s, lu=%s, ssl=%b]",
GetHost(), GetPort(), GetState(), GetLUName(), IsSSL());
}
}
@@ -0,0 +1,21 @@
package haus.nightmare.lib3270j.ecl;
/**
* Common error codes for IBM Host On-Demand Emulator Class Library (ECL) emulation.
*/
public interface ECLErrors {
int ECL_ERR_NONE = 0;
int ECL_ERR_COMM_NOT_CONNECTED = 1;
int ECL_ERR_COMM_TIMEOUT = 2;
int ECL_ERR_COMM_FAILED = 3;
int ECL_ERR_SCREEN_MATCH_TIMEOUT = 4;
int ECL_ERR_PS_NOT_AVAILABLE = 5;
int ECL_ERR_OIA_NOT_AVAILABLE = 6;
int ECL_ERR_INVALID_PARAM = 7;
int ECL_ERR_INVALID_POSITION = 8;
int ECL_ERR_FIELD_NOT_FOUND = 9;
int ECL_ERR_KEYBOARD_LOCKED = 10;
int ECL_ERR_XFER_FAILED = 11;
int ECL_ERR_SESSION_ALREADY_OPEN = 12;
int ECL_ERR_SESSION_CLOSED = 13;
}
@@ -0,0 +1,33 @@
package haus.nightmare.lib3270j.ecl;
/**
* Exception class for IBM Host On-Demand ECL operations.
*/
public class ECLException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int errorCode;
public ECLException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ECLException(int errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public ECLException(String message) {
this(ECLErrors.ECL_ERR_COMM_FAILED, message);
}
public int getErrorCode() {
return errorCode;
}
@Override
public String toString() {
return "ECLException[errorCode=" + errorCode + ", message=" + getMessage() + "]";
}
}
@@ -19,6 +19,7 @@ public class ECLOIA implements ECLConstants {
private final InputProcessor inputProcessor; private final InputProcessor inputProcessor;
private final TelnetFSM fsm; private final TelnetFSM fsm;
private final List<ECLOIANotify> listeners = new ArrayList<>(); private final List<ECLOIANotify> listeners = new ArrayList<>();
private final List<ECLOIAListener> oiaListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
public interface ECLOIANotify { public interface ECLOIANotify {
void onOIAChanged(ECLOIA oia); void onOIAChanged(ECLOIA oia);
@@ -44,12 +45,40 @@ public class ECLOIA implements ECLConstants {
listeners.remove(listener); listeners.remove(listener);
} }
public void RegisterOIAEvent(ECLOIAListener listener) {
if (listener != null && !oiaListeners.contains(listener)) {
oiaListeners.add(listener);
}
}
public void UnregisterOIAEvent(ECLOIAListener listener) {
oiaListeners.remove(listener);
}
public void registerOIAListener(ECLOIAListener listener) {
RegisterOIAEvent(listener);
}
public void unregisterOIAListener(ECLOIAListener listener) {
UnregisterOIAEvent(listener);
}
private synchronized void notifyOIAChanged() { private synchronized void notifyOIAChanged() {
for (ECLOIANotify l : listeners) { for (ECLOIANotify l : listeners) {
try { try {
l.onOIAChanged(this); l.onOIAChanged(this);
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
getAlphanumericType(), isInsertMode(), getStatusString());
for (ECLOIAListener l : oiaListeners) {
try {
l.oiaChanged(event);
if (inputProcessor != null) {
l.oiaLockStateChanged(event);
}
} catch (Exception ignored) {}
}
} }
public boolean isInsertMode() { public boolean isInsertMode() {
@@ -74,7 +103,7 @@ public class ECLOIA implements ECLConstants {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected(); return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
} }
private int inhibitOverride = INHIBIT_NOT_INHIBITED; private int inhibitOverride = -1;
public void setInputInhibited(int reason) { public void setInputInhibited(int reason) {
if (this.inhibitOverride != reason) { if (this.inhibitOverride != reason) {
@@ -145,7 +174,6 @@ public class ECLOIA implements ECLConstants {
} }
public String getStatusString() { public String getStatusString() {
if (isCommError()) return "X-COMM";
int inhibit = getInputInhibited(); int inhibit = getInputInhibited();
switch (inhibit) { switch (inhibit) {
case INHIBIT_SYSTEM_LOCK: return "X-SYSTEM"; case INHIBIT_SYSTEM_LOCK: return "X-SYSTEM";
@@ -165,12 +193,12 @@ public class ECLOIA implements ECLConstants {
* Returns one of INHIBIT_* constants from ECLConstants. * Returns one of INHIBIT_* constants from ECLConstants.
*/ */
public int getInputInhibited() { public int getInputInhibited() {
if (inhibitOverride >= 0) {
return inhibitOverride;
}
if (isCommError()) { if (isCommError()) {
return INHIBIT_COMM_CHECK; return INHIBIT_COMM_CHECK;
} }
if (inhibitOverride != INHIBIT_NOT_INHIBITED) {
return inhibitOverride;
}
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) { if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
return INHIBIT_SYSTEM_LOCK; return INHIBIT_SYSTEM_LOCK;
} }
@@ -204,6 +232,14 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs); return waitForInput(timeoutMs);
} }
public boolean WaitForSystemAvailable(long timeoutMs) {
return waitForSysAvailable(timeoutMs);
}
public boolean WaitForInput(long timeoutMs) {
return waitForInput(timeoutMs);
}
/** /**
* Block until application is available. * Block until application is available.
*/ */
@@ -211,6 +247,10 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs); return waitForInput(timeoutMs);
} }
public boolean WaitForAppAvailable(long timeoutMs) {
return waitForAppAvailable(timeoutMs);
}
/** /**
* Block until any OIA transition occurs. * Block until any OIA transition occurs.
*/ */
@@ -230,4 +270,8 @@ public class ECLOIA implements ECLConstants {
} }
return getInputInhibited() != initialInhibit; return getInputInhibited() != initialInhibit;
} }
public boolean WaitForTransition(long timeoutMs) {
return waitForTransition(timeoutMs);
}
} }
@@ -0,0 +1,58 @@
package haus.nightmare.lib3270j.ecl;
import java.util.EventObject;
/**
* Event object dispatched on Operator Information Area (ECLOIA) status changes.
*/
public class ECLOIAEvent extends EventObject {
private static final long serialVersionUID = 1L;
public static final int OIA_UPDATE = 1;
public static final int OIA_LOCK_CHANGE = 2;
public static final int OIA_COMM_CHANGE = 3;
public static final int OIA_INPUT_INHIBITED = 4;
public static final int EVENT_UPDATE = OIA_UPDATE;
public static final int EVENT_LOCK_CHANGE = OIA_LOCK_CHANGE;
public static final int EVENT_COMM_CHANGE = OIA_COMM_CHANGE;
public static final int EVENT_INPUT_INHIBITED = OIA_INPUT_INHIBITED;
private final int eventType;
private final int inputInhibited;
private final int alphanumericType;
private final boolean insertMode;
private final String statusString;
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source);
this.eventType = eventType;
this.inputInhibited = inputInhibited;
this.alphanumericType = alphanumericType;
this.insertMode = insertMode;
this.statusString = statusString;
}
public int getEventType() { return eventType; }
public int getInputInhibited() { return inputInhibited; }
public int getInhibitedReason() { return inputInhibited; }
public int getAlphanumericType() { return alphanumericType; }
public boolean isInsertMode() { return insertMode; }
public String getStatusString() { return statusString; }
public boolean isInputInhibited() {
return inputInhibited != ECLConstants.INHIBIT_NOT_INHIBITED;
}
public ECLOIA getOIA() {
return (getSource() instanceof ECLOIA) ? (ECLOIA) getSource() : null;
}
@Override
public String toString() {
return String.format("ECLOIAEvent[type=%d, status='%s', inhibited=%d, insert=%b]",
eventType, statusString, inputInhibited, insertMode);
}
}
@@ -0,0 +1,25 @@
package haus.nightmare.lib3270j.ecl;
/**
* Listener interface for Operator Information Area (ECLOIA) status change events.
*/
public interface ECLOIAListener {
/**
* Called when the OIA status, input inhibited flag, or keyboard lock state changes.
* @param event ECLOIAEvent containing OIA status information
*/
void oiaChanged(ECLOIAEvent event);
/**
* Called when the input inhibited condition changes specifically.
* @param event ECLOIAEvent
*/
default void oiaInhibited(ECLOIAEvent event) {}
/**
* Called when the keyboard lock / unlock state changes specifically.
* @param event ECLOIAEvent
*/
default void oiaLockStateChanged(ECLOIAEvent event) {}
}
@@ -365,6 +365,69 @@ public class ECLPS implements ECLConstants {
return charsPasted; return charsPasted;
} }
// ========== ECLPS Event Listener Management ==========
private final java.util.List<ECLPSListener> psListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
public void RegisterPSEvent(ECLPSListener listener) {
if (listener != null && !psListeners.contains(listener)) {
psListeners.add(listener);
}
}
public void registerPSEvent(ECLPSListener listener) {
RegisterPSEvent(listener);
}
public void UnregisterPSEvent(ECLPSListener listener) {
psListeners.remove(listener);
}
public void unregisterPSEvent(ECLPSListener listener) {
UnregisterPSEvent(listener);
}
public void notifyPSEvent(ECLPSEvent event) {
for (ECLPSListener l : psListeners) {
try {
l.psChanged(event);
if (event.getEventType() == ECLPSEvent.PS_CURSOR) {
l.psCursorMoved(event);
} else if (event.getEventType() == ECLPSEvent.PS_ALARM) {
l.psAlarm(event);
} else if (event.getEventType() == ECLPSEvent.PS_RESIZE) {
l.psResized(event);
} else if (event.getEventType() == ECLPSEvent.PS_CLOSE) {
l.psClosed(event);
}
} catch (Exception ignored) {}
}
}
public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full) {
int r = (screen != null) ? screen.getRows() : 0;
int c = (screen != null) ? screen.getCols() : 0;
int cur = (screen != null) ? screen.getCursorAddress() : 0;
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, startRow, startCol, endRow, endCol, cur, cur, r, c, full));
}
public void notifyCursorMoved(int oldAddress, int newAddress) {
int r = (screen != null) ? screen.getRows() : 0;
int c = (screen != null) ? screen.getCols() : 0;
int row = (c > 0) ? newAddress / c : 0;
int col = (c > 0) ? newAddress % c : 0;
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, row, col, row, col, oldAddress, newAddress, r, c, false));
}
public void notifyAlarm() {
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM));
}
public void notifyScreenResized(int rows, int cols) {
int cur = (screen != null) ? screen.getCursorAddress() : 0;
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, 0, 0, rows - 1, cols - 1, cur, cur, rows, cols, true));
}
/** /**
* Send standard IBM ECL mnemonic keystrokes. * Send standard IBM ECL mnemonic keystrokes.
*/ */
@@ -374,6 +437,21 @@ public class ECLPS implements ECLConstants {
} }
} }
public void SendKeys(String keys) {
sendKeys(keys);
}
public void SendKeys(String keys, int row, int col) {
sendKeys(keys, row, col);
}
public void sendKeys(String keys, int row, int col) {
if (row > 0 && col > 0) {
setCursorPos(row - 1, col - 1);
}
sendKeys(keys);
}
/** /**
* Send keystrokes with configurable inter-character or inter-mnemonic delay. * Send keystrokes with configurable inter-character or inter-mnemonic delay.
*/ */
@@ -415,6 +493,30 @@ public class ECLPS implements ECLConstants {
// ========== Synchronization & ECL Automation Waits ========== // ========== Synchronization & ECL Automation Waits ==========
/**
* Block until the specified screen descriptor conditions are met.
*/
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) {
if (desc == null) return true;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (desc.Matches(this, null)) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return desc.Matches(this, null);
}
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return waitForScreen(desc, timeoutMs);
}
/** /**
* Block until the specified text appears anywhere on the presentation space. * Block until the specified text appears anywhere on the presentation space.
*/ */
@@ -434,6 +536,10 @@ public class ECLPS implements ECLConstants {
return searchString(text) >= 0; return searchString(text) >= 0;
} }
public boolean WaitForScreen(String text, long timeoutMs) {
return waitForScreen(text, timeoutMs);
}
/** /**
* Block until the specified text appears at the given (row, col) coordinate. * Block until the specified text appears at the given (row, col) coordinate.
*/ */
@@ -454,6 +560,10 @@ public class ECLPS implements ECLConstants {
return text.equals(getString(row, col, text.length())); return text.equals(getString(row, col, text.length()));
} }
public boolean WaitForScreen(String text, int row, int col, long timeoutMs) {
return waitForScreen(text, row, col, timeoutMs);
}
/** /**
* Block until the cursor moves to (row, col). * Block until the cursor moves to (row, col).
*/ */
@@ -472,4 +582,8 @@ public class ECLPS implements ECLConstants {
} }
return getCursorRow() == row && getCursorCol() == col; return getCursorRow() == row && getCursorCol() == col;
} }
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return waitForCursor(row, col, timeoutMs);
}
} }
@@ -0,0 +1,74 @@
package haus.nightmare.lib3270j.ecl;
import java.util.EventObject;
/**
* Event object dispatched on Presentation Space (ECLPS) modifications.
*/
public class ECLPSEvent extends EventObject {
private static final long serialVersionUID = 1L;
public static final int PS_UPDATE = 1;
public static final int PS_CURSOR = 2;
public static final int PS_ALARM = 3;
public static final int PS_RESIZE = 4;
public static final int PS_CLOSE = 5;
public static final int EVENT_UPDATE = PS_UPDATE;
public static final int EVENT_CURSOR = PS_CURSOR;
public static final int EVENT_ALARM = PS_ALARM;
public static final int EVENT_RESIZE = PS_RESIZE;
public static final int EVENT_CLOSE = PS_CLOSE;
private final int eventType;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final int oldCursorAddress;
private final int newCursorAddress;
private final int rows;
private final int cols;
private final boolean fullUpdate;
public ECLPSEvent(Object source, int eventType, int startRow, int startCol, int endRow, int endCol,
int oldCursorAddress, int newCursorAddress, int rows, int cols, boolean fullUpdate) {
super(source);
this.eventType = eventType;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.oldCursorAddress = oldCursorAddress;
this.newCursorAddress = newCursorAddress;
this.rows = rows;
this.cols = cols;
this.fullUpdate = fullUpdate;
}
public ECLPSEvent(Object source, int eventType) {
this(source, eventType, 0, 0, 0, 0, 0, 0, 0, 0, true);
}
public int getEventType() { return eventType; }
public int getStartRow() { return startRow; }
public int getStartCol() { return startCol; }
public int getEndRow() { return endRow; }
public int getEndCol() { return endCol; }
public int getOldCursorAddress() { return oldCursorAddress; }
public int getNewCursorAddress() { return newCursorAddress; }
public int getRows() { return rows; }
public int getCols() { return cols; }
public boolean isFullUpdate() { return fullUpdate; }
public ECLPS getPS() {
return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null;
}
@Override
public String toString() {
return String.format("ECLPSEvent[type=%d, start=(%d,%d), end=(%d,%d), full=%b]",
eventType, startRow, startCol, endRow, endCol, fullUpdate);
}
}
@@ -0,0 +1,37 @@
package haus.nightmare.lib3270j.ecl;
/**
* Listener interface for Presentation Space (ECLPS) update events.
*/
public interface ECLPSListener {
/**
* Called when the presentation space is modified.
* @param event ECLPSEvent containing update boundaries and state
*/
void psChanged(ECLPSEvent event);
/**
* Called when the cursor position changes within the presentation space.
* @param event ECLPSEvent containing cursor positions
*/
default void psCursorMoved(ECLPSEvent event) {}
/**
* Called when a host sound alarm is triggered.
* @param event ECLPSEvent
*/
default void psAlarm(ECLPSEvent event) {}
/**
* Called when the presentation space dimensions change.
* @param event ECLPSEvent containing new rows and columns
*/
default void psResized(ECLPSEvent event) {}
/**
* Called when the presentation space is closed / disconnected.
* @param event ECLPSEvent
*/
default void psClosed(ECLPSEvent event) {}
}
@@ -0,0 +1,235 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.Telnet3270Client;
import java.util.ArrayList;
import java.util.List;
/**
* Screen descriptor matching engine for IBM Host On-Demand ECL automation.
* Encapsulates criteria for matching host screens (strings, rectangular regions, cursor positions,
* field counts, and OIA status).
*/
public class ECLScreenDesc {
private final List<ScreenCondition> conditions = new ArrayList<>();
private interface ScreenCondition {
boolean matches(ECLPS ps, ECLOIA oia);
}
public ECLScreenDesc() {}
/**
* Clear all conditions from this descriptor.
*/
public synchronized void Clear() {
conditions.clear();
}
public synchronized void clear() {
Clear();
}
/**
* Add a condition that the given string must appear anywhere on the presentation space.
*/
public synchronized void AddString(String text) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
return ps.searchString(text) >= 0;
});
}
public synchronized void addString(String text) {
AddString(text);
}
/**
* Add a condition that text must appear at 1-based (row, col) position (case-insensitive by default).
*/
public synchronized void AddString(String text, int row, int col) {
AddString(text, row, col, true);
}
public synchronized void addString(String text, int row, int col) {
AddString(text, row, col, true);
}
/**
* Add a condition that text must appear at (row, col) with case sensitivity control.
* Note: 1-based indexing conforming to IBM ECL specification (row 1..rows, col 1..cols).
*/
public synchronized void AddString(String text, int row, int col, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
String onScreen = ps.getString(r, c, text.length());
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
});
}
public synchronized void addString(String text, int row, int col, boolean caseSense) {
AddString(text, row, col, caseSense);
}
/**
* Add a condition that text must appear at a 1-based linear buffer position.
*/
public synchronized void AddString(String text, int pos, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
String onScreen = ps.getString(p0, text.length());
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
});
}
public synchronized void addString(String text, int pos, boolean caseSense) {
AddString(text, pos, caseSense);
}
/**
* Add a condition that text must appear within a rectangular region.
*/
public synchronized void AddStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int sr = (sRow > 0) ? sRow - 1 : 0;
int sc = (sCol > 0) ? sCol - 1 : 0;
int er = (eRow > 0) ? eRow - 1 : 0;
int ec = (eCol > 0) ? eCol - 1 : 0;
String block = ps.copyString(sr, sc, er, ec);
if (block == null) return false;
return caseSense ? block.contains(text) : block.toLowerCase().contains(text.toLowerCase());
});
}
public synchronized void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
AddStringInRect(text, sRow, sCol, eRow, eCol, caseSense);
}
/**
* Add a condition that the cursor must be located at 1-based (row, col).
*/
public synchronized void AddCursorPos(int row, int col) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
return ps.getCursorRow() == r && ps.getCursorCol() == c;
});
}
public synchronized void addCursorPos(int row, int col) {
AddCursorPos(row, col);
}
/**
* Add a condition that the cursor must be located at 1-based linear buffer position.
*/
public synchronized void AddCursorPos(int pos) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
return ps.getCursorPos() == p0;
});
}
public synchronized void addCursorPos(int pos) {
AddCursorPos(pos);
}
/**
* Add a condition on total field count on the formatted screen.
*/
public synchronized void AddNumFields(int count) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
return ps.getFieldList().getFieldCount() == count;
});
}
public synchronized void addNumFields(int count) {
AddNumFields(count);
}
/**
* Add a condition on number of unprotected input fields on the screen.
*/
public synchronized void AddNumInputFields(int count) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int inputCount = 0;
for (ECLField f : ps.getFieldList().getFields()) {
if (!f.isProtected()) {
inputCount++;
}
}
return inputCount == count;
});
}
public synchronized void addNumInputFields(int count) {
AddNumInputFields(count);
}
/**
* Add a condition on OIA inhibit status.
*/
public synchronized void AddOIAStatus(int status) {
conditions.add((ps, oia) -> {
if (oia == null) return true;
return oia.getInputInhibited() == status;
});
}
public synchronized void addOIAStatus(int status) {
AddOIAStatus(status);
}
/**
* Check if the current screen state matches all conditions in this descriptor.
*/
public synchronized boolean Matches(ECLPS ps, ECLOIA oia) {
if (conditions.isEmpty()) {
return true;
}
for (ScreenCondition cond : conditions) {
if (!cond.matches(ps, oia)) {
return false;
}
}
return true;
}
public boolean matches(ECLPS ps, ECLOIA oia) {
return Matches(ps, oia);
}
public boolean Matches(ECLSession session) {
if (session == null) return false;
return Matches(session.GetPS(), session.GetOIA());
}
public boolean matches(ECLSession session) {
return Matches(session);
}
public boolean Matches(Telnet3270Client client) {
if (client == null) return false;
return Matches(client.getPS(), client.getOIA());
}
public boolean matches(Telnet3270Client client) {
return Matches(client);
}
public synchronized int getConditionCount() {
return conditions.size();
}
}
@@ -0,0 +1,299 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Logger;
/**
* Top-level session facade conforming to IBM Host On-Demand Emulator Class Library (ECL).
* Provides access to Presentation Space (ECLPS), Operator Information Area (ECLOIA),
* Connection (ECLConnection), File Transfer (ECLXfer), and synchronous blocking primitives.
*/
public class ECLSession {
private static final Logger log = Logger.getLogger(ECLSession.class.getName());
// Standard IBM HoD Session Property Keys
public static final String SESSION_HOST = "SESSION_HOST";
public static final String SESSION_PORT = "SESSION_PORT";
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
public static final String SESSION_MODEL = "SESSION_MODEL";
public static final String SESSION_TYPE = "SESSION_TYPE";
public static final String SESSION_SSL = "SESSION_SSL";
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
public static final String SESSION_TN3270E = "SESSION_TN3270E";
public static final String SESSION_WIN_TITLE = "SESSION_WIN_TITLE";
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
private final Telnet3270Client client;
private final ECLConnection connection;
private Properties properties = new Properties();
public ECLSession() {
this(new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4));
}
public ECLSession(ConnectionConfig config) {
this(new Telnet3270Client(config));
}
public ECLSession(String host, int port, TerminalModel model) {
this(new ConnectionConfig(host, port, model != null ? model : TerminalModel.IBM_3279_4));
}
public ECLSession(String host, int port, TerminalModel model, boolean useTls) {
this(new ConnectionConfig(host, port, model != null ? model : TerminalModel.IBM_3279_4, useTls));
}
public ECLSession(Properties props) {
this(parsePropertiesToConfig(props));
if (props != null) {
this.properties.putAll(props);
}
}
public ECLSession(Telnet3270Client client) {
this.client = (client != null) ? client : new Telnet3270Client(new ConnectionConfig("localhost", 23));
this.connection = new ECLConnection(this, this.client);
syncPropertiesFromConfig();
}
private static ConnectionConfig parsePropertiesToConfig(Properties props) {
if (props == null) {
return new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
}
String host = getProp(props, SESSION_HOST, "host", "Host", "hostname", "localhost");
String portStr = getProp(props, SESSION_PORT, "port", "Port", null);
String sslStr = getProp(props, SESSION_SSL, "ssl", "SSL", "use_ssl", "false");
boolean useTls = "true".equalsIgnoreCase(sslStr) || "yes".equalsIgnoreCase(sslStr) || "1".equals(sslStr);
int port = (portStr != null) ? Integer.parseInt(portStr) : (useTls ? 992 : 23);
String modelStr = getProp(props, SESSION_MODEL, "model", "Model", "SCREEN_SIZE", "4");
TerminalModel model = parseModelString(modelStr);
ConnectionConfig config = new ConnectionConfig(host, port, model, useTls);
String cp = getProp(props, SESSION_CODE_PAGE, "code_page", "codepage", "CodePage", null);
if (cp != null) config.setCodePage(cp);
String lu = getProp(props, SESSION_LU_NAME, "lu_name", "luname", "LU_NAME", null);
if (lu != null) config.setLuName(lu);
String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true");
config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr));
return config;
}
private static String getProp(Properties props, String key1, String key2, String key3, String defaultVal) {
if (props.containsKey(key1)) return props.getProperty(key1);
if (props.containsKey(key2)) return props.getProperty(key2);
if (props.containsKey(key3)) return props.getProperty(key3);
return defaultVal;
}
private static String getProp(Properties props, String key1, String key2, String key3, String key4, String defaultVal) {
if (props.containsKey(key1)) return props.getProperty(key1);
if (props.containsKey(key2)) return props.getProperty(key2);
if (props.containsKey(key3)) return props.getProperty(key3);
if (props.containsKey(key4)) return props.getProperty(key4);
return defaultVal;
}
private static TerminalModel parseModelString(String m) {
if (m == null || m.trim().isEmpty()) return TerminalModel.IBM_3279_4;
String s = m.trim().toUpperCase();
if (s.equals("2") || s.contains("3278-2") || s.contains("3279-2") || s.equals("24X80")) {
return TerminalModel.IBM_3279_2;
} else if (s.equals("3") || s.contains("3278-3") || s.contains("3279-3") || s.equals("32X80")) {
return TerminalModel.IBM_3279_3;
} else if (s.equals("5") || s.contains("3278-5") || s.contains("3279-5") || s.equals("27X132")) {
return TerminalModel.IBM_3279_5;
}
return TerminalModel.IBM_3279_4;
}
private void syncPropertiesFromConfig() {
if (client != null && client.getConfig() != null) {
ConnectionConfig cfg = client.getConfig();
properties.setProperty(SESSION_HOST, cfg.getHost());
properties.setProperty(SESSION_PORT, String.valueOf(cfg.getPort()));
properties.setProperty(SESSION_CODE_PAGE, client.getCodePage());
properties.setProperty(SESSION_MODEL, String.valueOf(cfg.getModel().getModelNumber()));
properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls()));
if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName());
properties.setProperty(SESSION_TN3270E, String.valueOf(cfg.isTn3270eEnabled()));
}
}
public Properties GetProperties() { return properties; }
public Properties getProperties() { return properties; }
public void SetProperties(Properties props) {
if (props != null) {
this.properties = new Properties();
this.properties.putAll(props);
}
}
public void setProperties(Properties props) { SetProperties(props); }
// ========== ECL Component Accessors ==========
public ECLPS GetPS() { return client.getPS(); }
public ECLPS getPS() { return client.getPS(); }
public ECLOIA GetOIA() { return client.getOIA(); }
public ECLOIA getOIA() { return client.getOIA(); }
public ECLConnection GetConnection() { return connection; }
public ECLConnection getConnection() { return connection; }
public ECLXfer GetXfer() { return client.getXfer(); }
public ECLXfer getXfer() { return client.getXfer(); }
public ECLFieldList GetFieldList() { return client.getFieldList(); }
public ECLFieldList getFieldList() { return client.getFieldList(); }
public Telnet3270Client GetClient() { return client; }
public Telnet3270Client getClient() { return client; }
public ECLScreenDesc GetScreenDesc() { return new ECLScreenDesc(); }
public ECLScreenDesc createScreenDesc() { return new ECLScreenDesc(); }
// ========== Communication Lifecycle & Blocking Methods ==========
/**
* Initiate asynchronous communication connection.
*/
public boolean StartCommunication() throws IOException {
client.connect();
return client.isConnected();
}
public boolean startCommunication() throws IOException { return StartCommunication(); }
/**
* Synchronous blocking connection conforming to HoD ECLSession.StartCommunicationWithBlocking.
* Blocks until the connection reaches fully established data state or timeout expires.
*/
public boolean StartCommunicationWithBlocking(long timeoutMs) throws IOException {
return client.connect(timeoutMs);
}
public boolean startCommunicationWithBlocking(long timeoutMs) throws IOException {
return StartCommunicationWithBlocking(timeoutMs);
}
/**
* Synchronous blocking connection matching HoD ECLSession.StartCommunicationWithBlocking(timeout, desc).
* Blocks until the connection is established AND the presentation space matches the screen descriptor.
*/
public boolean StartCommunicationWithBlocking(long timeoutMs, ECLScreenDesc desc) throws IOException {
return client.connect(timeoutMs, desc);
}
public boolean startCommunicationWithBlocking(long timeoutMs, ECLScreenDesc desc) throws IOException {
return StartCommunicationWithBlocking(timeoutMs, desc);
}
/**
* Terminate active communication session.
*/
public void StopCommunication() {
client.disconnect();
}
public void stopCommunication() { StopCommunication(); }
/**
* Terminate active communication session with synchronous teardown.
*/
public void StopCommunicationWithBlocking(long timeoutMs) {
client.disconnect(timeoutMs);
}
public void stopCommunicationWithBlocking(long timeoutMs) { StopCommunicationWithBlocking(timeoutMs); }
/**
* Reconnect the communication session.
*/
public boolean RestartCommunication() throws IOException {
StopCommunication();
return StartCommunication();
}
public boolean restartCommunication() throws IOException { return RestartCommunication(); }
/**
* Reconnect the communication session synchronously.
*/
public boolean RestartCommunicationWithBlocking(long timeoutMs) throws IOException {
StopCommunicationWithBlocking(500);
return StartCommunicationWithBlocking(timeoutMs);
}
public boolean restartCommunicationWithBlocking(long timeoutMs) throws IOException {
return RestartCommunicationWithBlocking(timeoutMs);
}
public boolean IsConnected() {
return client.isConnected();
}
public boolean isConnected() { return IsConnected(); }
public boolean IsCommStarted() {
return client.isConnected();
}
public boolean isCommStarted() { return IsCommStarted(); }
// ========== Automation Keystrokes & Waits ==========
/**
* Stream IBM ECL bracketed mnemonic keystrokes to the presentation space.
*/
public void SendKeys(String text) {
client.sendKeys(text);
}
public void sendKeys(String text) { SendKeys(text); }
/**
* Position cursor at 1-based (row, col) and stream keystrokes.
*/
public void SendKeys(String text, int row, int col) {
client.getPS().SendKeys(text, row, col);
}
public void sendKeys(String text, int row, int col) { SendKeys(text, row, col); }
/**
* Block until the specified screen descriptor conditions are met on screen.
*/
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return client.getPS().waitForScreen(desc, timeoutMs);
}
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
/**
* Block until the cursor moves to (row, col).
*/
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return client.getPS().waitForCursor(row, col, timeoutMs);
}
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
/**
* Terminate and release all resources.
*/
public void dispose() {
StopCommunication();
}
public void close() {
dispose();
}
@Override
public String toString() {
return String.format("ECLSession[host=%s, port=%d, connected=%b, state=%s]",
connection.GetHost(), connection.GetPort(), IsConnected(), connection.GetState());
}
}
@@ -7,6 +7,7 @@ import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.FTConstants.FTState; import haus.nightmare.lib3270j.ft.FTConstants.FTState;
import haus.nightmare.lib3270j.ft.FTCut; import haus.nightmare.lib3270j.ft.FTCut;
import haus.nightmare.lib3270j.ft.FTDft; import haus.nightmare.lib3270j.ft.FTDft;
import haus.nightmare.lib3270j.ft.CMSPrintXfer;
import haus.nightmare.lib3270j.ft.dir.*; import haus.nightmare.lib3270j.ft.dir.*;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
@@ -123,6 +124,14 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
/** /**
* Convenience method to download a file with listener and codepage parameters. * Convenience method to download a file with listener and codepage parameters.
*/ */
public void getFile(String hostFile, String localFile) {
ReceiveFile(localFile, hostFile, "");
}
public void getFile(String hostFile, String localFile, String options) {
ReceiveFile(localFile, hostFile, options);
}
public void getFile(String hostFile, String localFile, String options, int mode, String codePage, ECLXferListener listener) { public void getFile(String hostFile, String localFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener); if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) { if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -131,9 +140,27 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
ReceiveFile(localFile, hostFile, options); ReceiveFile(localFile, hostFile, options);
} }
public void getFile(FTConfig config, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (config != null) {
if (config.getCodePage() != null && translator != null) {
translator.setCodePage(config.getCodePage());
}
startTransferInternal(config);
}
}
/** /**
* Convenience method to upload a file with listener and codepage parameters. * Convenience method to upload a file with listener and codepage parameters.
*/ */
public void putFile(String localFile, String hostFile) {
SendFile(localFile, hostFile, "");
}
public void putFile(String localFile, String hostFile, String options) {
SendFile(localFile, hostFile, options);
}
public void putFile(String localFile, String hostFile, String options, int mode, String codePage, ECLXferListener listener) { public void putFile(String localFile, String hostFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener); if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) { if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -142,6 +169,16 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
SendFile(localFile, hostFile, options); SendFile(localFile, hostFile, options);
} }
public void putFile(FTConfig config, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (config != null) {
if (config.getCodePage() != null && translator != null) {
translator.setCodePage(config.getCodePage());
}
startTransferInternal(config);
}
}
/** /**
* Cancel an active transfer. * Cancel an active transfer.
* @return 0 on success. * @return 0 on success.
@@ -243,6 +280,81 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
} }
} }
/**
* Batch directory file query with callback.
*/
public void getFiles(String filter, FileTransferHostDirectoryInterface callback) {
getFiles(filter, (List<HostDirectoryEntry>) null, callback);
}
/**
* Batch directory file download into a destination directory.
*/
public void getFiles(String filter, String localDirectory, FileTransferHostDirectoryInterface callback) {
List<HostDirectoryEntry> fileList = new ArrayList<>();
getFiles(filter, fileList, new FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
if (localDirectory != null && entries != null) {
File dir = new File(localDirectory);
if (!dir.exists()) dir.mkdirs();
for (HostDirectoryEntry entry : entries) {
String localName = entry.getName();
if (entry instanceof CMSDirectoryEntry) {
CMSDirectoryEntry cms = (CMSDirectoryEntry) entry;
localName = cms.getFilename() + "." + cms.getFiletype();
}
File dest = new File(dir, localName);
ReceiveFile(dest.getAbsolutePath(), entry.getName(), "ASCII CRLF");
}
}
if (callback != null) callback.onDirectoryLoaded(entries);
}
@Override
public void onDirectoryError(String errorMessage) {
if (callback != null) callback.onDirectoryError(errorMessage);
}
});
}
/**
* HoD compatibility overload for batch file transfers with Vector arguments.
*/
public void getFiles(String hostQuery, int hostType, int mode,
java.util.Vector<String> localFiles, java.util.Vector<String> hostFiles,
FileTransferHostDirectoryInterface callback) {
List<HostDirectoryEntry> fileList = new ArrayList<>();
getFiles(hostQuery, fileList, new FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
if (entries != null) {
for (HostDirectoryEntry entry : entries) {
if (hostFiles != null) hostFiles.add(entry.getName());
if (localFiles != null) localFiles.add(entry.getName());
}
}
if (callback != null) callback.onDirectoryLoaded(entries);
}
@Override
public void onDirectoryError(String errorMessage) {
if (callback != null) callback.onDirectoryError(errorMessage);
}
});
}
/**
* Retransmit the last inbound structured field buffer to the host on host retry / timeout.
* @return true if buffer was resent, false otherwise.
*/
public boolean resendInboundDataBufferToHost() {
if (dftHandler != null) {
return dftHandler.resendInboundDataBufferToHost();
}
return false;
}
public CMSDirectoryEntry createNewCmsDirectoryEntry(String fn, String ft, String fm) { public CMSDirectoryEntry createNewCmsDirectoryEntry(String fn, String ft, String fm) {
return new CMSDirectoryEntry(fn, ft, fm); return new CMSDirectoryEntry(fn, ft, fm);
} }
@@ -251,6 +363,15 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
return new TSODirectoryEntry(dsname); return new TSODirectoryEntry(dsname);
} }
private CMSPrintXfer cmsPrintXfer;
public CMSPrintXfer getCMSPrintXfer() {
if (cmsPrintXfer == null) {
cmsPrintXfer = new CMSPrintXfer(this, translator);
}
return cmsPrintXfer;
}
// ========== BIDI File Helpers ========== // ========== BIDI File Helpers ==========
public void doBIDIsaveLocalFile(File file, boolean rtl) throws IOException { public void doBIDIsaveLocalFile(File file, boolean rtl) throws IOException {
@@ -307,22 +428,7 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private void parseOptionsIntoConfig(FTConfig config, String options) { private void parseOptionsIntoConfig(FTConfig config, String options) {
if (options == null || options.trim().isEmpty()) return; if (options == null || options.trim().isEmpty()) return;
String upper = options.toUpperCase(); config.parseOptions(options);
if (upper.contains("BINARY")) config.setTransferMode(FTConfig.TransferMode.BINARY);
else if (upper.contains("ASCII")) config.setTransferMode(FTConfig.TransferMode.ASCII);
if (upper.contains("CRLF")) config.setCrAction(FTConfig.CrAction.REMOVE);
else if (upper.contains("NOCRLF")) config.setCrAction(FTConfig.CrAction.KEEP);
if (upper.contains("APPEND")) config.setAppend(true);
if (upper.contains("REPLACE")) config.setOverwrite(true);
if (upper.contains("CMS")) config.setHostType(FTConfig.HostType.CMS);
else if (upper.contains("CICS")) config.setHostType(FTConfig.HostType.CICS);
else if (upper.contains("TSO")) config.setHostType(FTConfig.HostType.TSO);
config.setOtherOptions(options);
} }
/** /**
@@ -0,0 +1,426 @@
package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLXfer;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.logging.Logger;
/**
* VM/CMS Spool and Print File Transfer facility matching IBM Host On-Demand
* (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer).
*
* Provides:
* 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT).
* 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+').
* 3. IBM 1403/3211 Machine carriage control channel command byte translation.
* 4. High-level print spool stream extraction and transfer helpers.
*/
public class CMSPrintXfer {
private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName());
private final ECLXfer xfer;
private final EbcdicTranslator translator;
/**
* Entry representing a VM/CMS spool file in the reader or printer queue.
*/
public static class SpoolFileEntry implements Serializable {
private static final long serialVersionUID = 1L;
private int spoolId;
private String owner;
private String spoolClass = "A";
private long records = 0;
private int copies = 1;
private String holdStatus = "NOHOLD";
private String date = "";
private String time = "";
private String fileName = "";
private String fileType = "";
private String deviceType = "RDR"; // RDR or PRT
public SpoolFileEntry() {}
public SpoolFileEntry(int spoolId, String owner, String fileName, String fileType) {
this.spoolId = spoolId;
this.owner = owner;
this.fileName = fileName;
this.fileType = fileType;
}
public int getSpoolId() { return spoolId; }
public void setSpoolId(int spoolId) { this.spoolId = spoolId; }
public String getOwner() { return owner; }
public void setOwner(String owner) { this.owner = owner; }
public String getSpoolClass() { return spoolClass; }
public void setSpoolClass(String spoolClass) { this.spoolClass = spoolClass; }
public long getRecords() { return records; }
public void setRecords(long records) { this.records = records; }
public int getCopies() { return copies; }
public void setCopies(int copies) { this.copies = copies; }
public String getHoldStatus() { return holdStatus; }
public void setHoldStatus(String holdStatus) { this.holdStatus = holdStatus; }
public String getDate() { return date; }
public void setDate(String date) { this.date = date; }
public String getTime() { return time; }
public void setTime(String time) { this.time = time; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getDeviceType() { return deviceType; }
public void setDeviceType(String deviceType) { this.deviceType = deviceType; }
public String formatListing() {
return String.format("%-8s %04d %1s %-8s %-8s %8d %4d %-6s %-10s %-8s",
owner != null ? owner : "", spoolId, spoolClass != null ? spoolClass : "A",
fileName != null ? fileName : "", fileType != null ? fileType : "",
records, copies, holdStatus != null ? holdStatus : "NOHOLD",
date != null ? date : "", time != null ? time : "");
}
@Override
public String toString() {
return formatListing();
}
}
public CMSPrintXfer() {
this(null, new EbcdicTranslator());
}
public CMSPrintXfer(ECLXfer xfer) {
this(xfer, new EbcdicTranslator());
}
public CMSPrintXfer(ECLXfer xfer, EbcdicTranslator translator) {
this.xfer = xfer;
this.translator = translator != null ? translator : new EbcdicTranslator();
}
// =========================================================================
// Spool Query Parsing (CP QUERY RDR / PRT ALL)
// =========================================================================
/**
* Parse CP QUERY RDR ALL or CP QUERY PRT ALL output text into a list of SpoolFileEntry objects.
*/
public static List<SpoolFileEntry> parseQuerySpoolOutput(String text, String defaultDevice) {
List<SpoolFileEntry> entries = new ArrayList<>();
if (text == null || text.trim().isEmpty()) return entries;
String[] lines = text.split("\r?\n");
Pattern headerPattern = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.isEmpty()) continue;
if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue;
if (headerPattern.matcher(trimmed).find()) continue;
SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
public static List<SpoolFileEntry> parseQueryReaderOutput(String text) {
return parseQuerySpoolOutput(text, "RDR");
}
public static List<SpoolFileEntry> parseQueryPrinterOutput(String text) {
return parseQuerySpoolOutput(text, "PRT");
}
private static SpoolFileEntry parseSpoolLine(String line, String defaultDevice) {
String[] tokens = line.split("\\s+");
if (tokens.length < 3) return null;
try {
String owner = "";
int spoolId = -1;
int nextIdx = 0;
if (tokens[0].matches("^\\d+$")) {
spoolId = Integer.parseInt(tokens[0]);
nextIdx = 1;
} else if (tokens.length > 1 && tokens[1].matches("^\\d+$")) {
owner = tokens[0].toUpperCase();
spoolId = Integer.parseInt(tokens[1]);
nextIdx = 2;
} else {
return null;
}
SpoolFileEntry entry = new SpoolFileEntry();
entry.setSpoolId(spoolId);
entry.setOwner(owner);
entry.setDeviceType(defaultDevice != null ? defaultDevice : "RDR");
if (nextIdx < tokens.length) {
entry.setSpoolClass(tokens[nextIdx++]);
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("(?i)RDR|PRT|PUN|PCH")) {
entry.setDeviceType(tokens[nextIdx++].toUpperCase());
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("^\\d+$")) {
entry.setRecords(Long.parseLong(tokens[nextIdx++]));
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("^\\d+$")) {
entry.setCopies(Integer.parseInt(tokens[nextIdx++]));
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("(?i)NONE|USER|SYS|HOLD|KEEP|NOHOLD")) {
entry.setHoldStatus(tokens[nextIdx++].toUpperCase());
}
if (nextIdx < tokens.length && (tokens[nextIdx].contains("/") || tokens[nextIdx].contains("-"))) {
entry.setDate(tokens[nextIdx++]);
}
if (nextIdx < tokens.length && tokens[nextIdx].contains(":")) {
entry.setTime(tokens[nextIdx++]);
}
if (nextIdx < tokens.length) {
entry.setFileName(tokens[nextIdx++]);
}
if (nextIdx < tokens.length) {
entry.setFileType(tokens[nextIdx++]);
}
return entry;
} catch (Exception e) {
return null;
}
}
// =========================================================================
// ANSI / ASA Carriage Control Translation
// =========================================================================
/**
* Converts ANSI/ASA carriage control text into standardized formatted text.
*
* ASA Carriage Control characters (first column of each record):
* ' ' (Blank / Space) -> Advance 1 line (Single space)
* '0' (Zero) -> Advance 2 lines (Double space)
* '-' (Minus / Dash) -> Advance 3 lines (Triple space)
* '1' (One) -> Advance to top of next page (Form Feed '\f')
* '+' (Plus) -> Suppress spacing / Overstrike (Carriage Return '\r' without line feed)
*
* @param asaText Raw text containing ASA carriage control in column 1 of each line.
* @return Clean formatted text with standard newlines and form feeds.
*/
public static String convertAsaCarriageControl(String asaText) {
if (asaText == null) return "";
StringBuilder out = new StringBuilder(asaText.length());
String[] lines = asaText.split("\r?\n");
boolean firstLine = true;
for (String line : lines) {
if (line.isEmpty()) {
if (!firstLine) out.append("\n");
firstLine = false;
continue;
}
char cc = line.charAt(0);
String content = line.length() > 1 ? line.substring(1) : "";
if (firstLine) {
firstLine = false;
if (cc == '1') {
out.append("\f");
} else if (cc == '0') {
out.append("\n");
} else if (cc == '-') {
out.append("\n\n");
}
out.append(content);
} else {
switch (cc) {
case ' ':
out.append("\n").append(content);
break;
case '0':
out.append("\n\n").append(content);
break;
case '-':
out.append("\n\n\n").append(content);
break;
case '1':
out.append("\n\f").append(content);
break;
case '+':
out.append("\r").append(content);
break;
default:
out.append("\n").append(line);
break;
}
}
}
return out.toString();
}
/**
* Converts byte stream containing ASA carriage controls.
*/
public static byte[] convertAsaCarriageControl(byte[] rawData, boolean isEbcdic) {
if (rawData == null || rawData.length == 0) return new byte[0];
String text;
if (isEbcdic) {
EbcdicTranslator trans = new EbcdicTranslator();
text = trans.ebcdicToString(rawData, 0, rawData.length);
} else {
text = new String(rawData, StandardCharsets.UTF_8);
}
String converted = convertAsaCarriageControl(text);
return converted.getBytes(StandardCharsets.UTF_8);
}
// =========================================================================
// IBM 1403/3211 Machine Carriage Control Translation
// =========================================================================
/**
* Translates IBM Machine Carriage Control Channel Command bytes into formatted text bytes.
*
* Command codes:
* 0x01: Write without line advance
* 0x09: Write and advance 1 line
* 0x11: Write and advance 2 lines
* 0x19: Write and advance 3 lines
* 0x89: Write and skip to channel 1 (Page Eject)
* 0x0B: Immediate space 1 line (no write)
* 0x13: Immediate space 2 lines (no write)
* 0x1B: Immediate space 3 lines (no write)
* 0x8B: Immediate skip to channel 1 (Page Eject)
*/
public static byte[] convertMachineCarriageControl(byte[] rawData) {
if (rawData == null || rawData.length == 0) return new byte[0];
ByteArrayOutputStream out = new ByteArrayOutputStream(rawData.length);
int pos = 0;
while (pos < rawData.length) {
int cmd = rawData[pos] & 0xFF;
pos++;
int recEnd = pos;
while (recEnd < rawData.length && rawData[recEnd] != 0x0A && rawData[recEnd] != 0x15) {
recEnd++;
}
int recLen = recEnd - pos;
byte[] recordData = new byte[recLen];
if (recLen > 0) {
System.arraycopy(rawData, pos, recordData, 0, recLen);
}
pos = (recEnd < rawData.length) ? recEnd + 1 : recEnd;
switch (cmd) {
case 0x01:
out.write(recordData, 0, recordData.length);
out.write('\r');
break;
case 0x09:
out.write(recordData, 0, recordData.length);
out.write('\n');
break;
case 0x11:
out.write(recordData, 0, recordData.length);
out.write('\n');
out.write('\n');
break;
case 0x19:
out.write(recordData, 0, recordData.length);
out.write('\n');
out.write('\n');
out.write('\n');
break;
case 0x89:
out.write(recordData, 0, recordData.length);
out.write('\n');
out.write(0x0C);
break;
case 0x0B:
out.write('\n');
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
case 0x13:
out.write('\n');
out.write('\n');
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
case 0x1B:
out.write('\n');
out.write('\n');
out.write('\n');
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
case 0x8B:
out.write(0x0C);
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
default:
out.write(recordData, 0, recordData.length);
out.write('\n');
break;
}
}
return out.toByteArray();
}
// =========================================================================
// Transfer Helpers
// =========================================================================
/**
* Issue CMS RECEIVE command for a spool file and pipe contents to local file.
*/
public int receiveSpoolFile(int spoolId, String localFilename, boolean convertCarriageControl) {
if (xfer == null) {
log.warning("CMSPrintXfer: ECLXfer is null");
return FTConstants.ECL_ERR_XFER_ABORT;
}
String hostFile = String.format("SPOOL%04d TEMP A", spoolId);
String options = "ASCII CRLF CMS";
return xfer.ReceiveFile(localFilename, hostFile, options);
}
/**
* Print a CMS file to the virtual printer spool.
*/
public int printCmsFile(String hostFilename, String printOptions) {
if (xfer == null) {
log.warning("CMSPrintXfer: ECLXfer is null");
return FTConstants.ECL_ERR_XFER_ABORT;
}
return 0;
}
}
@@ -192,7 +192,102 @@ public class FTConfig {
} }
public void setOptions(String opts) { public void setOptions(String opts) {
setOtherOptions(opts); parseOptions(opts);
}
/**
* Parse an options string (e.g. "ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(10,5) TRACKS REPLACE")
* into this FTConfig object.
*/
public void parseOptions(String opts) {
if (opts == null || opts.trim().isEmpty()) return;
String trimmed = opts.trim();
// Extract and process parenthesized or space-separated tokens
java.util.regex.Pattern recfmPattern = java.util.regex.Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
java.util.regex.Matcher recfmMatcher = recfmPattern.matcher(trimmed);
if (recfmMatcher.find()) {
setRecfm(recfmMatcher.group(1));
}
java.util.regex.Pattern lreclPattern = java.util.regex.Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher lreclMatcher = lreclPattern.matcher(trimmed);
if (lreclMatcher.find()) {
setLrecl(lreclMatcher.group(1));
}
java.util.regex.Pattern blkPattern = java.util.regex.Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher blkMatcher = blkPattern.matcher(trimmed);
if (blkMatcher.find()) {
setBlksize(blkMatcher.group(1));
}
java.util.regex.Pattern spacePattern = java.util.regex.Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
java.util.regex.Matcher spaceMatcher = spacePattern.matcher(trimmed);
if (spaceMatcher.find()) {
try {
this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
if (spaceMatcher.group(2) != null) {
this.secondarySpace = Integer.parseInt(spaceMatcher.group(2));
}
} catch (NumberFormatException ignored) {}
}
java.util.regex.Pattern avbPattern = java.util.regex.Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher avbMatcher = avbPattern.matcher(trimmed);
if (avbMatcher.find()) {
try {
this.avblock = Integer.parseInt(avbMatcher.group(1));
this.units = AllocationUnit.AVBLOCK;
} catch (NumberFormatException ignored) {}
}
java.util.regex.Pattern cpPattern = java.util.regex.Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
java.util.regex.Matcher cpMatcher = cpPattern.matcher(trimmed);
if (cpMatcher.find()) {
this.codePage = cpMatcher.group(1);
}
java.util.regex.Pattern mtuPattern = java.util.regex.Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher mtuMatcher = mtuPattern.matcher(trimmed);
if (mtuMatcher.find()) {
try {
setDftBufferSize(Integer.parseInt(mtuMatcher.group(1)));
} catch (NumberFormatException ignored) {}
}
String upper = trimmed.toUpperCase();
if (upper.contains("TRACKS") || upper.contains("TRK")) {
this.units = AllocationUnit.TRACKS;
} else if (upper.contains("CYLINDERS") || upper.contains("CYL")) {
this.units = AllocationUnit.CYLINDERS;
}
if (upper.contains("BINARY")) {
this.transferMode = TransferMode.BINARY;
} else if (upper.contains("ASCII")) {
this.transferMode = TransferMode.ASCII;
}
if (upper.contains("NOCRLF")) {
this.crAction = CrAction.KEEP;
} else if (upper.contains("CRLF")) {
this.crAction = CrAction.REMOVE;
}
if (upper.contains("APPEND")) {
this.existAction = ExistAction.APPEND;
} else if (upper.contains("REPLACE") || upper.contains("OVERWRITE")) {
this.existAction = ExistAction.REPLACE;
}
if (upper.contains("CICS")) {
this.hostType = HostType.CICS;
} else if (upper.contains("CMS") || upper.contains("VM")) {
this.hostType = HostType.CMS;
} else if (upper.contains("TSO")) {
this.hostType = HostType.TSO;
}
} }
// ========== Validation ========== // ========== Validation ==========
@@ -71,6 +71,9 @@ public class FTDft {
public void setMTUSize(int size) { public void setMTUSize(int size) {
this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF, this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, size)); Math.min(FTConstants.DFT_MAX_BUF, size));
if (listener != null && listener.getConfig() != null) {
listener.getConfig().setDftBufferSize(this.customMtuSize);
}
} }
public int getMTUSize() { public int getMTUSize() {
@@ -414,9 +417,9 @@ public class FTDft {
log.info("DFT host message: " + msg); log.info("DFT host message: " + msg);
String msgUpper = msg.toUpperCase(); String msgUpper = msg.toUpperCase();
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS")) { if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS") || msgUpper.contains("DFH0500") || msgUpper.contains("DFH0501")) {
listener.onTransferComplete(null); listener.onTransferComplete(null);
} else if (msgUpper.startsWith("TRANS") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || listener.getCurrentState() == FTState.ABORT_SENT) { } else if (msgUpper.startsWith("TRANS") || msgUpper.startsWith("DFH") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || (listener != null && listener.getCurrentState() == FTState.ABORT_SENT)) {
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg); listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
} else { } else {
// Informational message (default success) // Informational message (default success)
@@ -480,7 +483,7 @@ public class FTDft {
return; return;
} }
int bufferSize = config.getDftBufferSize(); int bufferSize = getMTUSize();
int numbytes = bufferSize - 27; int numbytes = bufferSize - 27;
byte[] readBuf = new byte[numbytes]; byte[] readBuf = new byte[numbytes];
int totalRead = 0; int totalRead = 0;
@@ -569,6 +572,71 @@ public class FTDft {
listener.onBytesTransferred(bytesTransferred); listener.onBytesTransferred(bytesTransferred);
} }
/**
* Explicitly send a framed data packet buffer to the host using DFT structured fields.
* Supports CICS, TSO, and VM/CMS data packaging.
*/
public void sendDataPacket(byte[] data, int offset, int length) {
if (data == null || length <= 0) return;
int mtu = getMTUSize();
int maxChunk = mtu - 27;
int sent = 0;
while (sent < length) {
int chunkLen = Math.min(length - sent, maxChunk);
ByteArrayOutputStream out = new ByteArrayOutputStream(chunkLen + 30);
out.write(AID_SF);
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA);
out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF);
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF));
out.write((int) ((recnum >> 16) & 0xFF));
out.write((int) ((recnum >> 8) & 0xFF));
out.write((int) (recnum & 0xFF));
recnum++;
out.write((TR_NOT_COMPRESSED >> 8) & 0xFF);
out.write(TR_NOT_COMPRESSED & 0xFF);
out.write(TR_BEGIN_DATA);
int dataFieldLen = chunkLen + 5;
out.write((dataFieldLen >> 8) & 0xFF);
out.write(dataFieldLen & 0xFF);
out.write(data, offset + sent, chunkLen);
byte[] result = out.toByteArray();
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
input.sendStructuredFieldData(result);
bytesTransferred += chunkLen;
if (listener != null) {
listener.onBytesTransferred(bytesTransferred);
}
sent += chunkLen;
}
}
/**
* Convenience method to send an entire data packet buffer.
*/
public void sendDataPacket(byte[] data) {
if (data != null) {
sendDataPacket(data, 0, data.length);
}
}
/** /**
* Read a byte from local file for upload, handling ASCII conversion and remapping. * Read a byte from local file for upload, handling ASCII conversion and remapping.
* Matching x3270 dft_ascii_read logic. * Matching x3270 dft_ascii_read logic.
@@ -537,6 +537,40 @@ public class InputProcessor {
return; return;
} }
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
int row = cursorAddr / cols;
int col = cursorAddr % cols;
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, row, col, aidCode, false, false, false
);
StringBuilder sfHex = new StringBuilder();
for (byte b : sf) {
sfHex.append(String.format("%02X ", b & 0xFF));
}
log.info(String.format(
"sendAid (graphic): goca=(%d, %d) row=%d col=%d cursorAddr=%d aid=0x%02X SF_HEX=[%s]",
gx, gy, row, col, cursorAddr, aidCode, sfHex.toString().trim()
));
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(AID_SF);
try {
out.write(sf);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
out.write(rma);
} catch (java.io.IOException ignored) {}
sendAidResponse(out.toByteArray());
return;
}
byte[] payload = buildReadModifiedInboundData(aidCode, false); byte[] payload = buildReadModifiedInboundData(aidCode, false);
sendAidResponse(payload); sendAidResponse(payload);
} }
@@ -588,42 +622,14 @@ public class InputProcessor {
out.write(AID_SF); out.write(AID_SF);
try { try {
out.write(sf); out.write(sf);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
out.write(rma);
} catch (java.io.IOException ignored) {} } catch (java.io.IOException ignored) {}
// Trailing AID + cursor address + modified fields matching HOD DS3270.sendMouseAid
out.write(aidCode);
byte[] caddr = encodeAddress(cursorAddr, numRows, cols);
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
if (screen.isFormatted()) {
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
int fieldStart = (i + 1) % size;
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
int pos = fieldStart;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
pos = (pos + 1) % size;
if (pos == fieldStart) break;
}
}
}
}
} else { } else {
out.write(aidCode); byte[] rma = buildReadModifiedInboundData(aidCode, false);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); try {
out.write(caddr[0] & 0xFF); out.write(rma);
out.write(caddr[1] & 0xFF); } catch (java.io.IOException ignored) {}
} }
sendAidResponse(out.toByteArray()); sendAidResponse(out.toByteArray());
@@ -10,14 +10,16 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* Network Virtual Terminal (NVT) processor. * Network Virtual Terminal (NVT) processor.
* Handles ASCII / ANSI VT100 character stream processing, cursor positioning, * Handles ASCII / ANSI VT100 / VT220 / VT320 character stream processing, cursor positioning,
* escape sequence decoding, terminal capability reports, and NVT transmission. * escape sequence decoding, terminal capability reports, OSC title/clipboard integration,
* DEC private modes, and NVT transmission.
*/ */
public class NvtProcessor { public class NvtProcessor {
@@ -26,12 +28,15 @@ public class NvtProcessor {
private final ScreenBuffer screenBuffer; private final ScreenBuffer screenBuffer;
private final EbcdicTranslator translator; private final EbcdicTranslator translator;
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>(); private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
private final List<NvtTitleListener> titleListeners = new CopyOnWriteArrayList<>();
// Escape sequence parser states // Escape sequence parser states
private static final int STATE_NORMAL = 0; private static final int STATE_NORMAL = 0;
private static final int STATE_ESC = 1; private static final int STATE_ESC = 1;
private static final int STATE_CSI = 2; private static final int STATE_CSI = 2;
private static final int STATE_CHARSET = 3; private static final int STATE_CHARSET = 3;
private static final int STATE_OSC = 4;
private static final int STATE_OSC_ESC = 5;
private int parseState = STATE_NORMAL; private int parseState = STATE_NORMAL;
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream(); private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
@@ -39,14 +44,24 @@ public class NvtProcessor {
// Output sender callback // Output sender callback
private OutputSender outputSender; private OutputSender outputSender;
// Clipboard handler for OSC 52
private ClipboardHandler clipboardHandler;
// Terminal window title
private String terminalTitle = "";
// Graphic Rendition state // Graphic Rendition state
private byte currentFg = 0; private byte currentFg = 0;
private byte currentBg = 0; private byte currentBg = 0;
private byte currentGr = 0; private byte currentGr = 0;
// Saved cursor position // Saved cursor position and attributes
private int savedCursorRow = 0; private int savedCursorRow = 0;
private int savedCursorCol = 0; private int savedCursorCol = 0;
private byte savedFg = 0;
private byte savedBg = 0;
private byte savedGr = 0;
private boolean savedOriginMode = false;
// Scrolling margins (0-indexed, inclusive) // Scrolling margins (0-indexed, inclusive)
private int scrollTop = 0; private int scrollTop = 0;
@@ -55,19 +70,44 @@ public class NvtProcessor {
// Tab stops // Tab stops
private boolean[] tabStops; private boolean[] tabStops;
// Cursor visibility // Terminal Modes & Options
private boolean cursorVisible = true; private boolean cursorVisible = true;
private boolean applicationKeypad = false;
private boolean applicationCursorKeys = false;
private boolean originMode = false;
private boolean autoWrap = true;
private boolean insertMode = false;
private boolean reverseVideo = false;
private boolean newLineMode = false;
private boolean alternateBufferActive = false;
// Line drawing mode // Device Attributes responses (VT100 default, VT220 available)
private boolean lineDrawingG0 = false; private String primaryDeviceAttributes = "\u001B[?1;2c";
private boolean lineDrawingG1 = false; private String secondaryDeviceAttributes = "\u001B[>1;10;0c";
private boolean activeCharsetG1 = false;
// Character Sets: G0, G1, G2, G3 designators ('B' = ASCII, '0' = DEC Special Graphics, 'A' = UK, '<' = DEC Supplemental, 'K' = German)
private char g0Charset = 'B';
private char g1Charset = '0';
private char g2Charset = 'B';
private char g3Charset = 'B';
private int activeCharset = 0; // 0=G0, 1=G1, 2=G2, 3=G3
private int singleShiftCharset = -1; // -1 none, 2=G2 (SS2), 3=G3 (SS3)
@FunctionalInterface @FunctionalInterface
public interface OutputSender { public interface OutputSender {
void sendRaw(byte[] data) throws IOException; void sendRaw(byte[] data) throws IOException;
} }
@FunctionalInterface
public interface NvtTitleListener {
void onTitleChanged(String title);
}
public interface ClipboardHandler {
String getClipboardText();
void setClipboardText(String text);
}
public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) { public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) {
this.screenBuffer = screenBuffer; this.screenBuffer = screenBuffer;
this.translator = translator; this.translator = translator;
@@ -86,6 +126,14 @@ public class NvtProcessor {
this.outputSender = outputSender; this.outputSender = outputSender;
} }
public void setClipboardHandler(ClipboardHandler clipboardHandler) {
this.clipboardHandler = clipboardHandler;
}
public ClipboardHandler getClipboardHandler() {
return clipboardHandler;
}
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l); screenListeners.add(l);
} }
@@ -94,10 +142,67 @@ public class NvtProcessor {
screenListeners.remove(l); screenListeners.remove(l);
} }
public boolean isCursorVisible() { public void addTitleListener(NvtTitleListener l) {
return cursorVisible; titleListeners.add(l);
} }
public void removeTitleListener(NvtTitleListener l) {
titleListeners.remove(l);
}
public String getTerminalTitle() {
return terminalTitle;
}
public void setTerminalTitle(String title) {
this.terminalTitle = (title != null) ? title : "";
for (NvtTitleListener l : titleListeners) {
try {
l.onTitleChanged(this.terminalTitle);
} catch (Exception e) {
log.warning("Error in NVT title listener: " + e.getMessage());
}
}
}
public boolean isCursorVisible() { return cursorVisible; }
public void setCursorVisible(boolean visible) { this.cursorVisible = visible; }
public boolean isApplicationKeypad() { return applicationKeypad; }
public void setApplicationKeypad(boolean appKeypad) { this.applicationKeypad = appKeypad; }
public boolean isApplicationCursorKeys() { return applicationCursorKeys; }
public void setApplicationCursorKeys(boolean appCursorKeys) { this.applicationCursorKeys = appCursorKeys; }
public boolean isOriginMode() { return originMode; }
public void setOriginMode(boolean originMode) { this.originMode = originMode; }
public boolean isAutoWrap() { return autoWrap; }
public void setAutoWrap(boolean autoWrap) { this.autoWrap = autoWrap; }
public boolean isInsertMode() { return insertMode; }
public void setInsertMode(boolean insertMode) { this.insertMode = insertMode; }
public boolean isReverseVideo() { return reverseVideo; }
public void setReverseVideo(boolean reverseVideo) { this.reverseVideo = reverseVideo; }
public boolean isNewLineMode() { return newLineMode; }
public void setNewLineMode(boolean newLineMode) { this.newLineMode = newLineMode; }
public boolean isAlternateBufferActive() { return alternateBufferActive; }
public String getPrimaryDeviceAttributes() { return primaryDeviceAttributes; }
public void setPrimaryDeviceAttributes(String da) { this.primaryDeviceAttributes = da; }
public String getSecondaryDeviceAttributes() { return secondaryDeviceAttributes; }
public void setSecondaryDeviceAttributes(String da2) { this.secondaryDeviceAttributes = da2; }
public char getG0Charset() { return g0Charset; }
public char getG1Charset() { return g1Charset; }
public char getG2Charset() { return g2Charset; }
public char getG3Charset() { return g3Charset; }
public int getActiveCharset() { return activeCharset; }
/** /**
* Process incoming ASCII NVT data bytes. * Process incoming ASCII NVT data bytes.
*/ */
@@ -129,6 +234,9 @@ public class NvtProcessor {
} else if (b == 0x0A) { // LF } else if (b == 0x0A) { // LF
int r = curAddr / cols; int r = curAddr / cols;
int c = curAddr % cols; int c = curAddr % cols;
if (newLineMode) {
c = 0; // CR + LF in New Line Mode
}
if (r == effectiveScrollBottom) { if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom); scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
} else if (r < rows - 1) { } else if (r < rows - 1) {
@@ -160,27 +268,50 @@ public class NvtProcessor {
for (ScreenUpdateListener l : screenListeners) { for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm(); l.onSoundAlarm();
} }
} else if (b == 0x0E) { // SO (Select G1 charset) } else if (b == 0x0E) { // SO / LS1 (Select G1 charset)
activeCharsetG1 = true; activeCharset = 1;
} else if (b == 0x0F) { // SI (Select G0 charset) } else if (b == 0x0F) { // SI / LS0 (Select G0 charset)
activeCharsetG1 = false; activeCharset = 0;
} else if (b >= 0x20 && b <= 0xFF) { // Printable character } else if (b >= 0x20 && b <= 0xFF) { // Printable character
char ch = (char) b; char setDesignator;
if (activeCharsetG1 ? lineDrawingG1 : lineDrawingG0) { if (singleShiftCharset >= 0) {
ch = mapVt100SpecialGraphics(ch); setDesignator = (singleShiftCharset == 2) ? g2Charset : g3Charset;
singleShiftCharset = -1; // Single shift applies only to next character
} else {
switch (activeCharset) {
case 1: setDesignator = g1Charset; break;
case 2: setDesignator = g2Charset; break;
case 3: setDesignator = g3Charset; break;
default: setDesignator = g0Charset; break;
}
} }
char ch = translateCharacter((char) b, setDesignator);
int r = curAddr / cols; int r = curAddr / cols;
int c = curAddr % cols; int c = curAddr % cols;
if (c >= cols) { if (c >= cols) {
c = 0; if (autoWrap) {
if (r == effectiveScrollBottom) { c = 0;
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom); if (r == effectiveScrollBottom) {
} else if (r < rows - 1) { scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
r++; } else if (r < rows - 1) {
r++;
}
curAddr = r * cols + c;
} else {
c = cols - 1;
curAddr = r * cols + c;
}
}
if (insertMode) {
// Shift characters right in line
int lineStart = r * cols;
for (int col = cols - 1; col > c; col--) {
screenBuffer.getCell(lineStart + col).copyFrom(screenBuffer.getCell(lineStart + col - 1));
} }
curAddr = r * cols + c;
} }
int ebc = translator.unicodeToEbcdic(ch); int ebc = translator.unicodeToEbcdic(ch);
@@ -194,17 +325,21 @@ public class NvtProcessor {
c++; c++;
if (c >= cols) { if (c >= cols) {
if (r < rows - 1) { if (autoWrap) {
if (r == effectiveScrollBottom) { if (r < rows - 1) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom); if (r == effectiveScrollBottom) {
c = 0; scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0;
} else {
r++;
c = 0;
}
} else { } else {
r++; scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0; c = 0;
} }
} else { } else {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom); c = cols - 1; // Stay at right margin without wrapping
c = 0;
} }
} }
curAddr = r * cols + c; curAddr = r * cols + c;
@@ -213,14 +348,26 @@ public class NvtProcessor {
escBuffer.write(b); escBuffer.write(b);
if (b == '[') { if (b == '[') {
parseState = STATE_CSI; parseState = STATE_CSI;
} else if (b == '(' || b == ')') { } else if (b == ']') {
parseState = STATE_OSC;
escBuffer.reset();
escBuffer.write(b);
} else if (b == '(' || b == ')' || b == '*' || b == '+') {
parseState = STATE_CHARSET; parseState = STATE_CHARSET;
} else if (b == '7') { // DECSC - Save cursor } else if (b == '7') { // DECSC - Save cursor
savedCursorRow = curAddr / cols; savedCursorRow = curAddr / cols;
savedCursorCol = curAddr % cols; savedCursorCol = curAddr % cols;
savedFg = currentFg;
savedBg = currentBg;
savedGr = currentGr;
savedOriginMode = originMode;
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (b == '8') { // DECRC - Restore cursor } else if (b == '8') { // DECRC - Restore cursor
curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol); curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol);
currentFg = savedFg;
currentBg = savedBg;
currentGr = savedGr;
originMode = savedOriginMode;
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (b == 'D') { // IND - Index (down 1 line) } else if (b == 'D') { // IND - Index (down 1 line)
int r = curAddr / cols; int r = curAddr / cols;
@@ -266,8 +413,41 @@ public class NvtProcessor {
scrollTop = 0; scrollTop = 0;
scrollBottom = rows - 1; scrollBottom = rows - 1;
cursorVisible = true; cursorVisible = true;
applicationKeypad = false;
applicationCursorKeys = false;
originMode = false;
autoWrap = true;
insertMode = false;
reverseVideo = false;
newLineMode = false;
g0Charset = 'B';
g1Charset = '0';
g2Charset = 'B';
g3Charset = 'B';
activeCharset = 0;
singleShiftCharset = -1;
initTabStops(); initTabStops();
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (b == '=') { // DECKPAM - Keypad Application Mode
applicationKeypad = true;
parseState = STATE_NORMAL;
} else if (b == '>') { // DECKPNM - Keypad Numeric Mode
applicationKeypad = false;
parseState = STATE_NORMAL;
} else if (b == 'N') { // SS2 - Single Shift G2
singleShiftCharset = 2;
parseState = STATE_NORMAL;
} else if (b == 'O') { // SS3 - Single Shift G3
singleShiftCharset = 3;
parseState = STATE_NORMAL;
} else if (b == 'n') { // LS2 - Locking Shift 2 -> G2
activeCharset = 2;
parseState = STATE_NORMAL;
} else if (b == 'o') { // LS3 - Locking Shift 3 -> G3
activeCharset = 3;
parseState = STATE_NORMAL;
} else if (b == '\\') { // ST - String Terminator
parseState = STATE_NORMAL;
} else { } else {
// Unknown 2-byte escape, return to normal // Unknown 2-byte escape, return to normal
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
@@ -275,12 +455,36 @@ public class NvtProcessor {
} else if (parseState == STATE_CHARSET) { } else if (parseState == STATE_CHARSET) {
byte[] seq = escBuffer.toByteArray(); byte[] seq = escBuffer.toByteArray();
if (seq.length >= 2) { if (seq.length >= 2) {
boolean isG1 = (seq[1] == ')'); char target = (char) seq[1];
boolean isLineDraw = (b == '0'); char designator = (char) b;
if (isG1) lineDrawingG1 = isLineDraw; if (target == '(') g0Charset = designator;
else lineDrawingG0 = isLineDraw; else if (target == ')') g1Charset = designator;
else if (target == '*') g2Charset = designator;
else if (target == '+') g3Charset = designator;
} }
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (parseState == STATE_OSC) {
if (b == 0x07) { // BEL terminator
handleOSC(escBuffer.toByteArray());
parseState = STATE_NORMAL;
} else if (b == 0x1B) { // Possible ESC \ (ST)
parseState = STATE_OSC_ESC;
} else if (b == 0x9C) { // 8-bit ST
handleOSC(escBuffer.toByteArray());
parseState = STATE_NORMAL;
} else {
escBuffer.write(b);
}
} else if (parseState == STATE_OSC_ESC) {
if (b == '\\') { // ESC \ (ST) terminator
handleOSC(escBuffer.toByteArray());
parseState = STATE_NORMAL;
} else {
// False alarm on ST, put ESC and byte into OSC buffer
escBuffer.write(0x1B);
escBuffer.write(b);
parseState = STATE_OSC;
}
} else if (parseState == STATE_CSI) { } else if (parseState == STATE_CSI) {
escBuffer.write(b); escBuffer.write(b);
// CSI final bytes are in the range 0x40..0x7E // CSI final bytes are in the range 0x40..0x7E
@@ -298,6 +502,81 @@ public class NvtProcessor {
notifyScreenUpdated(); notifyScreenUpdated();
} }
/**
* Handle Operating System Command (OSC) escape sequences.
*/
public void handleOSC(byte[] oscData) {
if (oscData == null || oscData.length == 0) return;
int start = 0;
if (oscData[0] == ']') {
start = 1;
}
if (start >= oscData.length) return;
String oscStr = new String(oscData, start, oscData.length - start, StandardCharsets.UTF_8);
int semi = oscStr.indexOf(';');
if (semi < 0) return;
String psStr = oscStr.substring(0, semi).trim();
String pt = oscStr.substring(semi + 1);
int ps;
try {
ps = Integer.parseInt(psStr);
} catch (NumberFormatException e) {
return;
}
switch (ps) {
case 0: // Set icon name and window title
case 2: // Set window title
setTerminalTitle(pt);
break;
case 1: // Set icon name
// No-op or update title if desired
break;
case 52: // OSC 52 Clipboard operations (read/write Base64 data)
handleOsc52(pt);
break;
default:
log.fine("Unhandled OSC command: " + ps);
break;
}
}
private void handleOsc52(String pt) {
// Format: [target];[base64Data]
int semi = pt.indexOf(';');
String target = "c";
String data = pt;
if (semi >= 0) {
target = pt.substring(0, semi);
data = pt.substring(semi + 1);
}
if ("?".equals(data)) {
// Clipboard query request: reply with OSC 52 containing base64 encoded clipboard
if (clipboardHandler != null) {
String text = clipboardHandler.getClipboardText();
if (text == null) text = "";
String b64 = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
sendResponseString(String.format("\u001B]52;%s;%s\u0007", target, b64));
}
} else {
// Clipboard write request: decode base64 and set to clipboard handler
try {
byte[] decoded = Base64.getDecoder().decode(data.getBytes(StandardCharsets.US_ASCII));
String text = new String(decoded, StandardCharsets.UTF_8);
if (clipboardHandler != null) {
clipboardHandler.setClipboardText(text);
}
} catch (Exception e) {
log.warning("Invalid OSC 52 Base64 data: " + e.getMessage());
}
}
}
/** /**
* Parse and execute an ANSI CSI escape sequence. * Parse and execute an ANSI CSI escape sequence.
* Returns updated cursor address. * Returns updated cursor address.
@@ -314,26 +593,60 @@ public class NvtProcessor {
int r = curAddr / cols; int r = curAddr / cols;
int c = curAddr % cols; int c = curAddr % cols;
// Check for DECSTR: ESC [ ! p (DEC Soft Terminal Reset)
if (finalByte == 'p' && (paramStr.startsWith("!") || paramStr.contains("!"))) {
cursorVisible = true;
scrollTop = 0;
scrollBottom = rows - 1;
applicationCursorKeys = false;
applicationKeypad = false;
originMode = false;
autoWrap = true;
insertMode = false;
currentFg = 0;
currentBg = 0;
currentGr = 0;
g0Charset = 'B';
g1Charset = '0';
g2Charset = 'B';
g3Charset = 'B';
activeCharset = 0;
singleShiftCharset = -1;
savedCursorRow = 0;
savedCursorCol = 0;
savedFg = 0;
savedBg = 0;
savedGr = 0;
savedOriginMode = false;
return curAddr;
}
switch (finalByte) { switch (finalByte) {
case 'H': // CUP - Cursor Position case 'H': // CUP - Cursor Position
case 'f': // HVP - Horizontal and Vertical Position case 'f': // HVP - Horizontal and Vertical Position
{ {
int p1 = parseParam(params, 0, 1) - 1; int p1 = parseParam(params, 0, 1) - 1;
int p2 = parseParam(params, 1, 1) - 1; int p2 = parseParam(params, 1, 1) - 1;
r = Math.max(0, Math.min(rows - 1, p1)); if (originMode) {
r = Math.max(effectiveScrollTop, Math.min(effectiveScrollBottom, effectiveScrollTop + p1));
} else {
r = Math.max(0, Math.min(rows - 1, p1));
}
c = Math.max(0, Math.min(cols - 1, p2)); c = Math.max(0, Math.min(cols - 1, p2));
return r * cols + c; return r * cols + c;
} }
case 'A': // CUU - Cursor Up case 'A': // CUU - Cursor Up
{ {
int count = parseParam(params, 0, 1); int count = parseParam(params, 0, 1);
r = Math.max(0, r - count); int topLimit = originMode ? effectiveScrollTop : 0;
r = Math.max(topLimit, r - count);
return r * cols + c; return r * cols + c;
} }
case 'B': // CUD - Cursor Down case 'B': // CUD - Cursor Down
{ {
int count = parseParam(params, 0, 1); int count = parseParam(params, 0, 1);
r = Math.min(rows - 1, r + count); int botLimit = originMode ? effectiveScrollBottom : rows - 1;
r = Math.min(botLimit, r + count);
return r * cols + c; return r * cols + c;
} }
case 'C': // CUF - Cursor Forward case 'C': // CUF - Cursor Forward
@@ -351,13 +664,15 @@ public class NvtProcessor {
case 'E': // CNL - Cursor Next Line case 'E': // CNL - Cursor Next Line
{ {
int count = parseParam(params, 0, 1); int count = parseParam(params, 0, 1);
r = Math.min(rows - 1, r + count); int botLimit = originMode ? effectiveScrollBottom : rows - 1;
r = Math.min(botLimit, r + count);
return r * cols; // column 0 return r * cols; // column 0
} }
case 'F': // CPL - Cursor Previous Line case 'F': // CPL - Cursor Previous Line
{ {
int count = parseParam(params, 0, 1); int count = parseParam(params, 0, 1);
r = Math.max(0, r - count); int topLimit = originMode ? effectiveScrollTop : 0;
r = Math.max(topLimit, r - count);
return r * cols; // column 0 return r * cols; // column 0
} }
case 'G': // CHA - Cursor Horizontal Absolute case 'G': // CHA - Cursor Horizontal Absolute
@@ -370,7 +685,11 @@ public class NvtProcessor {
case 'd': // VPA - Vertical Position Absolute case 'd': // VPA - Vertical Position Absolute
{ {
int p = parseParam(params, 0, 1) - 1; int p = parseParam(params, 0, 1) - 1;
r = Math.max(0, Math.min(rows - 1, p)); if (originMode) {
r = Math.max(effectiveScrollTop, Math.min(effectiveScrollBottom, effectiveScrollTop + p));
} else {
r = Math.max(0, Math.min(rows - 1, p));
}
return r * cols + c; return r * cols + c;
} }
case 'J': // ED - Erase in Display case 'J': // ED - Erase in Display
@@ -476,7 +795,7 @@ public class NvtProcessor {
scrollTop = 0; scrollTop = 0;
scrollBottom = rows - 1; scrollBottom = rows - 1;
} }
return 0; // Move cursor to home return originMode ? (scrollTop * cols) : 0; // Move cursor to home
} }
case 'm': // SGR - Select Graphic Rendition case 'm': // SGR - Select Graphic Rendition
{ {
@@ -497,25 +816,51 @@ public class NvtProcessor {
} }
case 'n': // DSR - Device Status Report case 'n': // DSR - Device Status Report
{ {
int code = parseParam(params, 0, 0); if (paramStr.startsWith("?")) {
if (code == 6) { // Cursor position request int code = parseParam(params, 0, 0);
// Reply: ESC [ <row> ; <col> R (1-indexed) if (code == 6) { // Extended Cursor Position Report (DECXCPR)
String response = String.format("\u001B[%d;%dR", r + 1, c + 1); int repRow = (originMode ? r - effectiveScrollTop : r) + 1;
sendResponseString(response); int repCol = c + 1;
} else if (code == 5) { // Status report request String response = String.format("\u001B[?%d;%d;1R", repRow, repCol);
sendResponseString("\u001B[0n"); // OK sendResponseString(response);
} else if (code == 15) { // Printer Status Report
sendResponseString("\u001B[?13n"); // No printer attached / ready
} else if (code == 25) { // User-defined keys (UDK) / Cursor status
sendResponseString(cursorVisible ? "\u001B[?20n" : "\u001B[?21n");
} else if (code == 26) { // Keyboard dialect status
sendResponseString("\u001B[?27;1n"); // North American / US keyboard
} else if (code == 62 || code == 63) { // Macro / Memory Checksum
sendResponseString("\u001B[?63;0n");
}
} else {
int code = parseParam(params, 0, 0);
if (code == 6) { // Cursor position request (1-indexed)
String response = String.format("\u001B[%d;%dR", r + 1, c + 1);
sendResponseString(response);
} else if (code == 5) { // Status report request
sendResponseString("\u001B[0n"); // OK
}
} }
return curAddr; return curAddr;
} }
case 'c': // DA - Device Attributes case 'c': // DA - Device Attributes
{ {
int code = parseParam(params, 0, 0); if (paramStr.startsWith(">")) {
if (code == 0) { // Secondary DA (DA2)
// Identify as standard VT100 with Advanced Video Option sendResponseString(secondaryDeviceAttributes);
sendResponseString("\u001B[?1;2c"); } else {
// Primary DA
sendResponseString(primaryDeviceAttributes);
} }
return curAddr; return curAddr;
} }
case 'x': // DECREQTPARM - Request Terminal Parameters
{
int sol = parseParam(params, 0, 1);
int repType = (sol == 0) ? 2 : 3;
sendResponseString(String.format("\u001B[%d;1;1;120;120;1;0x", repType));
return curAddr;
}
case 'g': // TBC - Tab Clear case 'g': // TBC - Tab Clear
{ {
int mode = parseParam(params, 0, 0); int mode = parseParam(params, 0, 0);
@@ -529,9 +874,16 @@ public class NvtProcessor {
case 'h': // Set Mode / Private Mode case 'h': // Set Mode / Private Mode
{ {
if (paramStr.startsWith("?")) { if (paramStr.startsWith("?")) {
String sub = paramStr.substring(1).trim(); String[] subParams = paramStr.substring(1).split(";");
if ("25".equals(sub)) { for (String sub : subParams) {
cursorVisible = true; int mode = parseParam(new String[]{sub}, 0, 0);
curAddr = applyDecPrivateMode(mode, true, curAddr, rows, cols);
}
} else {
for (String p : params) {
int mode = parseParam(new String[]{p}, 0, 0);
if (mode == 4) insertMode = true; // IRM - Insert Mode
else if (mode == 20) newLineMode = true; // LNM - New Line Mode
} }
} }
return curAddr; return curAddr;
@@ -539,9 +891,16 @@ public class NvtProcessor {
case 'l': // Reset Mode / Private Mode case 'l': // Reset Mode / Private Mode
{ {
if (paramStr.startsWith("?")) { if (paramStr.startsWith("?")) {
String sub = paramStr.substring(1).trim(); String[] subParams = paramStr.substring(1).split(";");
if ("25".equals(sub)) { for (String sub : subParams) {
cursorVisible = false; int mode = parseParam(new String[]{sub}, 0, 0);
curAddr = applyDecPrivateMode(mode, false, curAddr, rows, cols);
}
} else {
for (String p : params) {
int mode = parseParam(new String[]{p}, 0, 0);
if (mode == 4) insertMode = false; // IRM - Replace Mode
else if (mode == 20) newLineMode = false; // LNM - Line Feed Mode
} }
} }
return curAddr; return curAddr;
@@ -549,16 +908,185 @@ public class NvtProcessor {
case 's': // Save cursor case 's': // Save cursor
savedCursorRow = r; savedCursorRow = r;
savedCursorCol = c; savedCursorCol = c;
savedFg = currentFg;
savedBg = currentBg;
savedGr = currentGr;
savedOriginMode = originMode;
return curAddr; return curAddr;
case 'u': // Restore cursor case 'u': // Restore cursor
r = Math.min(rows - 1, savedCursorRow); r = Math.min(rows - 1, savedCursorRow);
c = Math.min(cols - 1, savedCursorCol); c = Math.min(cols - 1, savedCursorCol);
currentFg = savedFg;
currentBg = savedBg;
currentGr = savedGr;
originMode = savedOriginMode;
return r * cols + c; return r * cols + c;
default: default:
return curAddr; return curAddr;
} }
} }
private int applyDecPrivateMode(int mode, boolean set, int curAddr, int rows, int cols) {
switch (mode) {
case 1: // DECCKM - Cursor Keys Mode
applicationCursorKeys = set;
break;
case 3: // DECCOLM - Column Mode (80 vs 132 columns)
screenBuffer.clear();
scrollTop = 0;
scrollBottom = rows - 1;
return 0;
case 5: // DECSCNM - Screen Mode (Reverse Video)
reverseVideo = set;
break;
case 6: // DECOM - Origin Mode
originMode = set;
return (originMode ? scrollTop : 0) * cols;
case 7: // DECAWM - Auto Wrap Mode
autoWrap = set;
break;
case 25: // DECTCEM - Text Cursor Enable
cursorVisible = set;
break;
case 47:
case 1047:
case 1048:
case 1049: // Alternate Screen Buffer modes
alternateBufferActive = set;
if (mode == 1049 && set) {
screenBuffer.clear();
return 0;
}
break;
default:
break;
}
return curAddr;
}
/**
* Translate key codes in VT application keypad mode (DECKPAM).
*/
public byte[] processApplicationKeypad(int keyCode) {
String seq = mapKeypadKey(keyCode);
return seq != null ? seq.getBytes(StandardCharsets.US_ASCII) : null;
}
public String mapKeypadKey(int keyCode) {
if (!applicationKeypad) return null;
switch (keyCode) {
case 0x60: case 0x30: return "\u001BOp"; // 0
case 0x61: case 0x31: return "\u001BOq"; // 1
case 0x62: case 0x32: return "\u001BOr"; // 2
case 0x63: case 0x33: return "\u001BOs"; // 3
case 0x64: case 0x34: return "\u001BOt"; // 4
case 0x65: case 0x35: return "\u001BOu"; // 5
case 0x66: case 0x36: return "\u001BOv"; // 6
case 0x67: case 0x37: return "\u001BOw"; // 7
case 0x68: case 0x38: return "\u001BOx"; // 8
case 0x69: case 0x39: return "\u001BOy"; // 9
case 0x6D: case 0x2D: return "\u001BOm"; // Minus (-)
case 0x2C: return "\u001BOl"; // Comma (,)
case 0x6E: case 0x2E: return "\u001BOn"; // Period (.)
case 0x0A: return "\u001BOM"; // Keypad Enter
case 0x6F: case 0x2F: return "\u001BOQ"; // Slash (/)
case 0x6A: case 0x2A: return "\u001BOR"; // Asterisk (*)
case 0x6B: case 0x2B: return "\u001BOS"; // Plus (+)
default: return null;
}
}
/**
* Map a full key event (key code, character, modifiers) into standard VT / NVT byte sequence.
*/
public String mapKey(int keyCode, char keyChar, boolean shift, boolean ctrl, boolean alt) {
// Check application keypad first if enabled
if (applicationKeypad) {
String kp = mapKeypadKey(keyCode);
if (kp != null) return kp;
}
// Cursor navigation keys
switch (keyCode) {
case 0x26: // VK_UP
return applicationCursorKeys ? "\u001BOA" : "\u001B[A";
case 0x28: // VK_DOWN
return applicationCursorKeys ? "\u001BOB" : "\u001B[B";
case 0x27: // VK_RIGHT
return applicationCursorKeys ? "\u001BOC" : "\u001B[C";
case 0x25: // VK_LEFT
return applicationCursorKeys ? "\u001BOD" : "\u001B[D";
case 0x24: // VK_HOME
return "\u001B[1~";
case 0x23: // VK_END
return "\u001B[4~";
case 0x21: // VK_PAGE_UP
return "\u001B[5~";
case 0x22: // VK_PAGE_DOWN
return "\u001B[6~";
case 0x9B: // VK_INSERT
return "\u001B[2~";
case 0x7F: case 0x08: // VK_DELETE / BACKSPACE
if (keyCode == 0x7F || keyCode == 0x93) return "\u001B[3~";
return "\b";
case 0x09: // VK_TAB
return shift ? "\u001B[Z" : "\t";
case 0x0A: // VK_ENTER
return newLineMode ? "\r\n" : "\r";
}
// Function keys F1..F20
if (keyCode >= 0x70 && keyCode <= 0x83) { // VK_F1..VK_F20
int fn = keyCode - 0x70 + 1;
return getFunctionKeySequence(fn);
}
if (ctrl && !alt) {
if (keyCode >= 0x41 && keyCode <= 0x5A) {
char ctrlChar = (char) (keyCode - 0x41 + 1);
return String.valueOf(ctrlChar);
} else if (keyCode == 0xDB) { // VK_OPEN_BRACKET
return "\u001B";
} else if (keyCode == 0xDC) { // VK_BACK_SLASH
return "\u001C";
} else if (keyCode == 0xDD) { // VK_CLOSE_BRACKET
return "\u001D";
}
}
if (keyChar != 0 && keyChar != 0xFFFF && !ctrl && !alt) {
return String.valueOf(keyChar);
}
return null;
}
public String getFunctionKeySequence(int n) {
switch (n) {
case 1: return "\u001BOP";
case 2: return "\u001BOQ";
case 3: return "\u001BOR";
case 4: return "\u001BOS";
case 5: return "\u001B[15~";
case 6: return "\u001B[17~";
case 7: return "\u001B[18~";
case 8: return "\u001B[19~";
case 9: return "\u001B[20~";
case 10: return "\u001B[21~";
case 11: return "\u001B[23~";
case 12: return "\u001B[24~";
case 13: return "\u001B[25~";
case 14: return "\u001B[26~";
case 15: return "\u001B[28~";
case 16: return "\u001B[29~";
case 17: return "\u001B[31~";
case 18: return "\u001B[32~";
case 19: return "\u001B[33~";
case 20: return "\u001B[34~";
default: return "";
}
}
private void sendResponseString(String s) { private void sendResponseString(String s) {
if (outputSender != null) { if (outputSender != null) {
try { try {
@@ -573,7 +1101,9 @@ public class NvtProcessor {
if (params != null && idx < params.length && !params[idx].trim().isEmpty()) { if (params != null && idx < params.length && !params[idx].trim().isEmpty()) {
try { try {
String val = params[idx].trim(); String val = params[idx].trim();
if (val.startsWith("?")) val = val.substring(1); if (val.startsWith("?") || val.startsWith(">") || val.startsWith("!")) {
val = val.substring(1).trim();
}
return Integer.parseInt(val); return Integer.parseInt(val);
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
@@ -628,6 +1158,32 @@ public class NvtProcessor {
} }
} }
private char translateCharacter(char c, char charset) {
switch (charset) {
case '0': // DEC Special Graphics / Line Drawing
return mapVt100SpecialGraphics(c);
case 'A': // UK National (maps # to £)
if (c == '#') return '£';
return c;
case 'K': // German
switch (c) {
case '@': return '§';
case '[': return 'Ä';
case '\\': return 'Ö';
case ']': return 'Ü';
case '{': return 'ä';
case '|': return 'ö';
case '}': return 'ü';
case '~': return 'ß';
default: return c;
}
case '<': // DEC Supplemental
case 'B': // US ASCII / Latin-1
default:
return c;
}
}
private char mapVt100SpecialGraphics(char c) { private char mapVt100SpecialGraphics(char c) {
switch (c) { switch (c) {
case 'j': return '┘'; case 'j': return '┘';
@@ -708,7 +1264,11 @@ public class NvtProcessor {
public void sendNVTChar(char c) throws IOException { public void sendNVTChar(char c) throws IOException {
if (outputSender != null) { if (outputSender != null) {
if (c == '\n') { if (c == '\n') {
outputSender.sendRaw(new byte[] { (byte) '\r', (byte) '\n' }); if (newLineMode) {
outputSender.sendRaw(new byte[] { (byte) '\r', (byte) '\n' });
} else {
outputSender.sendRaw(new byte[] { (byte) '\n' });
}
} else { } else {
outputSender.sendRaw(new byte[] { (byte) c }); outputSender.sendRaw(new byte[] { (byte) c });
} }
@@ -20,6 +20,7 @@ public class PD3270 {
private static final Logger log = Logger.getLogger(PD3270.class.getName()); private static final Logger log = Logger.getLogger(PD3270.class.getName());
private final PrinterConfig config; private final PrinterConfig config;
private PrinterDefinitionTable pdt;
private String destination; private String destination;
private boolean open = false; private boolean open = false;
@@ -38,6 +39,7 @@ public class PD3270 {
public PD3270(PrinterConfig config) { public PD3270(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig(); this.config = config != null ? config : new PrinterConfig();
this.pdt = this.config.getPrinterDefinitionTable();
this.destination = this.config.getDestinationTarget(); this.destination = this.config.getDestinationTarget();
} }
@@ -118,6 +120,13 @@ public class PD3270 {
if (!open) { if (!open) {
openPrinter(destination); openPrinter(destination);
} }
if (pdt != null) {
byte[] translated = pdt.translateChar(c);
if (translated != null && translated.length > 0) {
writePrintBytes(translated, 0, translated.length);
return;
}
}
try { try {
byte[] b = String.valueOf(c).getBytes(outputCharset); byte[] b = String.valueOf(c).getBytes(outputCharset);
memoryStream.write(b); memoryStream.write(b);
@@ -141,6 +150,12 @@ public class PD3270 {
if (!open) { if (!open) {
openPrinter(destination); openPrinter(destination);
} }
if (pdt != null) {
for (int i = 0; i < s.length(); i++) {
writePrintChar(s.charAt(i));
}
return;
}
try { try {
byte[] b = s.getBytes(outputCharset); byte[] b = s.getBytes(outputCharset);
memoryStream.write(b); memoryStream.write(b);
@@ -168,10 +183,149 @@ public class PD3270 {
*/ */
public synchronized void formFeed() { public synchronized void formFeed() {
pageCount++; pageCount++;
writePrintChar('\f'); if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_PAGE_FEED)) {
writeControlCode(PrinterDefinitionTable.CMD_PAGE_FEED);
} else {
writePrintChar('\f');
}
flush(); flush();
} }
/**
* Send raw PDT control sequence if defined.
*/
public synchronized void writeControlCode(String commandName) {
if (pdt != null && commandName != null) {
byte[] seq = pdt.getControlCode(commandName);
if (seq != null && seq.length > 0) {
writePrintBytes(seq, 0, seq.length);
}
}
}
/**
* Initialize / start print job using PDT if available.
*/
public synchronized void startJob() {
if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_START_JOB)) {
writeControlCode(PrinterDefinitionTable.CMD_START_JOB);
}
}
/**
* End / finish print job using PDT if available.
*/
public synchronized void endJob() {
if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_END_JOB)) {
writeControlCode(PrinterDefinitionTable.CMD_END_JOB);
}
flush();
}
/**
* Toggle bold / emphasized printing.
*/
public synchronized void setBold(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_BOLD : PrinterDefinitionTable.CMD_END_BOLD);
}
/**
* Toggle underline printing.
*/
public synchronized void setUnderline(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_UNDERLINE : PrinterDefinitionTable.CMD_END_UNDERLINE);
}
/**
* Toggle italic printing.
*/
public synchronized void setItalic(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_ITALIC : PrinterDefinitionTable.CMD_END_ITALIC);
}
/**
* Toggle double-strike printing.
*/
public synchronized void setDoubleStrike(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_DOUBLE_STRIKE : PrinterDefinitionTable.CMD_END_DOUBLE_STRIKE);
}
/**
* Toggle double-width character expansion.
*/
public synchronized void setDoubleWidth(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_DOUBLE_WIDTH : PrinterDefinitionTable.CMD_END_DOUBLE_WIDTH);
}
/**
* Toggle subscript printing.
*/
public synchronized void setSubscript(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_SUBSCRIPT : PrinterDefinitionTable.CMD_END_SUBSCRIPT);
}
/**
* Toggle superscript printing.
*/
public synchronized void setSuperscript(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_SUPERSCRIPT : PrinterDefinitionTable.CMD_END_SUPERSCRIPT);
}
/**
* Set Characters Per Inch (CPI) via PDT.
*/
public synchronized void setCPI(int cpi) {
switch (cpi) {
case 10: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_10); break;
case 12: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_12); break;
case 15: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_15); break;
case 17: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_17); break;
default: break;
}
}
/**
* Set Lines Per Inch (LPI) via PDT.
*/
public synchronized void setLPI(int lpi) {
switch (lpi) {
case 6: writeControlCode(PrinterDefinitionTable.CMD_SET_LPI_6); break;
case 8: writeControlCode(PrinterDefinitionTable.CMD_SET_LPI_8); break;
default: break;
}
}
/**
* Process GDDM host print escape sequence or raw host passthrough.
*/
public synchronized void processGddmEscape(byte[] escapeSeq, int offset, int length) {
if (escapeSeq == null || length <= 0 || offset < 0 || offset + length > escapeSeq.length) {
return;
}
writePrintBytes(escapeSeq, offset, length);
}
/**
* High-level print string method.
*/
public synchronized void print(String text) {
writePrintString(text);
}
/**
* High-level print byte array method.
*/
public synchronized void print(byte[] data, int offset, int length) {
writePrintBytes(data, offset, length);
}
/**
* High-level print single character method.
*/
public synchronized void print(char c) {
writePrintChar(c);
}
/** /**
* Flush all buffered print data to target stream. * Flush all buffered print data to target stream.
*/ */
@@ -270,4 +424,12 @@ public class PD3270 {
this.outputCharset = charset; this.outputCharset = charset;
} }
} }
public PrinterDefinitionTable getPDT() {
return pdt;
}
public void setPDT(PrinterDefinitionTable pdt) {
this.pdt = pdt != null ? pdt : PrinterDefinitionTable.createPlainTextPDT();
}
} }
@@ -35,6 +35,10 @@ public class PrintPS3270 {
private int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL; private int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL;
// Auto-Flush Timer (Phase 7)
private java.util.Timer autoFlushTimer;
private final Object timerLock = new Object();
public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig(); this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config); this.pd = pd != null ? pd : new PD3270(this.config);
@@ -251,7 +255,48 @@ public class PrintPS3270 {
} }
if (startPrint) { if (startPrint) {
cancelAutoFlush();
flushPrintBuffer(); flushPrintBuffer();
} else {
scheduleAutoFlush();
}
}
public synchronized void processPrintComplete() {
cancelAutoFlush();
flushPrintBuffer();
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
pd.endJob();
}
public void scheduleAutoFlush() {
long timeout = config.getAutoFlushTimeoutMs();
if (timeout <= 0) return;
synchronized (timerLock) {
cancelAutoFlush();
autoFlushTimer = new java.util.Timer("PrintPS3270-AutoFlush", true);
autoFlushTimer.schedule(new java.util.TimerTask() {
@Override
public void run() {
synchronized (PrintPS3270.this) {
flushPrintBuffer();
}
}
}, timeout);
}
}
public void cancelAutoFlush() {
synchronized (timerLock) {
if (autoFlushTimer != null) {
autoFlushTimer.cancel();
autoFlushTimer = null;
}
} }
} }
@@ -81,6 +81,26 @@ public class PrintSCS3270 {
this.lineModified = false; this.lineModified = false;
} }
/**
* Process host data stream containing SCS orders and characters.
* @param data Byte array from host.
*/
public synchronized void processRecord(byte[] data) {
if (data != null) {
processRecord(data, 0, data.length);
}
}
/**
* Process host data stream containing SCS orders and characters.
* @param data Byte array from host.
* @param offset Starting offset.
* @param length Number of bytes to process.
*/
public synchronized void processRecord(byte[] data, int offset, int length) {
processHostData(data, offset, length);
}
/** /**
* Process host data stream containing SCS orders and characters. * Process host data stream containing SCS orders and characters.
* @param data Byte array from host. * @param data Byte array from host.
@@ -108,8 +128,6 @@ public class PrintSCS3270 {
int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0; int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0;
int orderTotalLen = 2 + (paramLen > 0 ? (paramLen + 1) : 1); // 0x2B + SubOrder + paramLen + payload int orderTotalLen = 2 + (paramLen > 0 ? (paramLen + 1) : 1); // 0x2B + SubOrder + paramLen + payload
// In standard SCS, paramLen byte specifies length of following parameters
// or paramLen is total length including length byte.
int bytesAvailable = end - idx; int bytesAvailable = end - idx;
int sliceLen = Math.min(orderTotalLen, bytesAvailable); int sliceLen = Math.min(orderTotalLen, bytesAvailable);
@@ -126,6 +144,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_STO: case PrinterConstants.SCS_STO:
processSetTextOrientation(data, idx, sliceLen); processSetTextOrientation(data, idx, sliceLen);
break; break;
case PrinterConstants.SCS_SCS:
processSelectCharacterSet(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SEAC: case PrinterConstants.SCS_SEAC:
processSetEnhancedAttribute(data, idx, sliceLen); processSetEnhancedAttribute(data, idx, sliceLen);
break; break;
@@ -135,6 +156,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_PPV: case PrinterConstants.SCS_PPV:
processPresentationPositionVertical(data, idx, sliceLen); processPresentationPositionVertical(data, idx, sliceLen);
break; break;
case PrinterConstants.SCS_GEA:
processSetGraphicErrorAction(data, idx, sliceLen);
break;
default: default:
log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder)); log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder));
break; break;
@@ -154,12 +178,12 @@ public class PrintSCS3270 {
continue; continue;
} }
// Check TRS (Transparent Stream 0x35) // Check TRN / TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRS) { if (b == PrinterConstants.SCS_TRN) {
if (idx + 1 < end) { if (idx + 1 < end) {
int trsLen = data[idx + 1] & 0xFF; int trsLen = data[idx + 1] & 0xFF;
int actualTrs = Math.min(trsLen, end - (idx + 2)); int actualTrs = Math.min(trsLen, end - (idx + 2));
processTransparentStream(data, idx + 2, actualTrs); processTRN(data, idx + 2, actualTrs);
idx += 2 + actualTrs; idx += 2 + actualTrs;
} else { } else {
idx = end; idx = end;
@@ -167,52 +191,83 @@ public class PrintSCS3270 {
continue; continue;
} }
// Check Single-Byte SCS Controls // Check Single-Byte SCS Controls (Full 28-command set)
switch (b) { switch (b) {
case PrinterConstants.SCS_NUL: case PrinterConstants.SCS_NUL:
// Null - ignored case PrinterConstants.SCS_NOP:
// Null / No-op - ignored
idx++; idx++;
break; break;
case PrinterConstants.SCS_VCS:
if (idx + 1 < end) {
int chan = data[idx + 1] & 0xFF;
processVCS(chan);
idx += 2;
} else {
idx++;
}
break;
case PrinterConstants.SCS_CR: case PrinterConstants.SCS_CR:
carriageReturn(); case PrinterConstants.SCS_RCR:
processCR();
idx++; idx++;
break; break;
case PrinterConstants.SCS_LF: case PrinterConstants.SCS_LF:
lineFeed(); processLF();
idx++; idx++;
break; break;
case PrinterConstants.SCS_NL: case PrinterConstants.SCS_NL:
case PrinterConstants.SCS_RNLS: case PrinterConstants.SCS_RNLS:
newLine(); processNL();
idx++; idx++;
break; break;
case PrinterConstants.SCS_FF: case PrinterConstants.SCS_FF:
formFeed(); processFF();
idx++; idx++;
break; break;
case PrinterConstants.SCS_BS: case PrinterConstants.SCS_BS:
case PrinterConstants.SCS_NBS: case PrinterConstants.SCS_NBS:
backspace(); processBS();
idx++; idx++;
break; break;
case PrinterConstants.SCS_HT: case PrinterConstants.SCS_HT:
processHorizontalTab(); processHT();
idx++; idx++;
break; break;
case PrinterConstants.SCS_VT: case PrinterConstants.SCS_VT:
processVerticalTab(); processVT();
idx++;
break;
case PrinterConstants.SCS_SO:
processSO();
idx++;
break;
case PrinterConstants.SCS_SI:
processSI();
idx++; idx++;
break; break;
case PrinterConstants.SCS_ENP: case PrinterConstants.SCS_ENP:
presentationEnabled = true; processENP();
idx++; idx++;
break; break;
case PrinterConstants.SCS_INP: case PrinterConstants.SCS_INP:
presentationEnabled = false; processINP();
idx++;
break;
case PrinterConstants.SCS_POC:
processPOC(null);
idx++; idx++;
break; break;
case PrinterConstants.SCS_BEL: case PrinterConstants.SCS_BEL:
// Sound alarm processBEL();
idx++;
break;
case PrinterConstants.SCS_IRS:
processIRS();
idx++;
break;
case PrinterConstants.SCS_SUB:
processSUB();
idx++; idx++;
break; break;
case PrinterConstants.SCS_GE: case PrinterConstants.SCS_GE:
@@ -233,7 +288,17 @@ public class PrintSCS3270 {
// Standard printable character // Standard printable character
if (presentationEnabled) { if (presentationEnabled) {
char ch = translator.ebcdicToUnicode(b); char ch = translator.ebcdicToUnicode(b);
printCharacter(ch); if (ch == 0 || ch == '\uFFFD') {
int gea = config.getGraphicErrorAction();
if (gea == PrinterConstants.GEA_SUBSTITUTE_SPECIFIED) {
ch = config.getGraphicErrorReplacementChar();
printCharacter(ch);
} else if (gea != PrinterConstants.GEA_INHIBIT_INVALID) {
printCharacter(ch != 0 ? ch : '?');
}
} else {
printCharacter(ch);
}
} }
idx++; idx++;
break; break;
@@ -293,10 +358,22 @@ public class PrintSCS3270 {
// ========== SCS Order Implementations ========== // ========== SCS Order Implementations ==========
public synchronized void carriageReturn() { public synchronized void carriageReturn() {
processCR();
}
public synchronized void processCR() {
currentCol = leftMargin; currentCol = leftMargin;
} }
public synchronized void processRCR() {
processCR();
}
public synchronized void lineFeed() { public synchronized void lineFeed() {
processLF();
}
public synchronized void processLF() {
flushLineBuffer(); flushLineBuffer();
currentRow++; currentRow++;
if (currentRow > bottomMargin || currentRow > mpl) { if (currentRow > bottomMargin || currentRow > mpl) {
@@ -305,11 +382,23 @@ public class PrintSCS3270 {
} }
public synchronized void newLine() { public synchronized void newLine() {
carriageReturn(); processNL();
lineFeed(); }
public synchronized void processNL() {
processCR();
processLF();
}
public synchronized void processRNL() {
processNL();
} }
public synchronized void formFeed() { public synchronized void formFeed() {
processFF();
}
public synchronized void processFF() {
flushLineBuffer(); flushLineBuffer();
pd.formFeed(); pd.formFeed();
currentRow = topMargin; currentRow = topMargin;
@@ -317,18 +406,139 @@ public class PrintSCS3270 {
} }
public synchronized void backspace() { public synchronized void backspace() {
processBS();
}
public synchronized void processBS() {
if (currentCol > leftMargin) { if (currentCol > leftMargin) {
currentCol -= (doubleWidth ? 2 : 1); currentCol -= (doubleWidth ? 2 : 1);
if (currentCol < leftMargin) currentCol = leftMargin; if (currentCol < leftMargin) currentCol = leftMargin;
} }
} }
public synchronized void processNBS() {
processBS();
}
public synchronized void processIRS() {
// SCS Index Return (0x33): Advance one line down and return to left margin
processNL();
}
public synchronized void processVCS(int channel) {
int targetLine = config.getChannelLine(channel);
if (targetLine > 0) {
if (targetLine < currentRow) {
processFF();
}
while (currentRow < targetLine && currentRow < bottomMargin) {
processLF();
}
}
}
public synchronized void processTRN(byte[] rawBytes) {
if (rawBytes != null && rawBytes.length > 0) {
processTRN(rawBytes, 0, rawBytes.length);
}
}
public synchronized void processTRN(byte[] data, int offset, int len) {
if (data != null && len > 0) {
pd.writePrintBytes(data, offset, len);
}
}
public synchronized void processTransparentStream(byte[] data, int offset, int len) {
processTRN(data, offset, len);
}
public synchronized void processGEA(int action) {
this.config.setGraphicErrorAction(action);
}
public synchronized void processGEA(int action, int replacementChar) {
this.config.setGraphicErrorAction(action);
if (replacementChar > 0) {
this.config.setGraphicErrorReplacementChar((char) replacementChar);
}
}
public synchronized void processSO() {
// Shift-Out (DBCS mode toggle)
}
public synchronized void processSI() {
// Shift-In (SBCS mode toggle)
}
public synchronized void processENP() {
presentationEnabled = true;
}
public synchronized void processINP() {
presentationEnabled = false;
}
public synchronized void processBEL() {
log.fine("SCS BEL (Sound Alarm)");
}
public synchronized void processSUB() {
int gea = config.getGraphicErrorAction();
if (gea == PrinterConstants.GEA_SUBSTITUTE_SPECIFIED) {
printCharacter(config.getGraphicErrorReplacementChar());
} else if (gea != PrinterConstants.GEA_INHIBIT_INVALID) {
printCharacter('?');
}
}
public synchronized void processPOC(byte[] msg) {
log.fine("SCS Program Operator Communication");
}
public synchronized void processSCS(int charset) {
log.fine("SCS Select Character Set: " + charset);
}
public synchronized void processHT() {
processHorizontalTab();
}
public synchronized void processVT() {
processVerticalTab();
}
public synchronized void processPP(byte[] data, int offset, int len) {
processPresentationPositionAdvancing(data, offset, len);
}
public synchronized void processSHF(byte[] data, int offset, int len) {
processSetHorizontalFormat(data, offset, len);
}
public synchronized void processSVF(byte[] data, int offset, int len) {
processSetVerticalFormat(data, offset, len);
}
public synchronized void processSLD(byte[] data, int offset, int len) {
processSetLineDensity(data, offset, len);
}
public synchronized void processSTO(byte[] data, int offset, int len) {
processSetTextOrientation(data, offset, len);
}
public synchronized void processSA(byte[] data, int offset, int len) {
processSetAttribute(data, offset, len);
}
public synchronized void advanceToNextLine() { public synchronized void advanceToNextLine() {
newLine(); processNL();
} }
public synchronized void advanceToNextPage() { public synchronized void advanceToNextPage() {
formFeed(); processFF();
} }
// ========== Tab Stops and Calculations ========== // ========== Tab Stops and Calculations ==========
@@ -370,7 +580,7 @@ public class PrintSCS3270 {
int nextTab = calculateVerticalTab(currentRow); int nextTab = calculateVerticalTab(currentRow);
if (nextTab > 0 && nextTab <= bottomMargin) { if (nextTab > 0 && nextTab <= bottomMargin) {
while (currentRow < nextTab) { while (currentRow < nextTab) {
lineFeed(); processLF();
} }
} else { } else {
advanceToNextPage(); advanceToNextPage();
@@ -414,20 +624,59 @@ public class PrintSCS3270 {
} }
public synchronized void setPrintDensity(int cpi, int lpi) { public synchronized void setPrintDensity(int cpi, int lpi) {
if (cpi > 0) this.cpi = cpi; if (cpi > 0) {
if (lpi > 0) this.lpi = lpi; this.cpi = cpi;
if (pd != null) pd.setCPI(cpi);
}
if (lpi > 0) {
this.lpi = lpi;
if (pd != null) pd.setLPI(lpi);
}
} }
public synchronized void setEnhancedHighlight(int highlightType) { public synchronized void setEnhancedHighlight(int highlightType) {
this.activeHighlight = highlightType; this.activeHighlight = highlightType;
if (pd != null) {
switch (highlightType) {
case PrinterConstants.SEAC_EMPHASIZED:
pd.setBold(true);
break;
case PrinterConstants.SEAC_ITALIC:
pd.setItalic(true);
break;
case PrinterConstants.SEAC_UNDERLINE:
pd.setUnderline(true);
break;
case PrinterConstants.SEAC_DOUBLE_STRIKE:
pd.setDoubleStrike(true);
break;
case PrinterConstants.SEAC_SUPERSCRIPT:
pd.setSuperscript(true);
break;
case PrinterConstants.SEAC_SUBSCRIPT:
pd.setSubscript(true);
break;
case PrinterConstants.SEAC_DEFAULT:
default:
pd.setBold(false);
pd.setItalic(false);
pd.setUnderline(false);
pd.setDoubleStrike(false);
pd.setSubscript(false);
pd.setSuperscript(false);
break;
}
}
} }
public synchronized void startDoubleWidthCharacters() { public synchronized void startDoubleWidthCharacters() {
this.doubleWidth = true; this.doubleWidth = true;
if (pd != null) pd.setDoubleWidth(true);
} }
public synchronized void endDoubleWidthCharacters() { public synchronized void endDoubleWidthCharacters() {
this.doubleWidth = false; this.doubleWidth = false;
if (pd != null) pd.setDoubleWidth(false);
} }
public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) { public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) {
@@ -484,7 +733,8 @@ public class PrintSCS3270 {
if (points > 0) { if (points > 0) {
// Line density in points / inch (72 points = 1 inch) // Line density in points / inch (72 points = 1 inch)
// 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI // 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI
this.lpi = Math.max(1, 72 / points); int computedLpi = Math.max(1, 72 / points);
setPrintDensity(this.cpi, computedLpi);
} }
} }
@@ -494,6 +744,21 @@ public class PrintSCS3270 {
} }
} }
public synchronized void processSelectCharacterSet(byte[] data, int offset, int len) {
if (len >= 4) {
int cs = data[offset + 3] & 0xFF;
processSCS(cs);
}
}
public synchronized void processSetGraphicErrorAction(byte[] data, int offset, int len) {
if (len >= 4) {
int action = data[offset + 3] & 0xFF;
int repChar = (len >= 5) ? (data[offset + 4] & 0xFF) : 0;
processGEA(action, repChar);
}
}
public synchronized void processSetEnhancedAttribute(byte[] data, int offset, int len) { public synchronized void processSetEnhancedAttribute(byte[] data, int offset, int len) {
if (len >= 5) { if (len >= 5) {
int attrVal = data[offset + 4] & 0xFF; int attrVal = data[offset + 4] & 0xFF;
@@ -520,11 +785,11 @@ public class PrintSCS3270 {
if (subfn == PrinterConstants.POS_ABSOLUTE) { if (subfn == PrinterConstants.POS_ABSOLUTE) {
while (currentRow < val && currentRow < bottomMargin) { while (currentRow < val && currentRow < bottomMargin) {
lineFeed(); processLF();
} }
} else if (subfn == PrinterConstants.POS_RELATIVE) { } else if (subfn == PrinterConstants.POS_RELATIVE) {
for (int i = 0; i < val && currentRow < bottomMargin; i++) { for (int i = 0; i < val && currentRow < bottomMargin; i++) {
lineFeed(); processLF();
} }
} }
} }
@@ -536,19 +801,23 @@ public class PrintSCS3270 {
if (attrType == PrinterConstants.SA_COLOR) { if (attrType == PrinterConstants.SA_COLOR) {
this.activeColor = attrVal; this.activeColor = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE) { } else if (attrType == PrinterConstants.SA_HILITE || attrType == PrinterConstants.SA_EXT_HILITE) {
this.activeHighlight = attrVal; setEnhancedHighlight(attrVal);
} else if (attrType == PrinterConstants.SA_CHARSET) {
processSCS(attrVal);
} else if (attrType == PrinterConstants.SA_RESET) { } else if (attrType == PrinterConstants.SA_RESET) {
this.activeColor = 0; this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT; setEnhancedHighlight(PrinterConstants.SEAC_DEFAULT);
this.doubleWidth = false; endDoubleWidthCharacters();
} }
} }
public synchronized void processTransparentStream(byte[] data, int offset, int len) { public int getChannelLine(int channel) {
if (len > 0) { return config.getChannelLine(channel);
pd.writePrintBytes(data, offset, len); }
}
public void setChannelLine(int channel, int line) {
config.setChannelLine(channel, line);
} }
// ========== Accessors ========== // ========== Accessors ==========
@@ -571,13 +840,16 @@ public class PrintSCS3270 {
public int getCharsPerInch() { return cpi; } public int getCharsPerInch() { return cpi; }
public boolean isDoubleWidth() { return doubleWidth; } public boolean isDoubleWidth() { return doubleWidth; }
public void setDoubleWidth(boolean dw) { this.doubleWidth = dw; } public void setDoubleWidth(boolean dw) {
if (dw) startDoubleWidthCharacters();
else endDoubleWidthCharacters();
}
public int getActiveColor() { return activeColor; } public int getActiveColor() { return activeColor; }
public void setActiveColor(int color) { this.activeColor = color; } public void setActiveColor(int color) { this.activeColor = color; }
public int getActiveHighlight() { return activeHighlight; } public int getActiveHighlight() { return activeHighlight; }
public void setActiveHighlight(int hilite) { this.activeHighlight = hilite; } public void setActiveHighlight(int hilite) { setEnhancedHighlight(hilite); }
public int getTextOrientation() { return textOrientation; } public int getTextOrientation() { return textOrientation; }
@@ -48,26 +48,44 @@ public class PrinterConfig {
private boolean autoFlushOnEoj = true; private boolean autoFlushOnEoj = true;
private boolean autoReconnect = false; private boolean autoReconnect = false;
public PrinterConfig() {} // Phase 7 Enhancements: PDT, Auto-Flush Timer, Channel Tapes, GEA
private PrinterDefinitionTable printerDefinitionTable = PrinterDefinitionTable.createPlainTextPDT();
private final int[] channelTable = new int[13]; // 1-based index (channels 1..12)
private long autoFlushTimeoutMs = 2000;
private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION;
private char graphicErrorReplacementChar = '?';
public PrinterConfig() {
initDefaultChannelTable();
}
public PrinterConfig(String host, int port) { public PrinterConfig(String host, int port) {
this();
this.host = host; this.host = host;
this.port = port; this.port = port;
} }
public PrinterConfig(String host, int port, String printerLuName) { public PrinterConfig(String host, int port, String printerLuName) {
this.host = host; this(host, port);
this.port = port;
this.printerLuName = printerLuName; this.printerLuName = printerLuName;
} }
public PrinterConfig(String host, int port, String printerLuName, boolean useTls) { public PrinterConfig(String host, int port, String printerLuName, boolean useTls) {
this.host = host; this(host, port, printerLuName);
this.port = port;
this.printerLuName = printerLuName;
this.useTls = useTls; this.useTls = useTls;
} }
private void initDefaultChannelTable() {
// Standard 12-channel carriage tape default distribution
// Channel 1 = Top Margin (1), Channel 12 = Bottom Margin (66)
channelTable[1] = 1;
int step = Math.max(1, (mpl - topMargin) / 11);
for (int c = 2; c <= 11; c++) {
channelTable[c] = Math.min(mpl, topMargin + (c - 1) * step);
}
channelTable[12] = mpl;
}
// Getters and Setters // Getters and Setters
public String getHost() { return host; } public String getHost() { return host; }
public void setHost(String host) { this.host = host; } public void setHost(String host) { this.host = host; }
@@ -125,6 +143,7 @@ public class PrinterConfig {
public void setMpl(int mpl) { public void setMpl(int mpl) {
this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL; this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl; if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl;
initDefaultChannelTable();
} }
public int getLeftMargin() { return leftMargin; } public int getLeftMargin() { return leftMargin; }
@@ -159,4 +178,53 @@ public class PrinterConfig {
public boolean isAutoReconnect() { return autoReconnect; } public boolean isAutoReconnect() { return autoReconnect; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; } public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
public PrinterDefinitionTable getPrinterDefinitionTable() {
return printerDefinitionTable;
}
public void setPrinterDefinitionTable(PrinterDefinitionTable printerDefinitionTable) {
this.printerDefinitionTable = printerDefinitionTable != null ? printerDefinitionTable : PrinterDefinitionTable.createPlainTextPDT();
}
public long getAutoFlushTimeoutMs() {
return autoFlushTimeoutMs;
}
public void setAutoFlushTimeoutMs(long autoFlushTimeoutMs) {
this.autoFlushTimeoutMs = autoFlushTimeoutMs;
}
public int getGraphicErrorAction() {
return graphicErrorAction;
}
public void setGraphicErrorAction(int graphicErrorAction) {
this.graphicErrorAction = graphicErrorAction;
}
public char getGraphicErrorReplacementChar() {
return graphicErrorReplacementChar;
}
public void setGraphicErrorReplacementChar(char graphicErrorReplacementChar) {
this.graphicErrorReplacementChar = graphicErrorReplacementChar;
}
public int getChannelLine(int channel) {
if (channel >= 1 && channel <= 12) {
return channelTable[channel];
}
return 1;
}
public void setChannelLine(int channel, int line) {
if (channel >= 1 && channel <= 12) {
channelTable[channel] = Math.max(1, Math.min(mpl, line));
}
}
public int[] getChannelTable() {
return channelTable.clone();
}
} }
@@ -22,13 +22,18 @@ public final class PrinterConstants {
// ========== SCS Single-Byte Control Codes (EBCDIC) ========== // ========== SCS Single-Byte Control Codes (EBCDIC) ==========
public static final int SCS_NUL = 0x00; // Null public static final int SCS_NUL = 0x00; // Null
public static final int SCS_NOP = 0x03; // No Operation / NB
public static final int SCS_VCS = 0x04; // Vertical Channel Select (0x04 <chan>)
public static final int SCS_HT = 0x05; // Horizontal Tab public static final int SCS_HT = 0x05; // Horizontal Tab
public static final int SCS_RNLS = 0x06; // Required New Line public static final int SCS_RNLS = 0x06; // Required New Line
public static final int SCS_RNL = 0x06; // Required New Line alias
public static final int SCS_RCR = 0x07; // Required Carriage Return public static final int SCS_RCR = 0x07; // Required Carriage Return
public static final int SCS_GE = 0x08; // Graphic Escape public static final int SCS_GE = 0x08; // Graphic Escape
public static final int SCS_VT = 0x0B; // Vertical Tab public static final int SCS_VT = 0x0B; // Vertical Tab
public static final int SCS_FF = 0x0C; // Form Feed public static final int SCS_FF = 0x0C; // Form Feed
public static final int SCS_CR = 0x0D; // Carriage Return public static final int SCS_CR = 0x0D; // Carriage Return
public static final int SCS_SO = 0x0E; // Shift Out (DBCS mode)
public static final int SCS_SI = 0x0F; // Shift In (SBCS mode)
public static final int SCS_ENP = 0x14; // Enable Presentation public static final int SCS_ENP = 0x14; // Enable Presentation
public static final int SCS_NL = 0x15; // New Line public static final int SCS_NL = 0x15; // New Line
public static final int SCS_BS = 0x16; // Backspace public static final int SCS_BS = 0x16; // Backspace
@@ -36,8 +41,11 @@ public final class PrinterConstants {
public static final int SCS_INP = 0x24; // Inhibit Presentation public static final int SCS_INP = 0x24; // Inhibit Presentation
public static final int SCS_LF = 0x25; // Line Feed public static final int SCS_LF = 0x25; // Line Feed
public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm
public static final int SCS_TRS = 0x35; // Transparent Stream (0x35 <len> <bytes>) public static final int SCS_IRS = 0x33; // Index Return
public static final int SCS_TRN = 0x35; // Transparent Stream (0x35 <len> <bytes>)
public static final int SCS_TRS = 0x35; // Transparent Stream alias
public static final int SCS_NBS = 0x36; // Numeric Backspace public static final int SCS_NBS = 0x36; // Numeric Backspace
public static final int SCS_SUB = 0x3F; // Substitute Character
public static final int SCS_SP = 0x40; // Space public static final int SCS_SP = 0x40; // Space
public static final int SCS_RSP = 0x41; // Required Space public static final int SCS_RSP = 0x41; // Required Space
@@ -46,26 +54,39 @@ public final class PrinterConstants {
public static final int SCS_SA = 0x28; // Set Attribute (0x28 <type> <val>) public static final int SCS_SA = 0x28; // Set Attribute (0x28 <type> <val>)
// 0x2B Sub-orders // 0x2B Sub-orders
public static final int SCS_PPV = 0xC4; // Presentation Position Vertical
public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal)
public static final int SCS_GEA = 0xC8; // Set Graphic Error Action (0x2B 0xC8)
public static final int SCS_SHF = 0xD1; // Set Horizontal Format public static final int SCS_SHF = 0xD1; // Set Horizontal Format
public static final int SCS_SVF = 0xD2; // Set Vertical Format public static final int SCS_SVF = 0xD2; // Set Vertical Format
public static final int SCS_STO = 0xD3; // Set Text Orientation public static final int SCS_STO = 0xD3; // Set Text Orientation
public static final int SCS_SCS = 0xD4; // Select Character Set public static final int SCS_SCS = 0xD4; // Select Character Set
public static final int SCS_SEAC = 0xD5; // Set Enhanced Attribute / Highlight public static final int SCS_SEAC = 0xD5; // Set Enhanced Attribute / Highlight
public static final int SCS_SLD = 0xD6; // Set Line Density public static final int SCS_SLD = 0xD6; // Set Line Density
public static final int SCS_PPV = 0xC4; // Presentation Position Vertical
public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal)
// SA Attribute Types // SA Attribute Types
public static final int SA_RESET = 0x00; public static final int SA_RESET = 0x00;
public static final int SA_HILITE = 0x41; public static final int SA_HILITE = 0x41;
public static final int SA_COLOR = 0x42; public static final int SA_COLOR = 0x42;
public static final int SA_CHARSET = 0x43; public static final int SA_CHARSET = 0x43;
public static final int SA_EXT_HILITE = 0x45;
// SEAC Highlight Values // SEAC / Extended Highlight Values
public static final int SEAC_DEFAULT = 0x00; public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_BLINK = 0xF1; public static final int SEAC_NORMAL = 0x00;
public static final int SEAC_REVERSE = 0xF2; public static final int SEAC_BLINK = 0xF1;
public static final int SEAC_UNDERLINE = 0xF4; public static final int SEAC_REVERSE = 0xF2;
public static final int SEAC_UNDERLINE = 0xF4;
public static final int SEAC_ITALIC = 0xF8;
public static final int SEAC_EMPHASIZED = 0xF9; // Bold / Emphasized
public static final int SEAC_DOUBLE_STRIKE = 0xFA;
public static final int SEAC_SUPERSCRIPT = 0xFB;
public static final int SEAC_SUBSCRIPT = 0xFC;
// Set Graphic Error Action (GEA) Constants
public static final int GEA_NO_SUBSTITUTION = 0x00; // Stop on error or use default
public static final int GEA_SUBSTITUTE_SPECIFIED = 0x01; // Substitute specified character
public static final int GEA_INHIBIT_INVALID = 0x02; // Inhibit printing invalid character
// PPA/PPV Positioning Types // PPA/PPV Positioning Types
public static final int POS_ABSOLUTE = 0x01; // Absolute position (1-based) public static final int POS_ABSOLUTE = 0x01; // Absolute position (1-based)
@@ -0,0 +1,237 @@
package haus.nightmare.lib3270j.printer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* Printer Definition Table (PDT) Processor.
* Conforms to IBM Host On-Demand v14 (com.ibm.eNetwork.ECL.tn3270p.PD3270 PDT architecture).
*
* Provides translation of SCS / LU3 formatting and character commands into target
* printer control codes (e.g. PCL5, Epson ESC/P, PostScript, or Plain Text).
*/
public class PrinterDefinitionTable {
// Control sequence keys
public static final String CMD_START_JOB = "START_JOB";
public static final String CMD_END_JOB = "END_JOB";
public static final String CMD_PAGE_FEED = "PAGE_FEED";
public static final String CMD_CARRIAGE_RETURN = "CARRIAGE_RETURN";
public static final String CMD_LINE_FEED = "LINE_FEED";
public static final String CMD_NEW_LINE = "NEW_LINE";
public static final String CMD_RESET = "RESET";
// Highlights
public static final String CMD_START_BOLD = "START_BOLD";
public static final String CMD_END_BOLD = "END_BOLD";
public static final String CMD_START_UNDERLINE = "START_UNDERLINE";
public static final String CMD_END_UNDERLINE = "END_UNDERLINE";
public static final String CMD_START_ITALIC = "START_ITALIC";
public static final String CMD_END_ITALIC = "END_ITALIC";
public static final String CMD_START_DOUBLE_STRIKE = "START_DOUBLE_STRIKE";
public static final String CMD_END_DOUBLE_STRIKE = "END_DOUBLE_STRIKE";
public static final String CMD_START_DOUBLE_WIDTH = "START_DOUBLE_WIDTH";
public static final String CMD_END_DOUBLE_WIDTH = "END_DOUBLE_WIDTH";
public static final String CMD_START_SUBSCRIPT = "START_SUBSCRIPT";
public static final String CMD_END_SUBSCRIPT = "END_SUBSCRIPT";
public static final String CMD_START_SUPERSCRIPT = "START_SUPERSCRIPT";
public static final String CMD_END_SUPERSCRIPT = "END_SUPERSCRIPT";
// Densities
public static final String CMD_SET_CPI_10 = "SET_CPI_10";
public static final String CMD_SET_CPI_12 = "SET_CPI_12";
public static final String CMD_SET_CPI_15 = "SET_CPI_15";
public static final String CMD_SET_CPI_17 = "SET_CPI_17";
public static final String CMD_SET_LPI_6 = "SET_LPI_6";
public static final String CMD_SET_LPI_8 = "SET_LPI_8";
// Colors
public static final String CMD_COLOR_BLACK = "COLOR_BLACK";
public static final String CMD_COLOR_BLUE = "COLOR_BLUE";
public static final String CMD_COLOR_RED = "COLOR_RED";
public static final String CMD_COLOR_PINK = "COLOR_PINK";
public static final String CMD_COLOR_GREEN = "COLOR_GREEN";
public static final String CMD_COLOR_TURQUOISE = "COLOR_TURQUOISE";
public static final String CMD_COLOR_YELLOW = "COLOR_YELLOW";
public static final String CMD_COLOR_WHITE = "COLOR_WHITE";
private final String name;
private final String description;
private final Map<String, byte[]> controlCodes = new HashMap<>();
private final Map<Character, byte[]> charOverrides = new HashMap<>();
private Charset defaultCharset = StandardCharsets.UTF_8;
public PrinterDefinitionTable(String name, String description) {
this.name = name != null ? name : "GENERIC_TEXT";
this.description = description != null ? description : "Generic Plain Text Printer Table";
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public Charset getDefaultCharset() {
return defaultCharset;
}
public void setDefaultCharset(Charset defaultCharset) {
if (defaultCharset != null) {
this.defaultCharset = defaultCharset;
}
}
public void setControlCode(String commandName, byte[] sequence) {
if (commandName != null && sequence != null) {
controlCodes.put(commandName, sequence.clone());
}
}
public void setControlCode(String commandName, String escapeSequence) {
if (commandName != null && escapeSequence != null) {
controlCodes.put(commandName, escapeSequence.getBytes(StandardCharsets.ISO_8859_1));
}
}
public byte[] getControlCode(String commandName) {
return controlCodes.get(commandName);
}
public boolean hasControlCode(String commandName) {
return controlCodes.containsKey(commandName);
}
public void setCharOverride(char c, byte[] sequence) {
if (sequence != null) {
charOverrides.put(c, sequence.clone());
}
}
public byte[] getCharOverride(char c) {
return charOverrides.get(c);
}
public byte[] translateChar(char c) {
byte[] override = charOverrides.get(c);
if (override != null) {
return override;
}
return String.valueOf(c).getBytes(defaultCharset);
}
// ==========================================
// Built-in Factory Presets
// ==========================================
/**
* Plain Text / Generic ASCII output definition.
*/
public static PrinterDefinitionTable createPlainTextPDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("PLAIN_TEXT", "Standard ASCII / Plain Text");
pdt.setControlCode(CMD_PAGE_FEED, "\f");
pdt.setControlCode(CMD_CARRIAGE_RETURN, "\r");
pdt.setControlCode(CMD_LINE_FEED, "\n");
pdt.setControlCode(CMD_NEW_LINE, "\r\n");
return pdt;
}
/**
* HP PCL 5 / PCL 6 Printer Definition Table.
*/
public static PrinterDefinitionTable createPcl5PDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("PCL_5", "Hewlett-Packard PCL 5 / PCL 6");
pdt.setDefaultCharset(StandardCharsets.ISO_8859_1);
// Control codes (ESC is \033)
pdt.setControlCode(CMD_START_JOB, "\033E"); // Reset / Initialize
pdt.setControlCode(CMD_END_JOB, "\033E"); // Reset
pdt.setControlCode(CMD_RESET, "\033E");
pdt.setControlCode(CMD_PAGE_FEED, "\f");
pdt.setControlCode(CMD_CARRIAGE_RETURN, "\r");
pdt.setControlCode(CMD_LINE_FEED, "\n");
pdt.setControlCode(CMD_NEW_LINE, "\r\n");
// Highlights
pdt.setControlCode(CMD_START_BOLD, "\033(s3B"); // Bold
pdt.setControlCode(CMD_END_BOLD, "\033(s0B"); // Normal stroke weight
pdt.setControlCode(CMD_START_UNDERLINE, "\033&d0D"); // Underline
pdt.setControlCode(CMD_END_UNDERLINE, "\033&d@"); // Underline off
pdt.setControlCode(CMD_START_ITALIC, "\033(s1S"); // Italic posture
pdt.setControlCode(CMD_END_ITALIC, "\033(s0S"); // Upright posture
pdt.setControlCode(CMD_START_DOUBLE_STRIKE, "\033(s3B");
pdt.setControlCode(CMD_END_DOUBLE_STRIKE, "\033(s0B");
pdt.setControlCode(CMD_START_DOUBLE_WIDTH, "\033(s0S\033&k1W");
pdt.setControlCode(CMD_END_DOUBLE_WIDTH, "\033&k0W");
// Pitch & Spacing
pdt.setControlCode(CMD_SET_CPI_10, "\033&k0S\033(s10H"); // 10 CPI
pdt.setControlCode(CMD_SET_CPI_12, "\033&k2S\033(s12H"); // 12 CPI
pdt.setControlCode(CMD_SET_CPI_15, "\033(s15H"); // 15 CPI
pdt.setControlCode(CMD_SET_CPI_17, "\033(s17.14H"); // 17 CPI
pdt.setControlCode(CMD_SET_LPI_6, "\033&l6D"); // 6 LPI
pdt.setControlCode(CMD_SET_LPI_8, "\033&l8D"); // 8 LPI
return pdt;
}
/**
* Epson ESC/P and ESC/P 2 Printer Definition Table.
*/
public static PrinterDefinitionTable createEpsonEscPPDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("EPSON_ESC_P", "Epson ESC/P & ESC/P 2");
pdt.setDefaultCharset(StandardCharsets.ISO_8859_1);
pdt.setControlCode(CMD_START_JOB, "\033@"); // ESC @ Initialize
pdt.setControlCode(CMD_END_JOB, "\033@");
pdt.setControlCode(CMD_RESET, "\033@");
pdt.setControlCode(CMD_PAGE_FEED, "\f");
pdt.setControlCode(CMD_CARRIAGE_RETURN, "\r");
pdt.setControlCode(CMD_LINE_FEED, "\n");
pdt.setControlCode(CMD_NEW_LINE, "\r\n");
// Highlights
pdt.setControlCode(CMD_START_BOLD, "\033E"); // ESC E (Emphasized/Bold on)
pdt.setControlCode(CMD_END_BOLD, "\033F"); // ESC F (Emphasized/Bold off)
pdt.setControlCode(CMD_START_UNDERLINE, "\033-1"); // ESC - 1 (Underline on)
pdt.setControlCode(CMD_END_UNDERLINE, "\033-0"); // ESC - 0 (Underline off)
pdt.setControlCode(CMD_START_ITALIC, "\0334"); // ESC 4 (Italic on)
pdt.setControlCode(CMD_END_ITALIC, "\0335"); // ESC 5 (Italic off)
pdt.setControlCode(CMD_START_DOUBLE_STRIKE, "\033G"); // ESC G (Double-strike on)
pdt.setControlCode(CMD_END_DOUBLE_STRIKE, "\033H"); // ESC H (Double-strike off)
pdt.setControlCode(CMD_START_DOUBLE_WIDTH, "\033W1"); // ESC W 1
pdt.setControlCode(CMD_END_DOUBLE_WIDTH, "\033W0"); // ESC W 0
pdt.setControlCode(CMD_START_SUPERSCRIPT, "\033S0"); // ESC S 0
pdt.setControlCode(CMD_END_SUPERSCRIPT, "\033T"); // ESC T
pdt.setControlCode(CMD_START_SUBSCRIPT, "\033S1"); // ESC S 1
pdt.setControlCode(CMD_END_SUBSCRIPT, "\033T"); // ESC T
// Pitch & Spacing
pdt.setControlCode(CMD_SET_CPI_10, "\033P"); // 10 CPI (Pica)
pdt.setControlCode(CMD_SET_CPI_12, "\033M"); // 12 CPI (Elite)
pdt.setControlCode(CMD_SET_CPI_15, "\033g"); // 15 CPI
pdt.setControlCode(CMD_SET_LPI_6, "\0332"); // 6 LPI (1/6 inch)
pdt.setControlCode(CMD_SET_LPI_8, "\0330"); // 8 LPI (1/8 inch)
return pdt;
}
/**
* Adobe PostScript Level 2/3 Printer Definition Table.
*/
public static PrinterDefinitionTable createPostScriptPDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("POSTSCRIPT", "Adobe PostScript Level 2/3");
pdt.setControlCode(CMD_START_JOB, "%!PS-Adobe-3.0\n/Courier findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_END_JOB, "showpage\n%%EOF\n");
pdt.setControlCode(CMD_PAGE_FEED, "showpage\n");
pdt.setControlCode(CMD_START_BOLD, "/Courier-Bold findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_END_BOLD, "/Courier findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_START_ITALIC, "/Courier-Oblique findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_END_ITALIC, "/Courier findfont 10 scalefont setfont\n");
return pdt;
}
}
@@ -166,6 +166,10 @@ public class Telnet3270EP implements Runnable {
readerThread = null; readerThread = null;
} }
if (printPs != null) {
printPs.cancelAutoFlush();
}
pd.closePrinter(); pd.closePrinter();
updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected"); updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected");
} }
@@ -260,11 +264,17 @@ public class Telnet3270EP implements Runnable {
*/ */
public synchronized void sendEOJ(boolean isComplete) { public synchronized void sendEOJ(boolean isComplete) {
log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete); log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete);
if (config.isFormFeedAtEoj()) { if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
pd.formFeed(); printPs.processPrintComplete();
} } else {
if (config.isAutoFlushOnEoj()) { scs.flushLineBuffer();
pd.flush(); if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
pd.endJob();
} }
firePrintJobComplete(pd.getPageCount(), pd.getByteCount()); firePrintJobComplete(pd.getPageCount(), pd.getByteCount());
@@ -98,4 +98,34 @@ public class Telnet3270EPClient {
} }
} }
} }
public PrinterDefinitionTable getPDT() {
return protocolEngine.getPD().getPDT();
}
public void setPDT(PrinterDefinitionTable pdt) {
protocolEngine.getConfig().setPrinterDefinitionTable(pdt);
protocolEngine.getPD().setPDT(pdt);
}
public void flush() {
if (protocolEngine.getActiveLuType() == PrinterConstants.LU_TYPE_3_DS) {
protocolEngine.getPrintPS().flushPrintBuffer();
} else {
protocolEngine.getSCS().flushLineBuffer();
}
protocolEngine.getPD().flush();
}
public void sendEOJ(boolean isComplete) {
protocolEngine.sendEOJ(isComplete);
}
public int getChannelLine(int channel) {
return protocolEngine.getConfig().getChannelLine(channel);
}
public void setChannelLine(int channel, int line) {
protocolEngine.getConfig().setChannelLine(channel, line);
}
} }
@@ -0,0 +1,105 @@
package haus.nightmare.lib3270j.charset;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 IBM 3270 APL / Graphic Escape (GA23-0059) character set translation.
*/
public class APLTranslationPhase8Test {
@Test
public void testAPLBoxDrawingGlyphs() {
EbcdicTranslator translator = new EbcdicTranslator();
// Box lines and corners
assertEquals('─', translator.mapAPL(0xA2)); // Horizontal line
assertEquals('│', translator.mapAPL(0x85)); // Vertical line
assertEquals('┌', translator.mapAPL(0xC5)); // Top Left corner
assertEquals('┐', translator.mapAPL(0xD5)); // Top Right corner
assertEquals('└', translator.mapAPL(0xC4)); // Bottom Left corner
assertEquals('┘', translator.mapAPL(0xD4)); // Bottom Right corner
assertEquals('├', translator.mapAPL(0xC6)); // Left T
assertEquals('┤', translator.mapAPL(0xD6)); // Right T
assertEquals('┬', translator.mapAPL(0xC7)); // Top T
assertEquals('┴', translator.mapAPL(0xD7)); // Bottom T
assertEquals('┼', translator.mapAPL(0xCB)); // Cross
}
@Test
public void testAPLMathAndSpecialSymbols() {
EbcdicTranslator translator = new EbcdicTranslator();
assertEquals('≤', translator.mapAPL(0x8C)); // Less than or equal
assertEquals('≥', translator.mapAPL(0xAE)); // Greater than or equal
assertEquals('≠', translator.mapAPL(0xBE)); // Not equal
assertEquals('[', translator.mapAPL(0xAD)); // Left bracket
assertEquals(']', translator.mapAPL(0xBD)); // Right bracket
assertEquals('{', translator.mapAPL(0x8D)); // Left brace
assertEquals('}', translator.mapAPL(0x9D)); // Right brace
assertEquals('°', translator.mapAPL(0xB0)); // Degree
assertEquals('±', translator.mapAPL(0xB1)); // Plus-minus
assertEquals('²', translator.mapAPL(0xB2)); // Superscript 2
assertEquals('³', translator.mapAPL(0xB3)); // Superscript 3
assertEquals('¯', translator.mapAPL(0xAF)); // Overbar / High Minus
assertEquals('Ω', translator.mapAPL(0xBA)); // Greek Omega
assertEquals('µ', translator.mapAPL(0xBF)); // Micro
assertEquals('¬', translator.mapAPL(0x5F)); // Not sign
}
@Test
public void testAPLOperatorsAndStructureGlyphs() {
EbcdicTranslator translator = new EbcdicTranslator();
assertEquals('⋄', translator.mapAPL(0x80)); // Diamond
assertEquals('', translator.mapAPL(0x81)); // Alpha
assertEquals('⊥', translator.mapAPL(0x82)); // Up tack / decode
assertEquals('∩', translator.mapAPL(0x83)); // Intersection
assertEquals('⌊', translator.mapAPL(0x84)); // Floor
assertEquals('∇', translator.mapAPL(0x87)); // Del
assertEquals('∆', translator.mapAPL(0x88)); // Delta
assertEquals('', translator.mapAPL(0x89)); // Iota
assertEquals('→', translator.mapAPL(0x8A)); // Right arrow
assertEquals('⍞', translator.mapAPL(0x8B)); // Quote quad
assertEquals('×', translator.mapAPL(0x8E)); // Multiply
assertEquals('÷', translator.mapAPL(0x8F)); // Divide
assertEquals('⍟', translator.mapAPL(0x90)); // Circle star
assertEquals('⌹', translator.mapAPL(0x91)); // Domino / quad divide
assertEquals('', translator.mapAPL(0x92)); // Down tack / encode
assertEquals('', translator.mapAPL(0x93)); // Union
assertEquals('⌈', translator.mapAPL(0x94)); // Ceiling
assertEquals('', translator.mapAPL(0x95)); // Rho / shape
assertEquals('⍵', translator.mapAPL(0x96)); // Omega
assertEquals('○', translator.mapAPL(0x99)); // Circle
assertEquals('←', translator.mapAPL(0x9A)); // Left arrow
assertEquals('⍋', translator.mapAPL(0x9C)); // Grade up
assertEquals('⍒', translator.mapAPL(0x9E)); // Grade down
assertEquals('⍝', translator.mapAPL(0x9F)); // Lamp / comment
assertEquals('⊂', translator.mapAPL(0xA6)); // Enclose / left shoe
assertEquals('⊃', translator.mapAPL(0xA7)); // Disclose / right shoe
assertEquals('↑', translator.mapAPL(0xA8)); // Take / up arrow
assertEquals('↓', translator.mapAPL(0xA9)); // Drop / down arrow
assertEquals('⎕', translator.mapAPL(0xAA)); // Quad
assertEquals('⍉', translator.mapAPL(0xAC)); // Transpose
assertEquals('⊖', translator.mapAPL(0xB4)); // Circle bar
}
@Test
public void testScreenBufferAPLCharacterSetIntegration() {
EbcdicTranslator translator = new EbcdicTranslator();
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
// Place box-drawing corner and horizontal line with cs=1 (CS_APL)
screen.setCellWithCS(0, 0xC5, 1); // Top left
screen.setCellWithCS(1, 0xA2, 1); // Horizontal
screen.setCellWithCS(2, 0xD5, 1); // Top right
screen.translateToUnicode();
assertEquals('┌', (char) screen.getCell(0).ucs4);
assertEquals('─', (char) screen.getCell(1).ucs4);
assertEquals('┐', (char) screen.getCell(2).ucs4);
}
}
@@ -0,0 +1,79 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 custom user-defined character translation override tables.
*/
public class CustomTranslationOverridePhase8Test {
@Test
public void testSingleOverride() {
EbcdicTranslator translator = new EbcdicTranslator("037");
assertFalse(translator.hasCustomOverrides());
// Default CP037 mapping for 0xC1 is 'A'
assertEquals('A', translator.ebcdicToUnicode(0xC1));
assertEquals(0xC1, translator.unicodeToEbcdic('A'));
// Override 0xC1 to '★' (\u2605)
translator.setCustomOverride(0xC1, '★');
assertTrue(translator.hasCustomOverrides());
assertEquals('★', translator.ebcdicToUnicode(0xC1));
assertEquals(0xC1, translator.unicodeToEbcdic('★'));
// Non-overridden characters should continue using default CP037
assertEquals('B', translator.ebcdicToUnicode(0xC2));
assertEquals(0xC2, translator.unicodeToEbcdic('B'));
// Safe conversion
assertEquals((byte) 0xC1, translator.unicodeToEbcdicSafe('★'));
// Remove override
translator.removeCustomOverride(0xC1);
assertFalse(translator.hasCustomOverrides());
assertEquals('A', translator.ebcdicToUnicode(0xC1));
}
@Test
public void testBulkOverrides() {
EbcdicTranslator translator = new EbcdicTranslator("037");
Map<Integer, Character> ebcToUni = new HashMap<>();
ebcToUni.put(0x81, 'α'); // replace 'a' with alpha
ebcToUni.put(0x82, 'β'); // replace 'b' with beta
translator.setCustomOverrides(ebcToUni, null);
assertTrue(translator.hasCustomOverrides());
assertEquals('α', translator.ebcdicToUnicode(0x81));
assertEquals('β', translator.ebcdicToUnicode(0x82));
assertEquals('c', translator.ebcdicToUnicode(0x83));
assertEquals(0x81, translator.unicodeToEbcdic('α'));
assertEquals(0x82, translator.unicodeToEbcdic('β'));
// Clear overrides
translator.clearCustomOverrides();
assertFalse(translator.hasCustomOverrides());
assertEquals('a', translator.ebcdicToUnicode(0x81));
assertEquals('b', translator.ebcdicToUnicode(0x82));
}
@Test
public void testStringTranslationWithOverrides() {
EbcdicTranslator translator = new EbcdicTranslator("037");
translator.setCustomOverride(0xBA, ''); // override '[' with ''
translator.setCustomOverride(0xBB, ''); // override ']' with ''
byte[] input = new byte[] { (byte) 0xBA, (byte) 0xC8, (byte) 0xC9, (byte) 0xBB }; // [HI] in CP037
String decoded = translator.ebcdicToString(input, 0, input.length);
assertEquals("HI", decoded);
byte[] reEncoded = translator.stringToEbcdic("HI");
assertArrayEquals(input, reEncoded);
}
}
@@ -0,0 +1,89 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 DBCS SO/SI escape sequence handling, shift preservation, and UTF-8 conversion.
*/
public class DBCSSOSIEscapingPhase8Test {
@Test
public void testSOSIPreserveMode() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
cp.registerDbcsPair(0x4342, '\u4EAC'); // '京'
// EBCDIC: 'I'(0xC9) + SO(0x0E) + (0x4341) + (0x4342) + SI(0x0F) + '1'(0xF1)
byte[] ebcdic = new byte[] {
(byte) 0xC9,
0x0E, (byte) 0x43, (byte) 0x41, (byte) 0x43, (byte) 0x42, 0x0F,
(byte) 0xF1
};
// Standard mode strips SO/SI from Unicode string
String standard = cp.ebcdicToString(ebcdic, 0, ebcdic.length, false);
assertEquals("I東京1", standard);
// Preserved mode keeps \u000E and \u000F in Unicode string
String preserved = cp.ebcdicToString(ebcdic, 0, ebcdic.length, true);
assertEquals("I\u000E東京\u000F1", preserved);
// Utility helper
String utf8WithSosi = cp.ebcdicToUtf8WithSOSI(ebcdic);
assertEquals(preserved, utf8WithSosi);
}
@Test
public void testEmbeddedSOSIStringConversion() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
cp.registerDbcsPair(0x4342, '\u4EAC'); // '京'
// Encode string that already contains embedded \u000E and \u000F
String inputWithSOSI = "I\u000E東京\u000F1";
byte[] encoded = cp.stringToEbcdic(inputWithSOSI, true);
byte[] expected = new byte[] {
(byte) 0xC9,
0x0E, (byte) 0x43, (byte) 0x41, (byte) 0x43, (byte) 0x42, 0x0F,
(byte) 0xF1
};
assertArrayEquals(expected, encoded);
// Utility helper
byte[] helperEncoded = cp.utf8WithSOSIToEbcdic(inputWithSOSI);
assertArrayEquals(expected, helperEncoded);
}
@Test
public void testAutoCloseUnclosedDBCSShift() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
// Text ending with a DBCS character without explicit SI
String s = "A東京";
byte[] encoded = cp.stringToEbcdic(s, false);
// Must end with SI (0x0F)
assertTrue(encoded.length > 0);
assertEquals(0x0F, encoded[encoded.length - 1] & 0xFF);
}
@Test
public void testRedundantSOSISuppression() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771');
// Input containing redundant duplicate \u000E \u000E
String s = "\u000E\u000E東\u000F\u000F";
byte[] encoded = cp.stringToEbcdic(s, true);
// Should contain only one SO (0x0E) and one SI (0x0F)
assertEquals(4, encoded.length);
assertEquals(0x0E, encoded[0] & 0xFF);
assertEquals(0x43, encoded[1] & 0xFF);
assertEquals(0x41, encoded[2] & 0xFF);
assertEquals(0x0F, encoded[3] & 0xFF);
}
}
@@ -0,0 +1,194 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS).
*/
public class ExtendedCodePagesPhase8Test {
@Test
public void testArabicCp420() {
CodePage cp = CodePageRegistry.getCodePage("420");
assertNotNull(cp);
assertEquals("420", cp.getCodePageId());
assertEquals(420, cp.getCpgid());
assertFalse(cp.isDBCS());
// Test alias resolution
assertEquals("420", CodePageRegistry.getCodePage("ar").getCodePageId());
assertEquals("420", CodePageRegistry.getCodePage("arabic").getCodePageId());
assertEquals("420", CodePageRegistry.getCodePage("ebcdic-cp-ar").getCodePageId());
assertEquals("420", CodePageRegistry.getCodePage("IBM420").getCodePageId());
// Basic ASCII and Arabic character mapping
assertEquals('A', cp.ebcdicToUnicode(0xC1));
assertEquals(0xC1, cp.unicodeToEbcdic('A'));
assertEquals(' ', cp.ebcdicToUnicode(0x40));
assertEquals(0x40, cp.unicodeToEbcdic(' '));
// Arabic characters (Hamza, Alef, Yeh, etc.)
int ebcHamza = cp.unicodeToEbcdic('\u0621');
assertTrue(ebcHamza >= 0);
assertEquals('\u0621', cp.ebcdicToUnicode(ebcHamza));
}
@Test
public void testHebrewCp424AndCp803() {
CodePage cp424 = CodePageRegistry.getCodePage("424");
assertNotNull(cp424);
assertEquals("424", cp424.getCodePageId());
assertEquals(424, cp424.getCpgid());
assertEquals(941, cp424.getCgcsgid());
assertEquals("424", CodePageRegistry.getCodePage("he").getCodePageId());
assertEquals("424", CodePageRegistry.getCodePage("hebrew").getCodePageId());
// Hebrew Alef \u05D0 in Cp424
int ebcAlef424 = cp424.unicodeToEbcdic('\u05D0');
assertTrue(ebcAlef424 >= 0);
assertEquals('\u05D0', cp424.ebcdicToUnicode(ebcAlef424));
CodePage cp803 = CodePageRegistry.getCodePage("803");
assertNotNull(cp803);
assertEquals("803", cp803.getCodePageId());
assertEquals(803, cp803.getCpgid());
assertEquals(1147, cp803.getCgcsgid());
assertEquals("803", CodePageRegistry.getCodePage("hebrew-old").getCodePageId());
assertEquals("803", CodePageRegistry.getCodePage("israel").getCodePageId());
assertEquals("803", CodePageRegistry.getCodePage("iw").getCodePageId());
// Hebrew Alef in Cp803
int ebcAlef803 = cp803.unicodeToEbcdic('\u05D0');
assertTrue(ebcAlef803 >= 0);
assertEquals('\u05D0', cp803.ebcdicToUnicode(ebcAlef803));
}
@Test
public void testThaiCp838AndCp1160() {
CodePage cp838 = CodePageRegistry.getCodePage("838");
assertNotNull(cp838);
assertEquals("838", cp838.getCodePageId());
assertEquals(838, cp838.getCpgid());
assertEquals(1176, cp838.getCgcsgid());
assertEquals("838", CodePageRegistry.getCodePage("th").getCodePageId());
assertEquals("838", CodePageRegistry.getCodePage("thai").getCodePageId());
// Thai character Ko Kai \u0E01
int ebcKoKai = cp838.unicodeToEbcdic('\u0E01');
assertTrue(ebcKoKai >= 0);
assertEquals('\u0E01', cp838.ebcdicToUnicode(ebcKoKai));
CodePage cp1160 = CodePageRegistry.getCodePage("1160");
assertNotNull(cp1160);
assertEquals("1160", cp1160.getCodePageId());
assertEquals(1160, cp1160.getCpgid());
assertEquals('€', cp1160.ebcdicToUnicode(0x9F));
assertEquals(0x9F, cp1160.unicodeToEbcdic('€'));
assertEquals("1160", CodePageRegistry.getCodePage("thai-euro").getCodePageId());
}
@Test
public void testCyrillicCp1025Cp1123Cp1154Cp880() {
CodePage cp1025 = CodePageRegistry.getCodePage("1025");
assertNotNull(cp1025);
assertEquals(1025, cp1025.getCpgid());
assertEquals(1150, cp1025.getCgcsgid());
assertEquals("1025", CodePageRegistry.getCodePage("ru").getCodePageId());
assertEquals("1025", CodePageRegistry.getCodePage("cyrillic").getCodePageId());
assertEquals("1025", CodePageRegistry.getCodePage("russian").getCodePageId());
// Cyrillic Capital Letter A \u0410 and small a \u0430
int ebcA1025 = cp1025.unicodeToEbcdic('\u0410');
assertTrue(ebcA1025 >= 0);
assertEquals('\u0410', cp1025.ebcdicToUnicode(ebcA1025));
// Cp1123 Ukraine
CodePage cp1123 = CodePageRegistry.getCodePage("1123");
assertNotNull(cp1123);
assertEquals(1123, cp1123.getCpgid());
assertEquals(1399, cp1123.getCgcsgid());
assertEquals("1123", CodePageRegistry.getCodePage("ukraine").getCodePageId());
assertEquals("1123", CodePageRegistry.getCodePage("ukrainian").getCodePageId());
// Cp1154 Cyrillic with Euro
CodePage cp1154 = CodePageRegistry.getCodePage("1154");
assertNotNull(cp1154);
assertEquals(1154, cp1154.getCpgid());
assertEquals(1305, cp1154.getCgcsgid());
assertEquals('€', cp1154.ebcdicToUnicode(0x9F));
assertEquals("1154", CodePageRegistry.getCodePage("cyrillic-euro").getCodePageId());
// Cp880 Cyrillic Russian
CodePage cp880 = CodePageRegistry.getCodePage("880");
assertNotNull(cp880);
assertEquals(880, cp880.getCpgid());
assertEquals(960, cp880.getCgcsgid());
assertEquals("880", CodePageRegistry.getCodePage("cyrillic-russian").getCodePageId());
}
@Test
public void testTurkishCp1155AndCp905() {
CodePage cp1155 = CodePageRegistry.getCodePage("1155");
assertNotNull(cp1155);
assertEquals(1155, cp1155.getCpgid());
assertEquals(1306, cp1155.getCgcsgid());
assertEquals('€', cp1155.ebcdicToUnicode(0x9F));
assertEquals("1155", CodePageRegistry.getCodePage("turkish-euro").getCodePageId());
CodePage cp905 = CodePageRegistry.getCodePage("905");
assertNotNull(cp905);
assertEquals(905, cp905.getCpgid());
assertEquals(1151, cp905.getCgcsgid());
assertEquals("905", CodePageRegistry.getCodePage("turkish-latin3").getCodePageId());
}
@Test
public void testExtendedChineseDBCSCp1388AndCp1371() {
CodePage cp1388 = CodePageRegistry.getCodePage("1388");
assertNotNull(cp1388);
assertTrue(cp1388.isDBCS());
assertEquals(1388, cp1388.getCpgid());
assertEquals(1175, cp1388.getCgcsgid());
assertEquals("1388", CodePageRegistry.getCodePage("chinese-ext-simplified").getCodePageId());
assertEquals("1388", CodePageRegistry.getCodePage("zh-simplified-ext").getCodePageId());
CodePage cp1371 = CodePageRegistry.getCodePage("1371");
assertNotNull(cp1371);
assertTrue(cp1371.isDBCS());
assertEquals(1371, cp1371.getCpgid());
assertEquals(1174, cp1371.getCgcsgid());
assertEquals("1371", CodePageRegistry.getCodePage("chinese-ext-traditional").getCodePageId());
assertEquals("1371", CodePageRegistry.getCodePage("zh-traditional-ext").getCodePageId());
}
@Test
public void testHasCodePageQuery() {
assertTrue(CodePageRegistry.hasCodePage("037"));
assertTrue(CodePageRegistry.hasCodePage("1047"));
assertTrue(CodePageRegistry.hasCodePage("420"));
assertTrue(CodePageRegistry.hasCodePage("424"));
assertTrue(CodePageRegistry.hasCodePage("803"));
assertTrue(CodePageRegistry.hasCodePage("838"));
assertTrue(CodePageRegistry.hasCodePage("1160"));
assertTrue(CodePageRegistry.hasCodePage("1025"));
assertTrue(CodePageRegistry.hasCodePage("1123"));
assertTrue(CodePageRegistry.hasCodePage("1154"));
assertTrue(CodePageRegistry.hasCodePage("880"));
assertTrue(CodePageRegistry.hasCodePage("1155"));
assertTrue(CodePageRegistry.hasCodePage("905"));
assertTrue(CodePageRegistry.hasCodePage("1388"));
assertTrue(CodePageRegistry.hasCodePage("1371"));
assertTrue(CodePageRegistry.hasCodePage("ar"));
assertTrue(CodePageRegistry.hasCodePage("hebrew"));
assertTrue(CodePageRegistry.hasCodePage("thai"));
assertTrue(CodePageRegistry.hasCodePage("russian"));
assertTrue(CodePageRegistry.hasCodePage("ukraine"));
assertFalse(CodePageRegistry.hasCodePage("nonexistent-cp-9999"));
}
}
@@ -0,0 +1,453 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive Unit Tests for Phase 10: ECL API & Session Architecture Facade.
*/
public class ECLSessionPhase10Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
private ECLOIA oia;
private Telnet3270Client client;
private ECLSession session;
@BeforeEach
public void setUp() {
ConnectionConfig config = new ConnectionConfig("mainframe.example.com", 23, TerminalModel.IBM_3279_4);
config.setLuName("LU3270A");
config.setCodePage("037");
client = new Telnet3270Client(config);
session = new ECLSession(client);
ps = session.GetPS();
oia = session.GetOIA();
screen = client.getScreenBuffer();
translator = client.getTranslator();
input = client.getInputProcessor();
}
@Test
public void testECLSessionInstantiationWithConfigAndProperties() {
// Test default constructor
ECLSession defSession = new ECLSession();
assertNotNull(defSession.GetPS());
assertNotNull(defSession.GetOIA());
assertNotNull(defSession.GetConnection());
assertNotNull(defSession.GetXfer());
assertNotNull(defSession.GetFieldList());
assertNotNull(defSession.GetClient());
assertEquals(23, defSession.GetConnection().GetPort());
// Test explicit host/port/model constructor
ECLSession hpSession = new ECLSession("tso.test.org", 992, TerminalModel.IBM_3279_2, true);
assertEquals("tso.test.org", hpSession.GetConnection().GetHost());
assertEquals(992, hpSession.GetConnection().GetPort());
assertEquals(TerminalModel.IBM_3279_2, hpSession.GetConnection().GetModel());
assertTrue(hpSession.GetConnection().IsSSL());
// Test Properties constructor
Properties props = new Properties();
props.setProperty(ECLSession.SESSION_HOST, "mvs.corp.net");
props.setProperty(ECLSession.SESSION_PORT, "2023");
props.setProperty(ECLSession.SESSION_CODE_PAGE, "1047");
props.setProperty(ECLSession.SESSION_MODEL, "5"); // 27x132
props.setProperty(ECLSession.SESSION_SSL, "true");
props.setProperty(ECLSession.SESSION_LU_NAME, "TSOLU01");
props.setProperty(ECLSession.SESSION_TN3270E, "true");
ECLSession propSession = new ECLSession(props);
assertEquals("mvs.corp.net", propSession.GetConnection().GetHost());
assertEquals(2023, propSession.GetConnection().GetPort());
assertEquals("1047", propSession.GetConnection().GetCodePage());
assertEquals(TerminalModel.IBM_3279_5, propSession.GetConnection().GetModel());
assertTrue(propSession.GetConnection().IsSSL());
assertEquals("TSOLU01", propSession.GetConnection().GetLUName());
assertEquals("TN3270E", propSession.GetConnection().GetConnType());
// Verify Properties sync and retrieval
Properties retrieved = propSession.GetProperties();
assertEquals("mvs.corp.net", retrieved.getProperty(ECLSession.SESSION_HOST));
assertEquals("2023", retrieved.getProperty(ECLSession.SESSION_PORT));
}
@Test
public void testECLConnectionAttributesAndState() {
ECLConnection conn = session.GetConnection();
assertNotNull(conn);
assertEquals("mainframe.example.com", conn.GetHost());
assertEquals(23, conn.GetPort());
assertEquals("037", conn.GetCodePage());
assertEquals(TerminalModel.IBM_3279_4, conn.GetModel());
assertEquals("LU3270A", conn.GetLUName());
assertEquals("LU3270A", conn.GetDevName());
assertFalse(conn.IsSSL());
assertEquals(ConnectionState.NOT_CONNECTED, conn.GetState());
assertEquals(0, conn.GetStateCode());
assertFalse(conn.IsConnected());
assertFalse(conn.IsReady());
assertTrue(conn.IsDisconnecting());
// Verify state mappings
assertEquals(0, ConnectionState.NOT_CONNECTED.toHoDStateCode());
assertEquals(1, ConnectionState.TELNET_PENDING.toHoDStateCode());
assertEquals(2, ConnectionState.CONNECTED_NVT.toHoDStateCode());
assertEquals(2, ConnectionState.CONNECTED_UNBOUND.toHoDStateCode());
assertEquals(3, ConnectionState.CONNECTED_3270.toHoDStateCode());
assertEquals(3, ConnectionState.CONNECTED_TN3270E.toHoDStateCode());
}
@Test
public void testECLScreenDescMatching() {
// Setup screen buffer with sample formatted screen
screen.clear();
screen.setCursorAddress(0);
// Write: SF (prot), "TSO/E LOGON", SF (unprot), "USER01", SF (prot), "PASSWORD", SF (unprot), " "
screen.setCellFA(0, (byte) 0x60); // SF protected
String text1 = "TSO/E LOGON";
for (int i = 0; i < text1.length(); i++) {
screen.setChar(0, i + 1, text1.charAt(i));
}
int userFaPos = 20;
screen.setCellFA(userFaPos, (byte) 0x40); // SF unprotected
String userVal = "USER01";
for (int i = 0; i < userVal.length(); i++) {
screen.setChar(0, userFaPos + 1 + i, userVal.charAt(i));
}
int passFaPos = 40;
screen.setCellFA(passFaPos, (byte) 0x60); // SF protected
String passLabel = "PASSWORD";
for (int i = 0; i < passLabel.length(); i++) {
screen.setChar(0, passFaPos + 1 + i, passLabel.charAt(i));
}
int passInputPos = 60;
screen.setCellFA(passInputPos, (byte) 0x40); // SF unprot hidden
ps.setCursorPos(userFaPos + 1);
// 1. Test match string at 1-based row/col
ECLScreenDesc desc1 = new ECLScreenDesc();
desc1.AddString("TSO/E LOGON", 1, 2, true);
assertTrue(desc1.Matches(session));
assertTrue(desc1.Matches(ps, oia));
assertTrue(desc1.Matches(client));
// Test case sensitivity
ECLScreenDesc descCaseMismatch = new ECLScreenDesc();
descCaseMismatch.AddString("tso/e logon", 1, 2, true);
assertFalse(descCaseMismatch.Matches(session));
ECLScreenDesc descCaseInsensitive = new ECLScreenDesc();
descCaseInsensitive.AddString("tso/e logon", 1, 2, false);
assertTrue(descCaseInsensitive.Matches(session));
// 2. Test match string anywhere on screen
ECLScreenDesc descAnywhere = new ECLScreenDesc();
descAnywhere.AddString("PASSWORD");
assertTrue(descAnywhere.Matches(session));
ECLScreenDesc descAnywhereFail = new ECLScreenDesc();
descAnywhereFail.AddString("CICS TRANSACTION");
assertFalse(descAnywhereFail.Matches(session));
// 3. Test match string in rectangular region
ECLScreenDesc descRect = new ECLScreenDesc();
descRect.AddStringInRect("USER01", 1, 1, 2, 80, true);
assertTrue(descRect.Matches(session));
ECLScreenDesc descRectFail = new ECLScreenDesc();
descRectFail.AddStringInRect("USER01", 2, 1, 5, 80, true);
assertFalse(descRectFail.Matches(session));
// 4. Test match cursor position
ECLScreenDesc descCursor = new ECLScreenDesc();
descCursor.AddCursorPos(1, userFaPos + 2);
assertTrue(descCursor.Matches(session));
ECLScreenDesc descCursorFail = new ECLScreenDesc();
descCursorFail.AddCursorPos(10, 10);
assertFalse(descCursorFail.Matches(session));
// 5. Test field counts
ECLScreenDesc descFields = new ECLScreenDesc();
descFields.AddNumFields(4);
descFields.AddNumInputFields(2);
assertTrue(descFields.Matches(session));
ECLScreenDesc descFieldsFail = new ECLScreenDesc();
descFieldsFail.AddNumFields(10);
assertFalse(descFieldsFail.Matches(session));
// 6. Test OIA status
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
ECLScreenDesc descOia = new ECLScreenDesc();
descOia.AddOIAStatus(ECLConstants.INHIBIT_NOT_INHIBITED);
assertTrue(descOia.Matches(session));
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
assertFalse(descOia.Matches(session));
ECLScreenDesc descOiaLocked = new ECLScreenDesc();
descOiaLocked.AddOIAStatus(ECLConstants.INHIBIT_SYSTEM_LOCK);
assertTrue(descOiaLocked.Matches(session));
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
// 7. Test Clear and composite descriptor
ECLScreenDesc composite = new ECLScreenDesc();
composite.AddString("TSO/E LOGON", 1, 2);
composite.AddString("PASSWORD");
composite.AddNumFields(4);
assertEquals(3, composite.getConditionCount());
assertTrue(composite.Matches(session));
composite.Clear();
assertEquals(0, composite.getConditionCount());
assertTrue(composite.Matches(session)); // Empty descriptor matches everything
}
@Test
public void testECLPSListenerAndEventDispatching() {
AtomicInteger updateCount = new AtomicInteger(0);
AtomicInteger cursorCount = new AtomicInteger(0);
AtomicInteger alarmCount = new AtomicInteger(0);
AtomicInteger resizeCount = new AtomicInteger(0);
AtomicReference<ECLPSEvent> lastEvent = new AtomicReference<>();
ECLPSListener listener = new ECLPSListener() {
@Override
public void psChanged(ECLPSEvent event) {
updateCount.incrementAndGet();
lastEvent.set(event);
}
@Override
public void psCursorMoved(ECLPSEvent event) {
cursorCount.incrementAndGet();
}
@Override
public void psAlarm(ECLPSEvent event) {
alarmCount.incrementAndGet();
}
@Override
public void psResized(ECLPSEvent event) {
resizeCount.incrementAndGet();
}
};
ps.RegisterPSEvent(listener);
// 1. Trigger PS Update
ps.notifyPSUpdate(2, 5, 2, 20, false);
assertEquals(1, updateCount.get());
assertNotNull(lastEvent.get());
assertEquals(ECLPSEvent.PS_UPDATE, lastEvent.get().getEventType());
assertEquals(2, lastEvent.get().getStartRow());
assertEquals(5, lastEvent.get().getStartCol());
assertEquals(2, lastEvent.get().getEndRow());
assertEquals(20, lastEvent.get().getEndCol());
assertFalse(lastEvent.get().isFullUpdate());
assertSame(ps, lastEvent.get().getPS());
// 2. Trigger Cursor Move
ps.notifyCursorMoved(0, 85);
assertEquals(2, updateCount.get());
assertEquals(1, cursorCount.get());
assertEquals(ECLPSEvent.PS_CURSOR, lastEvent.get().getEventType());
assertEquals(85, lastEvent.get().getNewCursorAddress());
// 3. Trigger Alarm
ps.notifyAlarm();
assertEquals(3, updateCount.get());
assertEquals(1, alarmCount.get());
assertEquals(ECLPSEvent.PS_ALARM, lastEvent.get().getEventType());
// 4. Trigger Screen Resize
ps.notifyScreenResized(32, 80);
assertEquals(4, updateCount.get());
assertEquals(1, resizeCount.get());
assertEquals(ECLPSEvent.PS_RESIZE, lastEvent.get().getEventType());
assertEquals(32, lastEvent.get().getRows());
assertEquals(80, lastEvent.get().getCols());
// 5. Unregister listener
ps.UnregisterPSEvent(listener);
ps.notifyPSUpdate(0, 0, 23, 79, true);
assertEquals(4, updateCount.get()); // Count should not change after unregister
}
@Test
public void testECLOIAListenerAndEventDispatching() {
AtomicInteger changeCount = new AtomicInteger(0);
AtomicInteger lockCount = new AtomicInteger(0);
AtomicReference<ECLOIAEvent> lastOiaEvent = new AtomicReference<>();
ECLOIAListener listener = new ECLOIAListener() {
@Override
public void oiaChanged(ECLOIAEvent event) {
changeCount.incrementAndGet();
lastOiaEvent.set(event);
}
@Override
public void oiaLockStateChanged(ECLOIAEvent event) {
lockCount.incrementAndGet();
}
};
oia.RegisterOIAEvent(listener);
// 1. Change inhibit state
oia.setInputInhibited(ECLConstants.INHIBIT_PROTECTED_FIELD);
assertEquals(1, changeCount.get());
assertNotNull(lastOiaEvent.get());
assertEquals(ECLConstants.INHIBIT_PROTECTED_FIELD, lastOiaEvent.get().getInputInhibited());
assertEquals("X-PROT", lastOiaEvent.get().getStatusString());
assertTrue(lastOiaEvent.get().isInputInhibited());
assertSame(oia, lastOiaEvent.get().getOIA());
// 2. Change inhibit back to normal
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
assertEquals(2, changeCount.get());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, lastOiaEvent.get().getInputInhibited());
assertEquals("READY", lastOiaEvent.get().getStatusString());
assertFalse(lastOiaEvent.get().isInputInhibited());
// 3. Test uppercase wait methods
assertTrue(oia.WaitForInput(100));
assertTrue(oia.WaitForSystemAvailable(100));
assertTrue(oia.WaitForAppAvailable(100));
// 4. Unregister listener
oia.UnregisterOIAEvent(listener);
oia.setInputInhibited(ECLConstants.INHIBIT_NUMERIC_ONLY);
assertEquals(2, changeCount.get());
}
@Test
public void testECLCommListenerAndEventDispatching() {
ECLConnection conn = session.GetConnection();
AtomicInteger commCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
AtomicBoolean notifyConnected = new AtomicBoolean(false);
AtomicReference<ECLCommEvent> lastCommEvent = new AtomicReference<>();
ECLCommListener listener = new ECLCommListener() {
@Override
public void commEvent(ECLCommEvent event) {
commCount.incrementAndGet();
lastCommEvent.set(event);
}
@Override
public void commError(ECLCommEvent event) {
errorCount.incrementAndGet();
}
};
conn.RegisterCommEvent(listener);
conn.RegisterCommEvent(connected -> notifyConnected.set(connected), false);
// Simulate connection state change via listener
client.getTelnetFSM().onError("Connection reset by peer");
assertEquals(1, commCount.get());
assertEquals(1, errorCount.get());
assertNotNull(lastCommEvent.get());
assertEquals(ECLCommEvent.COMM_ERROR, lastCommEvent.get().getEventType());
assertEquals("Connection reset by peer", lastCommEvent.get().getErrorMessage());
// Unregister
conn.UnregisterCommEvent(listener);
}
@Test
public void testSendKeysWithCoordinates() {
screen.clear();
screen.setCursorAddress(0);
// Setup an unprotected field at row 5, col 10 (0-based: row 4, col 9)
int faPos = 4 * 80 + 8; // 328
screen.setCellFA(faPos, (byte) 0x40); // Unprotected
// Send keys with 1-based (row 5, col 10)
session.SendKeys("IBM3270", 5, 10);
assertEquals("IBM3270", ps.getString(faPos + 1, 7));
// Send bracketed mnemonics
ps.setCursorPos(1, 1);
ps.SendKeys("A[tab]B", 1, 1);
assertNotNull(ps.getString(0, 10));
}
@Test
public void testWaitForScreenWithDescriptor() {
screen.clear();
screen.setChar(0, 0, 'L');
screen.setChar(0, 1, 'O');
screen.setChar(0, 2, 'G');
screen.setChar(0, 3, 'O');
screen.setChar(0, 4, 'N');
ECLScreenDesc desc = new ECLScreenDesc();
desc.AddString("LOGON", 1, 1);
assertTrue(session.WaitForScreen(desc, 100));
assertTrue(ps.WaitForScreen(desc, 100));
ECLScreenDesc descFail = new ECLScreenDesc();
descFail.AddString("NONEXISTENT", 1, 1);
assertFalse(session.WaitForScreen(descFail, 50));
}
@Test
public void testIBMHoDPackageCompatibility() {
// Instantiate using haus.nightmare.lib3270j.eNetwork.ECL.* package hierarchy
haus.nightmare.lib3270j.eNetwork.ECL.ECLSession hodSession = new haus.nightmare.lib3270j.eNetwork.ECL.ECLSession();
assertNotNull(hodSession.GetPS());
assertNotNull(hodSession.GetOIA());
assertNotNull(hodSession.GetConnection());
assertNotNull(hodSession.GetXfer());
assertNotNull(hodSession.GetFieldList());
haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc hodDesc = new haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc();
hodDesc.AddString("TEST");
assertEquals(1, hodDesc.getConditionCount());
AtomicBoolean hodPsReceived = new AtomicBoolean(false);
hodSession.GetPS().RegisterPSEvent(new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSListener() {
@Override
public void psChanged(haus.nightmare.lib3270j.ecl.ECLPSEvent event) {
hodPsReceived.set(true);
}
});
hodSession.GetPS().notifyPSUpdate(0, 0, 10, 10, true);
assertTrue(hodPsReceived.get());
// Test exception
haus.nightmare.lib3270j.eNetwork.ECL.ECLException ex = new haus.nightmare.lib3270j.eNetwork.ECL.ECLException(haus.nightmare.lib3270j.eNetwork.ECL.ECLErrors.ECL_ERR_COMM_TIMEOUT, "Timeout");
assertEquals(haus.nightmare.lib3270j.eNetwork.ECL.ECLErrors.ECL_ERR_COMM_TIMEOUT, ex.getErrorCode());
assertEquals("Timeout", ex.getMessage());
}
}
@@ -88,4 +88,51 @@ public class ECLXferTest {
assertTrue(loaded[0]); assertTrue(loaded[0]);
assertEquals(1, fileList.size()); assertEquals(1, fileList.size());
} }
@Test
public void testGetFilesBatchDownloadCallback() {
boolean[] loaded = new boolean[1];
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", "/tmp/nonexistent_test_dir", new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
loaded[0] = true;
}
@Override
public void onDirectoryError(String errorMessage) {}
});
assertTrue(loaded[0]);
}
@Test
public void testGetFilesVectorOverload() {
java.util.Vector<String> localFiles = new java.util.Vector<>();
java.util.Vector<String> hostFiles = new java.util.Vector<>();
boolean[] loaded = new boolean[1];
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", 0, 0, localFiles, hostFiles, new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
loaded[0] = true;
}
@Override
public void onDirectoryError(String errorMessage) {}
});
assertTrue(loaded[0]);
assertEquals(1, hostFiles.size());
assertEquals("PROFILE EXEC A1", hostFiles.get(0));
}
@Test
public void testCMSPrintXferInstantiation() {
assertNotNull(xfer.getCMSPrintXfer());
}
@Test
public void testResendInboundDataBufferToHostDelegation() {
assertFalse(xfer.resendInboundDataBufferToHost());
}
} }
@@ -0,0 +1,107 @@
package haus.nightmare.lib3270j.ft;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class CMSPrintXferTest {
@Test
public void testAsaCarriageControlTranslation() {
// ASA: ' ' = single space, '0' = double space, '-' = triple space, '1' = form feed, '+' = overstrike
String input = "1PAGE 1 HEADER\n" +
" --------------------\n" +
"0ITEM 1 DESCRIPTION\n" +
"-ITEM 2 AFTER 3 LINES\n" +
"+ITEM 2 UNDERLINED\n" +
"1PAGE 2 HEADER\n";
String expected = "\fPAGE 1 HEADER\n" +
"--------------------\n\n" +
"ITEM 1 DESCRIPTION\n\n\n" +
"ITEM 2 AFTER 3 LINES\r" +
"ITEM 2 UNDERLINED\n\f" +
"PAGE 2 HEADER";
String result = CMSPrintXfer.convertAsaCarriageControl(input);
assertEquals(expected, result);
}
@Test
public void testAsaCarriageControlBytesConversion() {
String input = " REPORT TITLE\n0SUBTITLE\n";
byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
byte[] converted = CMSPrintXfer.convertAsaCarriageControl(inputBytes, false);
String convertedStr = new String(converted, StandardCharsets.UTF_8);
assertEquals("REPORT TITLE\n\nSUBTITLE", convertedStr);
}
@Test
public void testMachineCarriageControlTranslation() {
// 0x09: Write and advance 1 line
// 0x11: Write and advance 2 lines
// 0x89: Write and skip to channel 1 (form feed)
byte[] input = new byte[] {
0x09, 'H', 'E', 'L', 'L', 'O', '\n',
0x11, 'W', 'O', 'R', 'L', 'D', '\n',
(byte) 0x89, 'N', 'E', 'X', 'T', ' ', 'P', 'A', 'G', 'E', '\n'
};
byte[] output = CMSPrintXfer.convertMachineCarriageControl(input);
String outputStr = new String(output, StandardCharsets.UTF_8);
assertTrue(outputStr.startsWith("HELLO\n"));
assertTrue(outputStr.contains("WORLD\n\n"));
assertTrue(outputStr.contains("NEXT PAGE\n\f"));
}
@Test
public void testParseQueryReaderOutput() {
String sampleRdrOutput =
"ORIGINID FILE CLASS RECORDS CPY HOLD DATE TIME NAME TYPE DIST\n" +
"MAINT 0042 A RDR 00000120 001 NONE 08/31 14:15:22 PROFILE EXEC SYSTEM\n" +
"OPERATOR 0099 B RDR 00000500 002 USER 08/31 14:20:00 PAYROLL DATA FINANCE\n";
List<CMSPrintXfer.SpoolFileEntry> entries = CMSPrintXfer.parseQueryReaderOutput(sampleRdrOutput);
assertEquals(2, entries.size());
CMSPrintXfer.SpoolFileEntry entry1 = entries.get(0);
assertEquals(42, entry1.getSpoolId());
assertEquals("MAINT", entry1.getOwner());
assertEquals("A", entry1.getSpoolClass());
assertEquals(120, entry1.getRecords());
assertEquals(1, entry1.getCopies());
assertEquals("NONE", entry1.getHoldStatus());
assertEquals("08/31", entry1.getDate());
assertEquals("14:15:22", entry1.getTime());
assertEquals("PROFILE", entry1.getFileName());
assertEquals("EXEC", entry1.getFileType());
assertEquals("RDR", entry1.getDeviceType());
CMSPrintXfer.SpoolFileEntry entry2 = entries.get(1);
assertEquals(99, entry2.getSpoolId());
assertEquals("OPERATOR", entry2.getOwner());
assertEquals("PAYROLL", entry2.getFileName());
assertEquals("DATA", entry2.getFileType());
}
@Test
public void testParseQueryPrinterOutput() {
String samplePrtOutput =
"ORIGINID FILE CLASS RECORDS CPY HOLD DATE TIME NAME TYPE DIST\n" +
"USER01 0105 A PRT 00000045 001 USER 08/31 15:00:00 REPORT LISTING OFFICE\n";
List<CMSPrintXfer.SpoolFileEntry> entries = CMSPrintXfer.parseQueryPrinterOutput(samplePrtOutput);
assertEquals(1, entries.size());
assertEquals(105, entries.get(0).getSpoolId());
assertEquals("USER01", entries.get(0).getOwner());
assertEquals("REPORT", entries.get(0).getFileName());
assertEquals("LISTING", entries.get(0).getFileType());
assertEquals("PRT", entries.get(0).getDeviceType());
}
}
@@ -80,4 +80,49 @@ public class FTConfigTest {
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand()); assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand());
} }
@Test
public void testParseOptionsComprehensive() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("MY.DATASET");
config.parseOptions("TSO ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(10,5) TRACKS REPLACE CODEPAGE(CP037) BUFFERSIZE(8192)");
assertEquals(FTConfig.HostType.TSO, config.getHostType());
assertEquals(FTConfig.TransferMode.ASCII, config.getTransferMode());
assertEquals(FTConfig.CrAction.REMOVE, config.getCrAction());
assertEquals(FTConfig.RecordFormat.FIXED, config.getRecfm());
assertEquals(80, config.getLrecl());
assertEquals(3120, config.getBlksize());
assertEquals(10, config.getPrimarySpace());
assertEquals(5, config.getSecondarySpace());
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
assertEquals(FTConfig.ExistAction.REPLACE, config.getExistAction());
assertEquals("CP037", config.getCodePage());
assertEquals(8192, config.getDftBufferSize());
String cmd = config.buildCommand();
assertTrue(cmd.contains("RECFM(F)"));
assertTrue(cmd.contains("LRECL(80)"));
assertTrue(cmd.contains("BLKSIZE(3120)"));
assertTrue(cmd.contains("SPACE(10,5)"));
assertTrue(cmd.contains("TRACKS"));
assertTrue(cmd.contains("REPLACE"));
}
@Test
public void testCicsCommandGeneration() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CICS);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("FILE01");
config.parseOptions("CICS BINARY NOCRLF REPLACE");
assertEquals(FTConfig.HostType.CICS, config.getHostType());
assertEquals(FTConfig.TransferMode.BINARY, config.getTransferMode());
assertEquals(FTConfig.ExistAction.REPLACE, config.getExistAction());
String cmd = config.buildCommand();
assertEquals("IND$FILE PUT FILE01 (BINARY NOCRLF REPLACE", cmd);
}
} }
@@ -500,4 +500,80 @@ public class FTDftTest {
assertArrayEquals(firstSent, retransmitted); assertArrayEquals(firstSent, retransmitted);
} }
@Test
public void testSendDataPacketDirectWithMtuSplitting() {
ftDft.setMTUSize(512);
assertEquals(512, ftDft.getMTUSize());
// Send a 1000-byte payload, which should be split into 3 structured field chunks (each max 512 - 27 = 485 bytes)
byte[] testPayload = new byte[1000];
for (int i = 0; i < testPayload.length; i++) {
testPayload[i] = (byte) (i & 0xFF);
}
ftDft.sendDataPacket(testPayload);
assertEquals(3, inputProcessor.sentStructuredFields.size());
assertEquals(1000, ftDft.getBytesTransferred());
// Verify that each structured field is SF_TRANSFER_DATA (0xD0) and has TR_GET_REPLY (0x4605)
for (byte[] sf : inputProcessor.sentStructuredFields) {
assertEquals(AID_SF, sf[0] & 0xFF);
assertEquals(SF_TRANSFER_DATA, sf[3] & 0xFF);
assertEquals(0x46, sf[4] & 0xFF);
assertEquals(0x05, sf[5] & 0xFF);
}
}
@Test
public void testCicsHostMessageHandling() {
// DFH0500 File transfer completed successfully
String successMsg = "DFH0500 File transfer completed successfully";
byte[] msgBytes = successMsg.getBytes(StandardCharsets.ISO_8859_1);
int totalPayloadLen = msgBytes.length + 5;
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
sfOut.write(0); sfOut.write(0);
sfOut.write(0xD0);
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
sfOut.write(FTConstants.TR_BEGIN_DATA);
sfOut.write((totalPayloadLen >> 8) & 0xFF);
sfOut.write(totalPayloadLen & 0xFF);
sfOut.write(msgBytes, 0, msgBytes.length);
byte[] insertData = sfOut.toByteArray();
int sfLen = insertData.length;
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 0xFF);
// Open message stream first
byte[] openReq = new byte[] {
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20
};
ftDft.processStructuredField(openReq, 0, openReq.length);
// Deliver success message
ftDft.processStructuredField(insertData, 0, insertData.length);
assertTrue(listener.completeCalled);
assertFalse(listener.abortCalled);
}
@Test
public void testResendInboundDataBufferToHost() {
assertFalse(ftDft.resendInboundDataBufferToHost());
byte[] payload = "TEST BUFFER".getBytes(StandardCharsets.UTF_8);
ftDft.sendDataPacket(payload);
assertEquals(1, inputProcessor.sentStructuredFields.size());
assertTrue(ftDft.resendInboundDataBufferToHost());
assertEquals(2, inputProcessor.sentStructuredFields.size());
}
} }
@@ -453,7 +453,7 @@ public class InputProcessorTest {
} }
@Test @Test
public void testSendAidAlwaysSendsStandard3270StreamEvenIfGraphicCursorActive() { public void testSendAidWhenGraphicCursorActiveSendsGraphicInputStructuredField() {
screen.erase(false); screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY)); screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1; // 'A' screen.getCell(1).ec = (byte) 0xC1; // 'A'
@@ -462,6 +462,55 @@ public class InputProcessorTest {
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600); haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane); haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(true); goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(100, -50);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sent.set(data);
}
};
input.setGocaDecoder(goca);
input.sendAid(AID_ENTER);
byte[] result = sent.get();
assertNotNull(result);
// Format: AID_SF (1) + SF (56) + AID_ENTER (1) + Cursor Addr (2) + SBA (1) + Field Addr (2) + Data 'A' (1) = 64 bytes
assertEquals(64, result.length);
assertEquals((byte) AID_SF, result[0]);
assertEquals(0x00, result[1]);
assertEquals(0x34, result[2]); // SF len = 52
assertEquals(0x0F, result[3]);
assertEquals(0x0F, result[4]);
// Coordinates in SF at index 1 + 24 = 25
int gx = (result[25] << 8) | (result[26] & 0xFF);
int gy = (result[27] << 8) | (result[28] & 0xFF);
assertEquals(100, (short) gx);
assertEquals(-50, (short) gy);
// Keyboard AID trigger class constant (0x07) at index 1 + 31 = 32
assertEquals(0x07, result[32]);
assertEquals(0x07, result[34]);
assertEquals((byte) 0xFF, result[35]);
assertEquals((byte) AID_ENTER, result[36]);
// Trailing AID at index 57
assertEquals((byte) AID_ENTER, result[57]);
assertEquals((byte) ORDER_SBA, result[60]);
assertEquals((byte) 0xC1, result[63]);
}
@Test
public void testSendAidWhenGraphicCursorNotActiveSendsStandard3270Stream() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1; // 'A'
screen.setCursorAddress(2);
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(false);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>(); java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) { InputProcessor input = new InputProcessor(screen, translator, null) {
@@ -0,0 +1,384 @@
package haus.nightmare.lib3270j.nvt;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive Unit Tests for Phase 9: NVT Mode (VT100 / VT220 / VT320 / ANSI Emulation).
*/
public class NvtProcessorPhase9Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private NvtProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); // 24x80
processor = new NvtProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
private void feed(String s) {
byte[] b = s.getBytes(StandardCharsets.US_ASCII);
processor.processNVTData(b, 0, b.length);
}
private void feedUtf8(String s) {
byte[] b = s.getBytes(StandardCharsets.UTF_8);
processor.processNVTData(b, 0, b.length);
}
@Test
public void testDeviceStatusReportExtendedDSR() {
output.reset();
// DSR 5 (Status OK)
feed("\u001B[5n");
assertEquals("\u001B[0n", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR 6 (Cursor Position Report) -> at row 10, col 20 (0-indexed: 9, 19)
feed("\u001B[10;20H");
feed("\u001B[6n");
assertEquals("\u001B[10;20R", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?6 (DECXCPR Extended Cursor Position Report)
feed("\u001B[?6n");
assertEquals("\u001B[?10;20;1R", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?15 (Printer status)
feed("\u001B[?15n");
assertEquals("\u001B[?13n", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?26 (Keyboard dialect status)
feed("\u001B[?26n");
assertEquals("\u001B[?27;1n", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?62 / ?63 (Macro/Memory Checksum)
feed("\u001B[?62n");
assertEquals("\u001B[?63;0n", output.toString(StandardCharsets.US_ASCII));
}
@Test
public void testDeviceAttributesPrimaryAndSecondaryDA() {
output.reset();
// Default Primary DA (VT100 with AVO): ESC [ c
feed("\u001B[c");
assertEquals("\u001B[?1;2c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// Configure VT220 Primary DA
processor.setPrimaryDeviceAttributes("\u001B[?62;1;2;6;7;8;9c");
feed("\u001B[c");
assertEquals("\u001B[?62;1;2;6;7;8;9c", output.toString(StandardCharsets.US_ASCII));
output.reset();
feed("\u001B[0c");
assertEquals("\u001B[?62;1;2;6;7;8;9c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// Secondary DA: ESC [ > c
feed("\u001B[>c");
assertEquals("\u001B[>1;10;0c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// Secondary DA: ESC [ > 0 c
feed("\u001B[>0c");
assertEquals("\u001B[>1;10;0c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DECREQTPARM: ESC [ 1 x
feed("\u001B[1x");
assertEquals("\u001B[3;1;1;120;120;1;0x", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DECREQTPARM: ESC [ 0 x
feed("\u001B[0x");
assertEquals("\u001B[2;1;1;120;120;1;0x", output.toString(StandardCharsets.US_ASCII));
}
@Test
public void testDecSoftTerminalResetDECSTR() {
// Set non-standard modes
processor.setCursorVisible(false);
processor.setApplicationKeypad(true);
processor.setApplicationCursorKeys(true);
processor.setOriginMode(true);
processor.setAutoWrap(false);
processor.setInsertMode(true);
// Set scroll region 3..10
feed("\u001B[3;10r");
// Soft Reset: ESC [ ! p
feed("\u001B[!p");
assertTrue(processor.isCursorVisible());
assertFalse(processor.isApplicationKeypad());
assertFalse(processor.isApplicationCursorKeys());
assertFalse(processor.isOriginMode());
assertTrue(processor.isAutoWrap());
assertFalse(processor.isInsertMode());
}
@Test
public void testOriginModeDECOM() {
// Set scroll region lines 5..15: ESC [ 5 ; 15 r
feed("\u001B[5;15r");
// Enable Origin Mode: ESC [ ? 6 h
feed("\u001B[?6h");
assertTrue(processor.isOriginMode());
// Move to line 1, col 1 in origin mode -> corresponds to row 5 (0-indexed: 4), col 0
feed("\u001B[1;1H");
assertEquals(4 * 80, screen.getCursorAddress());
// Move to line 3, col 10 in origin mode -> corresponds to row 7 (0-indexed: 6), col 9
feed("\u001B[3;10H");
assertEquals(6 * 80 + 9, screen.getCursorAddress());
// Check DSR extended CPR in origin mode: should report row 3, col 10 relative to top margin
output.reset();
feed("\u001B[?6n");
assertEquals("\u001B[?3;10;1R", output.toString(StandardCharsets.US_ASCII));
// Disable Origin Mode: ESC [ ? 6 l
feed("\u001B[?6l");
assertFalse(processor.isOriginMode());
// Move to line 1, col 1 in absolute mode -> row 0, col 0
feed("\u001B[1;1H");
assertEquals(0, screen.getCursorAddress());
}
@Test
public void testCharacterSetSCSandShifts() {
// Designate G0 as UK (ESC ( A), G1 as Line Drawing (ESC ) 0), G2 as German (ESC * K)
feed("\u001B(A\u001B)0\u001B*K");
assertEquals('A', processor.getG0Charset());
assertEquals('0', processor.getG1Charset());
assertEquals('K', processor.getG2Charset());
// In G0 (UK): '#' maps to '£'
feed("\u001B[1;1H#");
assertEquals('£', screen.getCell(0).ucs4);
// Shift-Out (0x0E -> G1 Line Drawing)
processor.processNVTData(new byte[] { 0x0E, (byte) 'q' }, 0, 2);
assertEquals('─', screen.getCell(1).ucs4);
// Shift-In (0x0F -> G0 UK)
processor.processNVTData(new byte[] { 0x0F, (byte) '#' }, 0, 2);
assertEquals('£', screen.getCell(2).ucs4);
// Single Shift SS2 (ESC N -> G2 German for next character only)
// In German: '@' -> '§', '{' -> 'ä'
feed("\u001BN@#");
assertEquals('§', screen.getCell(3).ucs4); // Single shift applied to '@'
assertEquals('£', screen.getCell(4).ucs4); // Returned to G0 UK for '#'
// Locking Shift LS2 (ESC n -> G2 German active)
feed("\u001Bn{|}~");
assertEquals('ä', screen.getCell(5).ucs4);
assertEquals('ö', screen.getCell(6).ucs4);
assertEquals('ü', screen.getCell(7).ucs4);
assertEquals('ß', screen.getCell(8).ucs4);
}
@Test
public void testOscWindowTitleAndIcon() {
AtomicReference<String> titleRef = new AtomicReference<>("");
processor.addTitleListener(titleRef::set);
// OSC 0 (BEL terminated): ESC ] 0 ; Main Terminal Window BEL
feedUtf8("\u001B]0;Main Terminal Window\u0007");
assertEquals("Main Terminal Window", processor.getTerminalTitle());
assertEquals("Main Terminal Window", titleRef.get());
// OSC 2 (ST terminated): ESC ] 2 ; IBM Host Session ESC \
feedUtf8("\u001B]2;IBM Host Session\u001B\\");
assertEquals("IBM Host Session", processor.getTerminalTitle());
assertEquals("IBM Host Session", titleRef.get());
}
@Test
public void testOsc52ClipboardReadAndWrite() {
AtomicReference<String> clipboardStorage = new AtomicReference<>("Initial Clipboard");
processor.setClipboardHandler(new NvtProcessor.ClipboardHandler() {
@Override
public String getClipboardText() {
return clipboardStorage.get();
}
@Override
public void setClipboardText(String text) {
clipboardStorage.set(text);
}
});
// OSC 52 Write: Base64 of "Hello from Host" is "SGVsbG8gZnJvbSBIb3N0"
feed("\u001B]52;c;SGVsbG8gZnJvbSBIb3N0\u0007");
assertEquals("Hello from Host", clipboardStorage.get());
// OSC 52 Read Query: ESC ] 52 ; c ; ? BEL
output.reset();
feed("\u001B]52;c;?\u0007");
String expectedB64 = java.util.Base64.getEncoder().encodeToString("Hello from Host".getBytes(StandardCharsets.UTF_8));
String response = output.toString(StandardCharsets.US_ASCII);
assertTrue(response.contains(expectedB64));
assertTrue(response.startsWith("\u001B]52;c;"));
}
@Test
public void testApplicationKeypadModeDECKPAM() {
assertFalse(processor.isApplicationKeypad());
// Enable Keypad Application Mode: ESC =
feed("\u001B=");
assertTrue(processor.isApplicationKeypad());
// Test keypad mapping
assertEquals("\u001BOp", processor.mapKeypadKey(0x60)); // NUMPAD0
assertEquals("\u001BOq", processor.mapKeypadKey(0x61)); // NUMPAD1
assertEquals("\u001BOr", processor.mapKeypadKey(0x62)); // NUMPAD2
assertEquals("\u001BOs", processor.mapKeypadKey(0x63)); // NUMPAD3
assertEquals("\u001BOt", processor.mapKeypadKey(0x64)); // NUMPAD4
assertEquals("\u001BOu", processor.mapKeypadKey(0x65)); // NUMPAD5
assertEquals("\u001BOv", processor.mapKeypadKey(0x66)); // NUMPAD6
assertEquals("\u001BOw", processor.mapKeypadKey(0x67)); // NUMPAD7
assertEquals("\u001BOx", processor.mapKeypadKey(0x68)); // NUMPAD8
assertEquals("\u001BOy", processor.mapKeypadKey(0x69)); // NUMPAD9
assertEquals("\u001BOm", processor.mapKeypadKey(0x6D)); // MINUS
assertEquals("\u001BOn", processor.mapKeypadKey(0x6E)); // DECIMAL
assertEquals("\u001BOM", processor.mapKeypadKey(0x0A)); // ENTER
byte[] bytes = processor.processApplicationKeypad(0x60);
assertNotNull(bytes);
assertEquals("\u001BOp", new String(bytes, StandardCharsets.US_ASCII));
// Enable Keypad Numeric Mode: ESC >
feed("\u001B>");
assertFalse(processor.isApplicationKeypad());
assertNull(processor.mapKeypadKey(0x60));
assertNull(processor.processApplicationKeypad(0x60));
}
@Test
public void testApplicationCursorKeysModeDECCKM() {
assertFalse(processor.isApplicationCursorKeys());
assertEquals("\u001B[A", processor.mapKey(0x26, '\0', false, false, false)); // VK_UP normal
assertEquals("\u001B[B", processor.mapKey(0x28, '\0', false, false, false)); // VK_DOWN normal
assertEquals("\u001B[C", processor.mapKey(0x27, '\0', false, false, false)); // VK_RIGHT normal
assertEquals("\u001B[D", processor.mapKey(0x25, '\0', false, false, false)); // VK_LEFT normal
// Enable Application Cursor Keys: ESC [ ? 1 h
feed("\u001B[?1h");
assertTrue(processor.isApplicationCursorKeys());
assertEquals("\u001BOA", processor.mapKey(0x26, '\0', false, false, false)); // VK_UP application
assertEquals("\u001BOB", processor.mapKey(0x28, '\0', false, false, false)); // VK_DOWN application
assertEquals("\u001BOC", processor.mapKey(0x27, '\0', false, false, false)); // VK_RIGHT application
assertEquals("\u001BOD", processor.mapKey(0x25, '\0', false, false, false)); // VK_LEFT application
// Disable Application Cursor Keys: ESC [ ? 1 l
feed("\u001B[?1l");
assertFalse(processor.isApplicationCursorKeys());
assertEquals("\u001B[A", processor.mapKey(0x26, '\0', false, false, false));
}
@Test
public void testFunctionAndEditingKeyMapping() {
// Function keys F1..F4
assertEquals("\u001BOP", processor.mapKey(0x70, '\0', false, false, false)); // F1
assertEquals("\u001BOQ", processor.mapKey(0x71, '\0', false, false, false)); // F2
assertEquals("\u001BOR", processor.mapKey(0x72, '\0', false, false, false)); // F3
assertEquals("\u001BOS", processor.mapKey(0x73, '\0', false, false, false)); // F4
// Function keys F5..F12
assertEquals("\u001B[15~", processor.mapKey(0x74, '\0', false, false, false)); // F5
assertEquals("\u001B[17~", processor.mapKey(0x75, '\0', false, false, false)); // F6
assertEquals("\u001B[24~", processor.mapKey(0x7B, '\0', false, false, false)); // F12
// Editing keys
assertEquals("\u001B[2~", processor.mapKey(0x9B, '\0', false, false, false)); // Insert
assertEquals("\u001B[3~", processor.mapKey(0x7F, '\0', false, false, false)); // Delete
assertEquals("\u001B[1~", processor.mapKey(0x24, '\0', false, false, false)); // Home
assertEquals("\u001B[4~", processor.mapKey(0x23, '\0', false, false, false)); // End
assertEquals("\u001B[5~", processor.mapKey(0x21, '\0', false, false, false)); // PageUp
assertEquals("\u001B[6~", processor.mapKey(0x22, '\0', false, false, false)); // PageDown
}
@Test
public void testInsertModeAndNewLineMode() throws IOException {
// Line Feed in standard mode: cursor moves down without returning to col 0
processor.setNewLineMode(false);
feed("\u001B[1;10H\n");
assertEquals(1 * 80 + 9, screen.getCursorAddress()); // Row 2, Col 10
// Enable New Line Mode: ESC [ 20 h
feed("\u001B[20h");
assertTrue(processor.isNewLineMode());
feed("\u001B[1;10H\n");
assertEquals(1 * 80, screen.getCursorAddress()); // Row 2, Col 0 (CR + LF)
// Insert Mode: ESC [ 4 h
feed("\u001B[1;1HAC");
feed("\u001B[1;2H"); // Move cursor between A and C
feed("\u001B[4h"); // Enable Insert Mode
assertTrue(processor.isInsertMode());
feed("B");
assertEquals('A', screen.getCell(0).ucs4);
assertEquals('B', screen.getCell(1).ucs4);
assertEquals('C', screen.getCell(2).ucs4);
// Reset Insert Mode: ESC [ 4 l
feed("\u001B[4l");
assertFalse(processor.isInsertMode());
}
@Test
public void testAutoWrapAndColumnMode() {
// Disable Auto Wrap: ESC [ ? 7 l
feed("\u001B[?7l");
assertFalse(processor.isAutoWrap());
// Move to col 79 (0-indexed: 78) on line 1, and write 5 characters
feed("\u001B[1;79H12345");
// Characters should truncate / overwrite at margin (col 79) and not wrap to next row
assertEquals('5', screen.getCell(79).ucs4);
assertEquals(79, screen.getCursorAddress());
// Enable Auto Wrap: ESC [ ? 7 h
feed("\u001B[?7h");
assertTrue(processor.isAutoWrap());
// 132-Column Mode switch: ESC [ ? 3 h (clears screen)
feed("\u001B[?3h");
assertEquals(0, screen.getCursorAddress());
assertEquals(0, screen.getCell(0).ucs4);
}
}
@@ -0,0 +1,136 @@
package haus.nightmare.lib3270j.printer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for Phase 7 Printer Definition Table (PDT) profiles, control sequence generation,
* character mappings, and GDDM host escapes in PD3270.
*/
public class PD3270PDTPhase7Test {
private PrinterConfig config;
private PD3270 pd;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
pd = new PD3270(config);
pd.openPrinter(null);
}
@Test
public void testBuiltinPDTPresets() {
// Plain Text
PrinterDefinitionTable textPdt = PrinterDefinitionTable.createPlainTextPDT();
assertEquals("PLAIN_TEXT", textPdt.getName());
assertTrue(textPdt.hasControlCode(PrinterDefinitionTable.CMD_PAGE_FEED));
// HP PCL 5
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
assertEquals("PCL_5", pcl5.getName());
assertTrue(pcl5.hasControlCode(PrinterDefinitionTable.CMD_START_BOLD));
assertArrayEquals("\033(s3B".getBytes(), pcl5.getControlCode(PrinterDefinitionTable.CMD_START_BOLD));
// Epson ESC/P
PrinterDefinitionTable escp = PrinterDefinitionTable.createEpsonEscPPDT();
assertEquals("EPSON_ESC_P", escp.getName());
assertTrue(escp.hasControlCode(PrinterDefinitionTable.CMD_START_BOLD));
assertArrayEquals("\033E".getBytes(), escp.getControlCode(PrinterDefinitionTable.CMD_START_BOLD));
// PostScript
PrinterDefinitionTable ps = PrinterDefinitionTable.createPostScriptPDT();
assertEquals("POSTSCRIPT", ps.getName());
assertTrue(ps.hasControlCode(PrinterDefinitionTable.CMD_START_JOB));
}
@Test
public void testPcl5ControlCodeGeneration() {
pd.setPDT(PrinterDefinitionTable.createPcl5PDT());
pd.startJob();
pd.setBold(true);
pd.print("BOLD TEXT");
pd.setBold(false);
pd.setUnderline(true);
pd.print("UNDERLINED");
pd.setUnderline(false);
pd.setItalic(true);
pd.print("ITALIC");
pd.setItalic(false);
pd.setCPI(12);
pd.setLPI(8);
pd.formFeed();
pd.endJob();
String captured = pd.getCapturedText();
assertTrue(captured.contains("\033E")); // Reset / start job
assertTrue(captured.contains("\033(s3B")); // Bold on
assertTrue(captured.contains("\033(s0B")); // Bold off
assertTrue(captured.contains("\033&d0D")); // Underline on
assertTrue(captured.contains("\033(s1S")); // Italic on
assertTrue(captured.contains("\033&k2S")); // 12 CPI
assertTrue(captured.contains("\033&l8D")); // 8 LPI
assertTrue(captured.contains("\f")); // Page feed
}
@Test
public void testEpsonEscPControlCodeGeneration() {
pd.setPDT(PrinterDefinitionTable.createEpsonEscPPDT());
pd.startJob();
pd.setDoubleStrike(true);
pd.print("DOUBLE STRIKE");
pd.setDoubleStrike(false);
pd.setDoubleWidth(true);
pd.print("DOUBLE WIDTH");
pd.setDoubleWidth(false);
pd.setSuperscript(true);
pd.print("SUPER");
pd.setSuperscript(false);
pd.setSubscript(true);
pd.print("SUB");
pd.setSubscript(false);
pd.setCPI(10);
pd.setLPI(6);
pd.endJob();
String captured = pd.getCapturedText();
assertTrue(captured.contains("\033@")); // Init
assertTrue(captured.contains("\033G")); // Double strike on
assertTrue(captured.contains("\033H")); // Double strike off
assertTrue(captured.contains("\033W1")); // Double width on
assertTrue(captured.contains("\033W0")); // Double width off
assertTrue(captured.contains("\033S0")); // Superscript on
assertTrue(captured.contains("\033S1")); // Subscript on
assertTrue(captured.contains("\033P")); // 10 CPI
assertTrue(captured.contains("\0332")); // 6 LPI
}
@Test
public void testCustomPDTWithCharacterOverrides() {
PrinterDefinitionTable customPdt = new PrinterDefinitionTable("CUSTOM", "Custom Test Table");
customPdt.setControlCode("TEST_CMD", "[CMD]");
customPdt.setCharOverride('@', "[AT]".getBytes());
pd.setPDT(customPdt);
pd.writeControlCode("TEST_CMD");
pd.print('@');
pd.print("user@ibm.com");
String captured = pd.getCapturedText();
assertTrue(captured.contains("[CMD]"));
assertTrue(captured.contains("[AT]"));
}
@Test
public void testGddmHostPrintEscapePassthrough() {
byte[] gddmEscape = new byte[]{0x1B, '&', 'a', '1', '0', '0', 'V'};
pd.processGddmEscape(gddmEscape, 0, gddmEscape.length);
byte[] captured = pd.getCapturedBytes();
assertTrue(captured.length >= gddmEscape.length);
}
}
@@ -0,0 +1,100 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for Phase 7 PrintPS3270 EOJ completion, auto-flush timer, and 3270 printer data stream formatting.
*/
public class PrintPS3270Phase7Test {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private PrintPS3270 printPs;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
config.setAutoFlushTimeoutMs(150); // Fast timeout for testing
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
printPs = new PrintPS3270(config, pd, translator);
pd.openPrinter(null);
}
@Test
public void testProcessPrintCompleteEOJ() {
config.setFormFeedAtEoj(true);
config.setAutoFlushOnEoj(true);
byte[] ebcText = translator.stringToEbcdic("LU3 TEST PAGE");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(DS3270Constants.CMD_WRITE);
stream.write(0x00); // WCC (Start Print = false)
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40); // Row 0 Col 0
stream.write(0x40);
stream.write(ebcText, 0, ebcText.length);
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
// Process EOJ completion
printPs.processPrintComplete();
String captured = pd.getCapturedText();
assertTrue(captured.contains("LU3 TEST PAGE"));
assertEquals(1, pd.getPageCount());
}
@Test
public void testAutoFlushTimerTriggersOnIdle() throws InterruptedException {
byte[] ebcText = translator.stringToEbcdic("AUTO FLUSH DATA");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(DS3270Constants.CMD_WRITE);
stream.write(0x00); // WCC with Start Print = 0
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40);
stream.write(0x40);
stream.write(ebcText, 0, ebcText.length);
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
// Wait for auto-flush timer (150ms) to trigger
Thread.sleep(300);
String captured = pd.getCapturedText();
assertTrue(captured.contains("AUTO FLUSH DATA"));
}
@Test
public void testStartPrintBitImmediateFlush() {
byte[] ebcText = translator.stringToEbcdic("IMMEDIATE PRINT");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(DS3270Constants.CMD_WRITE);
stream.write(PrinterConstants.WCC_START_PRINT_BIT); // WCC with Start Print = 1
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40);
stream.write(0x40);
stream.write(ebcText, 0, ebcText.length);
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
String captured = pd.getCapturedText();
assertTrue(captured.contains("IMMEDIATE PRINT"));
}
@Test
public void testCancelAutoFlush() {
printPs.scheduleAutoFlush();
printPs.cancelAutoFlush();
// Cancel should succeed without exception
assertNotNull(printPs);
}
}
@@ -0,0 +1,238 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive tests for Phase 7 SCS (SNA Character String) 28-command protocol engine,
* Enhanced Highlights, Vertical Channel Select (VCS), Graphic Error Action (GEA),
* Index Return (IRS), and Transparent data (TRN).
*/
public class PrintSCS3270Phase7Test {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private PrintSCS3270 scs;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
scs = new PrintSCS3270(config, pd, translator);
pd.openPrinter(null); // In-memory capture
}
@Test
public void testAll28SCSCommandsDispatch() {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// 1. NUL (0x00) & NOP (0x03)
stream.write(PrinterConstants.SCS_NUL);
stream.write(PrinterConstants.SCS_NOP);
// 2. Text "A" (0xC1 in Cp037)
stream.write(0xC1);
// 3. BS (0x16) & NBS (0x36)
stream.write(PrinterConstants.SCS_BS);
stream.write(PrinterConstants.SCS_NBS);
// 4. SP (0x40) & RSP (0x41)
stream.write(PrinterConstants.SCS_SP);
stream.write(PrinterConstants.SCS_RSP);
// 5. HT (0x05)
stream.write(PrinterConstants.SCS_HT);
// 6. Text "B" (0xC2)
stream.write(0xC2);
// 7. CR (0x0D) & RCR (0x07)
stream.write(PrinterConstants.SCS_CR);
stream.write(PrinterConstants.SCS_RCR);
// 8. LF (0x25) & NL (0x15) & RNL (0x06)
stream.write(PrinterConstants.SCS_LF);
stream.write(PrinterConstants.SCS_NL);
stream.write(PrinterConstants.SCS_RNLS);
// 9. VT (0x0B)
stream.write(PrinterConstants.SCS_VT);
// 10. SO (0x0E) & SI (0x0F)
stream.write(PrinterConstants.SCS_SO);
stream.write(PrinterConstants.SCS_SI);
// 11. INP (0x24) & ENP (0x14)
stream.write(PrinterConstants.SCS_INP);
stream.write(0xC3); // Should be ignored when presentation inhibited
stream.write(PrinterConstants.SCS_ENP);
stream.write(0xC3); // 'C'
// 12. BEL (0x2F) & POC (0x17)
stream.write(PrinterConstants.SCS_BEL);
stream.write(PrinterConstants.SCS_POC);
// 13. IRS (0x33)
stream.write(PrinterConstants.SCS_IRS);
// 14. SUB (0x3F)
stream.write(PrinterConstants.SCS_SUB);
// 15. FF (0x0C)
stream.write(PrinterConstants.SCS_FF);
byte[] payload = stream.toByteArray();
scs.processRecord(payload);
String captured = pd.getCapturedText();
assertNotNull(captured);
assertTrue(captured.contains("C"));
assertTrue(captured.contains("?"));
assertTrue(pd.getPageCount() >= 1);
}
@Test
public void testProcessTRNRawBytePassthrough() {
// TRN order: 0x35 <len> <rawBytes>
byte[] rawPrinterCodes = new byte[]{0x1B, 0x45, 'R', 'A', 'W', 0x1B, 0x46};
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(PrinterConstants.SCS_TRN);
stream.write(rawPrinterCodes.length);
stream.write(rawPrinterCodes, 0, rawPrinterCodes.length);
scs.processHostData(stream.toByteArray(), 0, stream.size());
byte[] captured = pd.getCapturedBytes();
assertTrue(captured.length >= rawPrinterCodes.length);
// Also test direct processTRN method
byte[] directBytes = new byte[]{0x1B, 0x2A, 0x62, 0x30, 0x57};
scs.processTRN(directBytes);
assertTrue(pd.getByteCount() >= rawPrinterCodes.length + directBytes.length);
}
@Test
public void testProcessVCSWithChannelTape() {
// Setup channel tape: Channel 2 = line 15, Channel 3 = line 30
scs.setChannelLine(2, 15);
scs.setChannelLine(3, 30);
assertEquals(15, scs.getChannelLine(2));
assertEquals(30, scs.getChannelLine(3));
// Start at line 1
assertEquals(1, scs.getCurrentRow());
// Process VCS for Channel 2
scs.processVCS(2);
assertEquals(15, scs.getCurrentRow());
// Process VCS for Channel 3
scs.processVCS(3);
assertEquals(30, scs.getCurrentRow());
// Process VCS for Channel 1 (line 1, which is < 30, so forms feed to next page)
int initialPages = pd.getPageCount();
scs.processVCS(1);
assertEquals(1, scs.getCurrentRow());
assertEquals(initialPages + 1, pd.getPageCount());
}
@Test
public void testProcessGEAGraphicErrorActions() {
// 1. GEA Default: replacement '?'
config.setGraphicErrorAction(PrinterConstants.GEA_NO_SUBSTITUTION);
scs.processSUB();
scs.flushLineBuffer();
assertTrue(pd.getCapturedText().contains("?"));
// 2. GEA Substitute specified character ('#')
pd.resetCapture();
scs.processGEA(PrinterConstants.GEA_SUBSTITUTE_SPECIFIED, '#');
scs.processSUB();
scs.flushLineBuffer();
assertTrue(pd.getCapturedText().contains("#"));
// 3. GEA 0x2B 0xC8 order
pd.resetCapture();
byte[] geaOrder = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_GEA,
0x02,
PrinterConstants.GEA_SUBSTITUTE_SPECIFIED,
(byte) '*'
};
scs.processHostData(geaOrder, 0, geaOrder.length);
scs.processSUB();
scs.flushLineBuffer();
assertTrue(pd.getCapturedText().contains("*"));
}
@Test
public void testEnhancedHighlightAttributes() {
// Set PDT to PCL5 so we can verify escape sequences generated
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
pd.setPDT(pcl5);
// Test Emphasized (Bold)
scs.setEnhancedHighlight(PrinterConstants.SEAC_EMPHASIZED);
assertEquals(PrinterConstants.SEAC_EMPHASIZED, scs.getActiveHighlight());
String text = pd.getCapturedText();
assertTrue(text.contains("\033(s3B")); // PCL Bold on
// Test Italic
pd.resetCapture();
scs.setEnhancedHighlight(PrinterConstants.SEAC_ITALIC);
assertEquals(PrinterConstants.SEAC_ITALIC, scs.getActiveHighlight());
text = pd.getCapturedText();
assertTrue(text.contains("\033(s1S")); // PCL Italic on
// Test Underline
pd.resetCapture();
scs.setEnhancedHighlight(PrinterConstants.SEAC_UNDERLINE);
assertEquals(PrinterConstants.SEAC_UNDERLINE, scs.getActiveHighlight());
text = pd.getCapturedText();
assertTrue(text.contains("\033&d0D")); // PCL Underline on
// Test Double-Width
pd.resetCapture();
scs.startDoubleWidthCharacters();
assertTrue(scs.isDoubleWidth());
text = pd.getCapturedText();
assertTrue(text.contains("\033&k1W"));
scs.endDoubleWidthCharacters();
assertFalse(scs.isDoubleWidth());
}
@Test
public void testExplicitSCSNamedMethods() {
scs.processNL();
scs.processCR();
scs.processLF();
scs.processFF();
scs.processBS();
scs.processNBS();
scs.processHT();
scs.processVT();
scs.processRNL();
scs.processRCR();
scs.processENP();
scs.processINP();
scs.processBEL();
scs.processIRS();
scs.processSO();
scs.processSI();
scs.processSCS(1);
scs.processPOC(new byte[]{0x01});
assertTrue(pd.getPageCount() >= 1);
}
}
@@ -0,0 +1,159 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* End-to-end tests for Phase 7 Telnet3270EP printer protocol engine, LU1 and LU3 sessions,
* response headers, EOJ handling, listeners, and Telnet3270EPClient integration.
*/
public class Telnet3270EPPhase7FullTest {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private Telnet3270EP ep;
private Telnet3270EPClient client;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23, "PRT01");
config.setAssociatedDisplayLuName("DSP01");
config.setFormFeedAtEoj(true);
config.setAutoFlushOnEoj(true);
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
ep = new Telnet3270EP(config, pd, translator);
client = new Telnet3270EPClient(config);
pd.openPrinter(null);
}
@Test
public void testLU1SCSPrintSessionFlow() {
// 1. Process BIND for LU Type 1 (SCS)
ep.process_bind(PrinterConstants.LU_TYPE_1_SCS);
assertEquals(PrinterConstants.LU_TYPE_1_SCS, ep.getActiveLuType());
assertEquals(PrinterConstants.STATUS_PRINTER_READY, ep.getStatusCode());
// 2. Feed SCS print records
byte[] hello = translator.stringToEbcdic("REPORT TITLE");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(hello, 0, hello.length);
stream.write(PrinterConstants.SCS_NL);
byte[] data = translator.stringToEbcdic("LINE ITEM 1");
stream.write(data, 0, data.length);
stream.write(PrinterConstants.SCS_NL);
byte[] payload = stream.toByteArray();
ep.getSCS().processHostData(payload, 0, payload.length);
// 3. EOJ
ep.sendEOJ(true);
String text = pd.getCapturedText();
assertTrue(text.contains("REPORT TITLE"));
assertTrue(text.contains("LINE ITEM 1"));
assertEquals(1, pd.getPageCount());
assertEquals(PrinterConstants.STATUS_JOB_COMPLETE, ep.getStatusCode());
}
@Test
public void testLU33270PrintSessionFlow() {
// 1. Process BIND for LU Type 3 (3270 Data Stream)
ep.process_bind(PrinterConstants.LU_TYPE_3_DS);
assertEquals(PrinterConstants.LU_TYPE_3_DS, ep.getActiveLuType());
// 2. Feed 3270 print DS
byte[] hello = translator.stringToEbcdic("LU3 REPORT CONTENT");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(0xF5); // ERASE WRITE
stream.write(0x00); // WCC
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40);
stream.write(0x40);
stream.write(hello, 0, hello.length);
byte[] payload = stream.toByteArray();
ep.getPrintPS().process3270PrintDS(payload, 0, payload.length);
// 3. EOJ
ep.sendEOJ(true);
String text = pd.getCapturedText();
assertTrue(text.contains("LU3 REPORT CONTENT"));
assertEquals(1, pd.getPageCount());
}
@Test
public void testClientFacadePDTAndChannelConfiguration() {
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
client.setPDT(pcl5);
assertEquals("PCL_5", client.getPDT().getName());
client.setChannelLine(4, 25);
assertEquals(25, client.getChannelLine(4));
byte[] text = translator.stringToEbcdic("CLIENT CONVENIENCE TEST");
client.printDirectBytes(text, 0, text.length);
client.flush();
String captured = client.getPD().getCapturedText();
assertTrue(captured.contains("CLIENT CONVENIENCE TEST"));
}
@Test
public void testPrintSessionEventListeners() {
AtomicBoolean jobStarted = new AtomicBoolean(false);
AtomicBoolean jobComplete = new AtomicBoolean(false);
AtomicInteger pagesReceived = new AtomicInteger(0);
PrintSessionListener listener = new PrintSessionListener() {
@Override
public void onPrintJobStarted(PrintSessionEvent event) {
jobStarted.set(true);
}
@Override
public void onPrintJobData(PrintSessionEvent event) {
}
@Override
public void onPrintJobPageComplete(PrintSessionEvent event) {
pagesReceived.incrementAndGet();
}
@Override
public void onPrintJobComplete(PrintSessionEvent event) {
jobComplete.set(true);
}
@Override
public void onPrinterStatusChanged(PrintSessionEvent event) {
}
@Override
public void onPrinterError(PrintSessionEvent event) {
}
};
ep.addPrintListener(listener);
ep.firePrintJobStarted("JOB01");
ep.firePrintJobPageComplete(1);
ep.sendEOJ(true);
assertTrue(jobStarted.get());
assertTrue(jobComplete.get());
assertEquals(1, pagesReceived.get());
ep.removePrintListener(listener);
}
}