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
@@ -59,4 +59,32 @@ public enum ConnectionState {
public boolean isFullSession() {
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.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() {
@Override public void onScreenUpdated() { xfer.onScreenUpdated(); }
@Override public void onScreenSizeChanged(int rows, int cols) {}
@Override public void onSoundAlarm() {}
@Override public void onScreenUpdated() {
xfer.onScreenUpdated();
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();
}
/**
* 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.
*/
@@ -101,6 +157,35 @@ public class Telnet3270Client {
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).
*/
@@ -114,11 +199,20 @@ public class Telnet3270Client {
fsm.addConnectionListener(l);
}
public void removeConnectionListener(ConnectionListener l) {
fsm.removeConnectionListener(l);
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
fsm.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) {
fsm.addSCSInboundListener(l);
}
@@ -198,6 +292,37 @@ public class Telnet3270Client {
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 ==========
/** Type a character at the cursor position. */
@@ -106,6 +106,19 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override
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 "";
StringBuilder sb = new StringBuilder(length);
@@ -117,9 +130,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int b = ebcdic[i] & 0xFF;
if (b == SO) {
inDBCS = true;
if (preserveSOSI) {
sb.append((char) SO);
}
i++;
} else if (b == SI) {
inDBCS = false;
if (preserveSOSI) {
sb.append((char) SI);
}
i++;
} else if (inDBCS) {
if (i + 1 < end) {
@@ -127,13 +146,22 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
if (b2 == SI) {
// Orphaned single byte before SI
inDBCS = false;
if (preserveSOSI) {
sb.append(ebcdicToUnicode(b));
sb.append((char) SI);
}
i += 2;
} else if (b2 == SO) {
// Unmatched byte followed by another SO
sb.append(ebcdicToUnicode(b));
i++;
} else {
sb.append(dbcsToUnicode(b, b2));
i += 2;
}
} else {
// Trailing byte
// Trailing single byte inside unclosed DBCS shift
sb.append(ebcdicToUnicode(b));
i++;
}
} else {
@@ -147,14 +175,39 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override
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];
// Manual state machine encoding
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
boolean inDBCS = false;
for (int i = 0; i < s.length(); 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);
if (dbcsCode >= 0) {
@@ -175,10 +228,26 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
}
}
// Auto-close unclosed DBCS shift sequence
if (inDBCS) {
out.write(SI);
}
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 Cp933());
// Register Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS)
registerExtendedCodepages();
// Setup common aliases
addAlias("us", "037");
addAlias("usa", "037");
@@ -218,6 +221,100 @@ public class CodePageRegistry {
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.
*/
@@ -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;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* EBCDIC Unicode character translator conforming to IBM Host On-Demand (HoD v14).
* 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).
*/
public class EbcdicTranslator {
@@ -15,6 +18,8 @@ public class EbcdicTranslator {
public static final int[] CP037_TO_UNICODE = Cp037.MAPPING;
private CodePage activeCodePage;
private final Map<Integer, Character> customEbcdicToUnicode = new ConcurrentHashMap<>();
private final Map<Character, Integer> customUnicodeToEbcdic = new ConcurrentHashMap<>();
public EbcdicTranslator() {
this("037");
@@ -85,6 +90,107 @@ public class EbcdicTranslator {
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.
*/
@@ -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) {
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.
*/
public int unicodeToEbcdic(char unicode) {
Integer custom = customUnicodeToEbcdic.get(unicode);
if (custom != null) {
return custom;
}
return activeCodePage.unicodeToEbcdic(unicode);
}
@@ -132,20 +247,37 @@ public class EbcdicTranslator {
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/
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) {
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);
}
/**
* 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) {
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);
}
@@ -169,7 +301,7 @@ public class EbcdicTranslator {
*/
public char mapAPL(int ebcdicCodePoint) {
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 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
@@ -182,7 +314,7 @@ public class EbcdicTranslator {
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
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 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
@@ -194,11 +326,58 @@ public class EbcdicTranslator {
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xAF: return '\u00AF'; // Overbar / High Minus '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
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);
}
}
@@ -109,6 +109,10 @@ public class DataStreamProcessor {
screenListeners.add(l);
}
public void removeScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.remove(l);
}
/**
* 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 TelnetFSM fsm;
private final List<ECLOIANotify> listeners = new ArrayList<>();
private final List<ECLOIAListener> oiaListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
public interface ECLOIANotify {
void onOIAChanged(ECLOIA oia);
@@ -44,12 +45,40 @@ public class ECLOIA implements ECLConstants {
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() {
for (ECLOIANotify l : listeners) {
try {
l.onOIAChanged(this);
} 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() {
@@ -74,7 +103,7 @@ public class ECLOIA implements ECLConstants {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
}
private int inhibitOverride = INHIBIT_NOT_INHIBITED;
private int inhibitOverride = -1;
public void setInputInhibited(int reason) {
if (this.inhibitOverride != reason) {
@@ -145,7 +174,6 @@ public class ECLOIA implements ECLConstants {
}
public String getStatusString() {
if (isCommError()) return "X-COMM";
int inhibit = getInputInhibited();
switch (inhibit) {
case INHIBIT_SYSTEM_LOCK: return "X-SYSTEM";
@@ -165,12 +193,12 @@ public class ECLOIA implements ECLConstants {
* Returns one of INHIBIT_* constants from ECLConstants.
*/
public int getInputInhibited() {
if (inhibitOverride >= 0) {
return inhibitOverride;
}
if (isCommError()) {
return INHIBIT_COMM_CHECK;
}
if (inhibitOverride != INHIBIT_NOT_INHIBITED) {
return inhibitOverride;
}
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
return INHIBIT_SYSTEM_LOCK;
}
@@ -204,6 +232,14 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs);
}
public boolean WaitForSystemAvailable(long timeoutMs) {
return waitForSysAvailable(timeoutMs);
}
public boolean WaitForInput(long timeoutMs) {
return waitForInput(timeoutMs);
}
/**
* Block until application is available.
*/
@@ -211,6 +247,10 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs);
}
public boolean WaitForAppAvailable(long timeoutMs) {
return waitForAppAvailable(timeoutMs);
}
/**
* Block until any OIA transition occurs.
*/
@@ -230,4 +270,8 @@ public class ECLOIA implements ECLConstants {
}
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;
}
// ========== 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.
*/
@@ -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.
*/
@@ -415,6 +493,30 @@ public class ECLPS implements ECLConstants {
// ========== 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.
*/
@@ -434,6 +536,10 @@ public class ECLPS implements ECLConstants {
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.
*/
@@ -454,6 +560,10 @@ public class ECLPS implements ECLConstants {
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).
*/
@@ -472,4 +582,8 @@ public class ECLPS implements ECLConstants {
}
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.FTCut;
import haus.nightmare.lib3270j.ft.FTDft;
import haus.nightmare.lib3270j.ft.CMSPrintXfer;
import haus.nightmare.lib3270j.ft.dir.*;
import haus.nightmare.lib3270j.input.InputProcessor;
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.
*/
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) {
if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -131,9 +140,27 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
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.
*/
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) {
if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -142,6 +169,16 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
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.
* @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) {
return new CMSDirectoryEntry(fn, ft, fm);
}
@@ -251,6 +363,15 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
return new TSODirectoryEntry(dsname);
}
private CMSPrintXfer cmsPrintXfer;
public CMSPrintXfer getCMSPrintXfer() {
if (cmsPrintXfer == null) {
cmsPrintXfer = new CMSPrintXfer(this, translator);
}
return cmsPrintXfer;
}
// ========== BIDI File Helpers ==========
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) {
if (options == null || options.trim().isEmpty()) return;
String upper = options.toUpperCase();
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);
config.parseOptions(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) {
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 ==========
@@ -71,6 +71,9 @@ public class FTDft {
public void setMTUSize(int size) {
this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, size));
if (listener != null && listener.getConfig() != null) {
listener.getConfig().setDftBufferSize(this.customMtuSize);
}
}
public int getMTUSize() {
@@ -414,9 +417,9 @@ public class FTDft {
log.info("DFT host message: " + msg);
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);
} 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);
} else {
// Informational message (default success)
@@ -480,7 +483,7 @@ public class FTDft {
return;
}
int bufferSize = config.getDftBufferSize();
int bufferSize = getMTUSize();
int numbytes = bufferSize - 27;
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
@@ -569,6 +572,71 @@ public class FTDft {
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.
* Matching x3270 dft_ascii_read logic.
@@ -537,6 +537,40 @@ public class InputProcessor {
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);
sendAidResponse(payload);
}
@@ -588,42 +622,14 @@ public class InputProcessor {
out.write(AID_SF);
try {
out.write(sf);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
out.write(rma);
} 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 {
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
try {
out.write(rma);
} catch (java.io.IOException ignored) {}
}
sendAidResponse(out.toByteArray());
@@ -10,14 +10,16 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/**
* Network Virtual Terminal (NVT) processor.
* Handles ASCII / ANSI VT100 character stream processing, cursor positioning,
* escape sequence decoding, terminal capability reports, and NVT transmission.
* Handles ASCII / ANSI VT100 / VT220 / VT320 character stream processing, cursor positioning,
* escape sequence decoding, terminal capability reports, OSC title/clipboard integration,
* DEC private modes, and NVT transmission.
*/
public class NvtProcessor {
@@ -26,12 +28,15 @@ public class NvtProcessor {
private final ScreenBuffer screenBuffer;
private final EbcdicTranslator translator;
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
private final List<NvtTitleListener> titleListeners = new CopyOnWriteArrayList<>();
// Escape sequence parser states
private static final int STATE_NORMAL = 0;
private static final int STATE_ESC = 1;
private static final int STATE_CSI = 2;
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 final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
@@ -39,14 +44,24 @@ public class NvtProcessor {
// Output sender callback
private OutputSender outputSender;
// Clipboard handler for OSC 52
private ClipboardHandler clipboardHandler;
// Terminal window title
private String terminalTitle = "";
// Graphic Rendition state
private byte currentFg = 0;
private byte currentBg = 0;
private byte currentGr = 0;
// Saved cursor position
// Saved cursor position and attributes
private int savedCursorRow = 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)
private int scrollTop = 0;
@@ -55,19 +70,44 @@ public class NvtProcessor {
// Tab stops
private boolean[] tabStops;
// Cursor visibility
// Terminal Modes & Options
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
private boolean lineDrawingG0 = false;
private boolean lineDrawingG1 = false;
private boolean activeCharsetG1 = false;
// Device Attributes responses (VT100 default, VT220 available)
private String primaryDeviceAttributes = "\u001B[?1;2c";
private String secondaryDeviceAttributes = "\u001B[>1;10;0c";
// 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
public interface OutputSender {
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) {
this.screenBuffer = screenBuffer;
this.translator = translator;
@@ -86,6 +126,14 @@ public class NvtProcessor {
this.outputSender = outputSender;
}
public void setClipboardHandler(ClipboardHandler clipboardHandler) {
this.clipboardHandler = clipboardHandler;
}
public ClipboardHandler getClipboardHandler() {
return clipboardHandler;
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
}
@@ -94,10 +142,67 @@ public class NvtProcessor {
screenListeners.remove(l);
}
public boolean isCursorVisible() {
return cursorVisible;
public void addTitleListener(NvtTitleListener l) {
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.
*/
@@ -129,6 +234,9 @@ public class NvtProcessor {
} else if (b == 0x0A) { // LF
int r = curAddr / cols;
int c = curAddr % cols;
if (newLineMode) {
c = 0; // CR + LF in New Line Mode
}
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
} else if (r < rows - 1) {
@@ -160,27 +268,50 @@ public class NvtProcessor {
for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm();
}
} else if (b == 0x0E) { // SO (Select G1 charset)
activeCharsetG1 = true;
} else if (b == 0x0F) { // SI (Select G0 charset)
activeCharsetG1 = false;
} else if (b == 0x0E) { // SO / LS1 (Select G1 charset)
activeCharset = 1;
} else if (b == 0x0F) { // SI / LS0 (Select G0 charset)
activeCharset = 0;
} else if (b >= 0x20 && b <= 0xFF) { // Printable character
char ch = (char) b;
if (activeCharsetG1 ? lineDrawingG1 : lineDrawingG0) {
ch = mapVt100SpecialGraphics(ch);
char setDesignator;
if (singleShiftCharset >= 0) {
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 c = curAddr % cols;
if (c >= cols) {
c = 0;
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
} else if (r < rows - 1) {
r++;
if (autoWrap) {
c = 0;
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
} 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);
@@ -194,17 +325,21 @@ public class NvtProcessor {
c++;
if (c >= cols) {
if (r < rows - 1) {
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0;
if (autoWrap) {
if (r < rows - 1) {
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0;
} else {
r++;
c = 0;
}
} else {
r++;
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0;
}
} else {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0;
c = cols - 1; // Stay at right margin without wrapping
}
}
curAddr = r * cols + c;
@@ -213,14 +348,26 @@ public class NvtProcessor {
escBuffer.write(b);
if (b == '[') {
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;
} else if (b == '7') { // DECSC - Save cursor
savedCursorRow = curAddr / cols;
savedCursorCol = curAddr % cols;
savedFg = currentFg;
savedBg = currentBg;
savedGr = currentGr;
savedOriginMode = originMode;
parseState = STATE_NORMAL;
} else if (b == '8') { // DECRC - Restore cursor
curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol);
currentFg = savedFg;
currentBg = savedBg;
currentGr = savedGr;
originMode = savedOriginMode;
parseState = STATE_NORMAL;
} else if (b == 'D') { // IND - Index (down 1 line)
int r = curAddr / cols;
@@ -266,8 +413,41 @@ public class NvtProcessor {
scrollTop = 0;
scrollBottom = rows - 1;
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();
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 {
// Unknown 2-byte escape, return to normal
parseState = STATE_NORMAL;
@@ -275,12 +455,36 @@ public class NvtProcessor {
} else if (parseState == STATE_CHARSET) {
byte[] seq = escBuffer.toByteArray();
if (seq.length >= 2) {
boolean isG1 = (seq[1] == ')');
boolean isLineDraw = (b == '0');
if (isG1) lineDrawingG1 = isLineDraw;
else lineDrawingG0 = isLineDraw;
char target = (char) seq[1];
char designator = (char) b;
if (target == '(') g0Charset = designator;
else if (target == ')') g1Charset = designator;
else if (target == '*') g2Charset = designator;
else if (target == '+') g3Charset = designator;
}
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) {
escBuffer.write(b);
// CSI final bytes are in the range 0x40..0x7E
@@ -298,6 +502,81 @@ public class NvtProcessor {
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.
* Returns updated cursor address.
@@ -314,26 +593,60 @@ public class NvtProcessor {
int r = 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) {
case 'H': // CUP - Cursor Position
case 'f': // HVP - Horizontal and Vertical Position
{
int p1 = parseParam(params, 0, 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));
return r * cols + c;
}
case 'A': // CUU - Cursor Up
{
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;
}
case 'B': // CUD - Cursor Down
{
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;
}
case 'C': // CUF - Cursor Forward
@@ -351,13 +664,15 @@ public class NvtProcessor {
case 'E': // CNL - Cursor Next Line
{
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
}
case 'F': // CPL - Cursor Previous Line
{
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
}
case 'G': // CHA - Cursor Horizontal Absolute
@@ -370,7 +685,11 @@ public class NvtProcessor {
case 'd': // VPA - Vertical Position Absolute
{
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;
}
case 'J': // ED - Erase in Display
@@ -476,7 +795,7 @@ public class NvtProcessor {
scrollTop = 0;
scrollBottom = rows - 1;
}
return 0; // Move cursor to home
return originMode ? (scrollTop * cols) : 0; // Move cursor to home
}
case 'm': // SGR - Select Graphic Rendition
{
@@ -497,25 +816,51 @@ public class NvtProcessor {
}
case 'n': // DSR - Device Status Report
{
int code = parseParam(params, 0, 0);
if (code == 6) { // Cursor position request
// Reply: ESC [ <row> ; <col> R (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
if (paramStr.startsWith("?")) {
int code = parseParam(params, 0, 0);
if (code == 6) { // Extended Cursor Position Report (DECXCPR)
int repRow = (originMode ? r - effectiveScrollTop : r) + 1;
int repCol = c + 1;
String response = String.format("\u001B[?%d;%d;1R", repRow, repCol);
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;
}
case 'c': // DA - Device Attributes
{
int code = parseParam(params, 0, 0);
if (code == 0) {
// Identify as standard VT100 with Advanced Video Option
sendResponseString("\u001B[?1;2c");
if (paramStr.startsWith(">")) {
// Secondary DA (DA2)
sendResponseString(secondaryDeviceAttributes);
} else {
// Primary DA
sendResponseString(primaryDeviceAttributes);
}
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
{
int mode = parseParam(params, 0, 0);
@@ -529,9 +874,16 @@ public class NvtProcessor {
case 'h': // Set Mode / Private Mode
{
if (paramStr.startsWith("?")) {
String sub = paramStr.substring(1).trim();
if ("25".equals(sub)) {
cursorVisible = true;
String[] subParams = paramStr.substring(1).split(";");
for (String sub : subParams) {
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;
@@ -539,9 +891,16 @@ public class NvtProcessor {
case 'l': // Reset Mode / Private Mode
{
if (paramStr.startsWith("?")) {
String sub = paramStr.substring(1).trim();
if ("25".equals(sub)) {
cursorVisible = false;
String[] subParams = paramStr.substring(1).split(";");
for (String sub : subParams) {
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;
@@ -549,16 +908,185 @@ public class NvtProcessor {
case 's': // Save cursor
savedCursorRow = r;
savedCursorCol = c;
savedFg = currentFg;
savedBg = currentBg;
savedGr = currentGr;
savedOriginMode = originMode;
return curAddr;
case 'u': // Restore cursor
r = Math.min(rows - 1, savedCursorRow);
c = Math.min(cols - 1, savedCursorCol);
currentFg = savedFg;
currentBg = savedBg;
currentGr = savedGr;
originMode = savedOriginMode;
return r * cols + c;
default:
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) {
if (outputSender != null) {
try {
@@ -573,7 +1101,9 @@ public class NvtProcessor {
if (params != null && idx < params.length && !params[idx].trim().isEmpty()) {
try {
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);
} 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) {
switch (c) {
case 'j': return '┘';
@@ -708,7 +1264,11 @@ public class NvtProcessor {
public void sendNVTChar(char c) throws IOException {
if (outputSender != null) {
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 {
outputSender.sendRaw(new byte[] { (byte) c });
}
@@ -20,6 +20,7 @@ public class PD3270 {
private static final Logger log = Logger.getLogger(PD3270.class.getName());
private final PrinterConfig config;
private PrinterDefinitionTable pdt;
private String destination;
private boolean open = false;
@@ -38,6 +39,7 @@ public class PD3270 {
public PD3270(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.pdt = this.config.getPrinterDefinitionTable();
this.destination = this.config.getDestinationTarget();
}
@@ -118,6 +120,13 @@ public class PD3270 {
if (!open) {
openPrinter(destination);
}
if (pdt != null) {
byte[] translated = pdt.translateChar(c);
if (translated != null && translated.length > 0) {
writePrintBytes(translated, 0, translated.length);
return;
}
}
try {
byte[] b = String.valueOf(c).getBytes(outputCharset);
memoryStream.write(b);
@@ -141,6 +150,12 @@ public class PD3270 {
if (!open) {
openPrinter(destination);
}
if (pdt != null) {
for (int i = 0; i < s.length(); i++) {
writePrintChar(s.charAt(i));
}
return;
}
try {
byte[] b = s.getBytes(outputCharset);
memoryStream.write(b);
@@ -168,10 +183,149 @@ public class PD3270 {
*/
public synchronized void formFeed() {
pageCount++;
writePrintChar('\f');
if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_PAGE_FEED)) {
writeControlCode(PrinterDefinitionTable.CMD_PAGE_FEED);
} else {
writePrintChar('\f');
}
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.
*/
@@ -270,4 +424,12 @@ public class PD3270 {
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;
// Auto-Flush Timer (Phase 7)
private java.util.Timer autoFlushTimer;
private final Object timerLock = new Object();
public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
@@ -251,7 +255,48 @@ public class PrintPS3270 {
}
if (startPrint) {
cancelAutoFlush();
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;
}
/**
* 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.
* @param data Byte array from host.
@@ -108,8 +128,6 @@ public class PrintSCS3270 {
int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0;
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 sliceLen = Math.min(orderTotalLen, bytesAvailable);
@@ -126,6 +144,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_STO:
processSetTextOrientation(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SCS:
processSelectCharacterSet(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SEAC:
processSetEnhancedAttribute(data, idx, sliceLen);
break;
@@ -135,6 +156,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_PPV:
processPresentationPositionVertical(data, idx, sliceLen);
break;
case PrinterConstants.SCS_GEA:
processSetGraphicErrorAction(data, idx, sliceLen);
break;
default:
log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder));
break;
@@ -154,12 +178,12 @@ public class PrintSCS3270 {
continue;
}
// Check TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRS) {
// Check TRN / TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRN) {
if (idx + 1 < end) {
int trsLen = data[idx + 1] & 0xFF;
int actualTrs = Math.min(trsLen, end - (idx + 2));
processTransparentStream(data, idx + 2, actualTrs);
processTRN(data, idx + 2, actualTrs);
idx += 2 + actualTrs;
} else {
idx = end;
@@ -167,52 +191,83 @@ public class PrintSCS3270 {
continue;
}
// Check Single-Byte SCS Controls
// Check Single-Byte SCS Controls (Full 28-command set)
switch (b) {
case PrinterConstants.SCS_NUL:
// Null - ignored
case PrinterConstants.SCS_NOP:
// Null / No-op - ignored
idx++;
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:
carriageReturn();
case PrinterConstants.SCS_RCR:
processCR();
idx++;
break;
case PrinterConstants.SCS_LF:
lineFeed();
processLF();
idx++;
break;
case PrinterConstants.SCS_NL:
case PrinterConstants.SCS_RNLS:
newLine();
processNL();
idx++;
break;
case PrinterConstants.SCS_FF:
formFeed();
processFF();
idx++;
break;
case PrinterConstants.SCS_BS:
case PrinterConstants.SCS_NBS:
backspace();
processBS();
idx++;
break;
case PrinterConstants.SCS_HT:
processHorizontalTab();
processHT();
idx++;
break;
case PrinterConstants.SCS_VT:
processVerticalTab();
processVT();
idx++;
break;
case PrinterConstants.SCS_SO:
processSO();
idx++;
break;
case PrinterConstants.SCS_SI:
processSI();
idx++;
break;
case PrinterConstants.SCS_ENP:
presentationEnabled = true;
processENP();
idx++;
break;
case PrinterConstants.SCS_INP:
presentationEnabled = false;
processINP();
idx++;
break;
case PrinterConstants.SCS_POC:
processPOC(null);
idx++;
break;
case PrinterConstants.SCS_BEL:
// Sound alarm
processBEL();
idx++;
break;
case PrinterConstants.SCS_IRS:
processIRS();
idx++;
break;
case PrinterConstants.SCS_SUB:
processSUB();
idx++;
break;
case PrinterConstants.SCS_GE:
@@ -233,7 +288,17 @@ public class PrintSCS3270 {
// Standard printable character
if (presentationEnabled) {
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++;
break;
@@ -293,10 +358,22 @@ public class PrintSCS3270 {
// ========== SCS Order Implementations ==========
public synchronized void carriageReturn() {
processCR();
}
public synchronized void processCR() {
currentCol = leftMargin;
}
public synchronized void processRCR() {
processCR();
}
public synchronized void lineFeed() {
processLF();
}
public synchronized void processLF() {
flushLineBuffer();
currentRow++;
if (currentRow > bottomMargin || currentRow > mpl) {
@@ -305,11 +382,23 @@ public class PrintSCS3270 {
}
public synchronized void newLine() {
carriageReturn();
lineFeed();
processNL();
}
public synchronized void processNL() {
processCR();
processLF();
}
public synchronized void processRNL() {
processNL();
}
public synchronized void formFeed() {
processFF();
}
public synchronized void processFF() {
flushLineBuffer();
pd.formFeed();
currentRow = topMargin;
@@ -317,18 +406,139 @@ public class PrintSCS3270 {
}
public synchronized void backspace() {
processBS();
}
public synchronized void processBS() {
if (currentCol > leftMargin) {
currentCol -= (doubleWidth ? 2 : 1);
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() {
newLine();
processNL();
}
public synchronized void advanceToNextPage() {
formFeed();
processFF();
}
// ========== Tab Stops and Calculations ==========
@@ -370,7 +580,7 @@ public class PrintSCS3270 {
int nextTab = calculateVerticalTab(currentRow);
if (nextTab > 0 && nextTab <= bottomMargin) {
while (currentRow < nextTab) {
lineFeed();
processLF();
}
} else {
advanceToNextPage();
@@ -414,20 +624,59 @@ public class PrintSCS3270 {
}
public synchronized void setPrintDensity(int cpi, int lpi) {
if (cpi > 0) this.cpi = cpi;
if (lpi > 0) this.lpi = lpi;
if (cpi > 0) {
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) {
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() {
this.doubleWidth = true;
if (pd != null) pd.setDoubleWidth(true);
}
public synchronized void endDoubleWidthCharacters() {
this.doubleWidth = false;
if (pd != null) pd.setDoubleWidth(false);
}
public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) {
@@ -484,7 +733,8 @@ public class PrintSCS3270 {
if (points > 0) {
// Line density in points / inch (72 points = 1 inch)
// 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) {
if (len >= 5) {
int attrVal = data[offset + 4] & 0xFF;
@@ -520,11 +785,11 @@ public class PrintSCS3270 {
if (subfn == PrinterConstants.POS_ABSOLUTE) {
while (currentRow < val && currentRow < bottomMargin) {
lineFeed();
processLF();
}
} else if (subfn == PrinterConstants.POS_RELATIVE) {
for (int i = 0; i < val && currentRow < bottomMargin; i++) {
lineFeed();
processLF();
}
}
}
@@ -536,19 +801,23 @@ public class PrintSCS3270 {
if (attrType == PrinterConstants.SA_COLOR) {
this.activeColor = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE) {
this.activeHighlight = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE || attrType == PrinterConstants.SA_EXT_HILITE) {
setEnhancedHighlight(attrVal);
} else if (attrType == PrinterConstants.SA_CHARSET) {
processSCS(attrVal);
} else if (attrType == PrinterConstants.SA_RESET) {
this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
this.doubleWidth = false;
setEnhancedHighlight(PrinterConstants.SEAC_DEFAULT);
endDoubleWidthCharacters();
}
}
public synchronized void processTransparentStream(byte[] data, int offset, int len) {
if (len > 0) {
pd.writePrintBytes(data, offset, len);
}
public int getChannelLine(int channel) {
return config.getChannelLine(channel);
}
public void setChannelLine(int channel, int line) {
config.setChannelLine(channel, line);
}
// ========== Accessors ==========
@@ -571,13 +840,16 @@ public class PrintSCS3270 {
public int getCharsPerInch() { return cpi; }
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 void setActiveColor(int color) { this.activeColor = color; }
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; }
@@ -48,26 +48,44 @@ public class PrinterConfig {
private boolean autoFlushOnEoj = true;
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) {
this();
this.host = host;
this.port = port;
}
public PrinterConfig(String host, int port, String printerLuName) {
this.host = host;
this.port = port;
this(host, port);
this.printerLuName = printerLuName;
}
public PrinterConfig(String host, int port, String printerLuName, boolean useTls) {
this.host = host;
this.port = port;
this.printerLuName = printerLuName;
this(host, port, printerLuName);
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
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
@@ -125,6 +143,7 @@ public class PrinterConfig {
public void setMpl(int mpl) {
this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl;
initDefaultChannelTable();
}
public int getLeftMargin() { return leftMargin; }
@@ -159,4 +178,53 @@ public class PrinterConfig {
public boolean isAutoReconnect() { return 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) ==========
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_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_GE = 0x08; // Graphic Escape
public static final int SCS_VT = 0x0B; // Vertical Tab
public static final int SCS_FF = 0x0C; // Form Feed
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_NL = 0x15; // New Line
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_LF = 0x25; // Line Feed
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_SUB = 0x3F; // Substitute Character
public static final int SCS_SP = 0x40; // 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>)
// 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_SVF = 0xD2; // Set Vertical Format
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_SEAC = 0xD5; // Set Enhanced Attribute / Highlight
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
public static final int SA_RESET = 0x00;
public static final int SA_HILITE = 0x41;
public static final int SA_COLOR = 0x42;
public static final int SA_CHARSET = 0x43;
public static final int SA_EXT_HILITE = 0x45;
// SEAC Highlight Values
public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_BLINK = 0xF1;
public static final int SEAC_REVERSE = 0xF2;
public static final int SEAC_UNDERLINE = 0xF4;
// SEAC / Extended Highlight Values
public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_NORMAL = 0x00;
public static final int SEAC_BLINK = 0xF1;
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
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;
}
if (printPs != null) {
printPs.cancelAutoFlush();
}
pd.closePrinter();
updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected");
}
@@ -260,11 +264,17 @@ public class Telnet3270EP implements Runnable {
*/
public synchronized void sendEOJ(boolean isComplete) {
log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete);
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
printPs.processPrintComplete();
} else {
scs.flushLineBuffer();
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
pd.endJob();
}
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);
}
}