This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 8 IBM 3270 APL / Graphic Escape (GA23-0059) character set translation.
|
||||
*/
|
||||
public class APLTranslationPhase8Test {
|
||||
|
||||
@Test
|
||||
public void testAPLBoxDrawingGlyphs() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator();
|
||||
|
||||
// Box lines and corners
|
||||
assertEquals('─', translator.mapAPL(0xA2)); // Horizontal line
|
||||
assertEquals('│', translator.mapAPL(0x85)); // Vertical line
|
||||
assertEquals('┌', translator.mapAPL(0xC5)); // Top Left corner
|
||||
assertEquals('┐', translator.mapAPL(0xD5)); // Top Right corner
|
||||
assertEquals('└', translator.mapAPL(0xC4)); // Bottom Left corner
|
||||
assertEquals('┘', translator.mapAPL(0xD4)); // Bottom Right corner
|
||||
assertEquals('├', translator.mapAPL(0xC6)); // Left T
|
||||
assertEquals('┤', translator.mapAPL(0xD6)); // Right T
|
||||
assertEquals('┬', translator.mapAPL(0xC7)); // Top T
|
||||
assertEquals('┴', translator.mapAPL(0xD7)); // Bottom T
|
||||
assertEquals('┼', translator.mapAPL(0xCB)); // Cross
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAPLMathAndSpecialSymbols() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator();
|
||||
|
||||
assertEquals('≤', translator.mapAPL(0x8C)); // Less than or equal
|
||||
assertEquals('≥', translator.mapAPL(0xAE)); // Greater than or equal
|
||||
assertEquals('≠', translator.mapAPL(0xBE)); // Not equal
|
||||
assertEquals('[', translator.mapAPL(0xAD)); // Left bracket
|
||||
assertEquals(']', translator.mapAPL(0xBD)); // Right bracket
|
||||
assertEquals('{', translator.mapAPL(0x8D)); // Left brace
|
||||
assertEquals('}', translator.mapAPL(0x9D)); // Right brace
|
||||
assertEquals('°', translator.mapAPL(0xB0)); // Degree
|
||||
assertEquals('±', translator.mapAPL(0xB1)); // Plus-minus
|
||||
assertEquals('²', translator.mapAPL(0xB2)); // Superscript 2
|
||||
assertEquals('³', translator.mapAPL(0xB3)); // Superscript 3
|
||||
assertEquals('¯', translator.mapAPL(0xAF)); // Overbar / High Minus
|
||||
assertEquals('Ω', translator.mapAPL(0xBA)); // Greek Omega
|
||||
assertEquals('µ', translator.mapAPL(0xBF)); // Micro
|
||||
assertEquals('¬', translator.mapAPL(0x5F)); // Not sign
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAPLOperatorsAndStructureGlyphs() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator();
|
||||
|
||||
assertEquals('⋄', translator.mapAPL(0x80)); // Diamond
|
||||
assertEquals('⍺', translator.mapAPL(0x81)); // Alpha
|
||||
assertEquals('⊥', translator.mapAPL(0x82)); // Up tack / decode
|
||||
assertEquals('∩', translator.mapAPL(0x83)); // Intersection
|
||||
assertEquals('⌊', translator.mapAPL(0x84)); // Floor
|
||||
assertEquals('∇', translator.mapAPL(0x87)); // Del
|
||||
assertEquals('∆', translator.mapAPL(0x88)); // Delta
|
||||
assertEquals('⍳', translator.mapAPL(0x89)); // Iota
|
||||
assertEquals('→', translator.mapAPL(0x8A)); // Right arrow
|
||||
assertEquals('⍞', translator.mapAPL(0x8B)); // Quote quad
|
||||
assertEquals('×', translator.mapAPL(0x8E)); // Multiply
|
||||
assertEquals('÷', translator.mapAPL(0x8F)); // Divide
|
||||
assertEquals('⍟', translator.mapAPL(0x90)); // Circle star
|
||||
assertEquals('⌹', translator.mapAPL(0x91)); // Domino / quad divide
|
||||
assertEquals('⊤', translator.mapAPL(0x92)); // Down tack / encode
|
||||
assertEquals('∪', translator.mapAPL(0x93)); // Union
|
||||
assertEquals('⌈', translator.mapAPL(0x94)); // Ceiling
|
||||
assertEquals('⍴', translator.mapAPL(0x95)); // Rho / shape
|
||||
assertEquals('⍵', translator.mapAPL(0x96)); // Omega
|
||||
assertEquals('○', translator.mapAPL(0x99)); // Circle
|
||||
assertEquals('←', translator.mapAPL(0x9A)); // Left arrow
|
||||
assertEquals('⍋', translator.mapAPL(0x9C)); // Grade up
|
||||
assertEquals('⍒', translator.mapAPL(0x9E)); // Grade down
|
||||
assertEquals('⍝', translator.mapAPL(0x9F)); // Lamp / comment
|
||||
assertEquals('⊂', translator.mapAPL(0xA6)); // Enclose / left shoe
|
||||
assertEquals('⊃', translator.mapAPL(0xA7)); // Disclose / right shoe
|
||||
assertEquals('↑', translator.mapAPL(0xA8)); // Take / up arrow
|
||||
assertEquals('↓', translator.mapAPL(0xA9)); // Drop / down arrow
|
||||
assertEquals('⎕', translator.mapAPL(0xAA)); // Quad
|
||||
assertEquals('⍉', translator.mapAPL(0xAC)); // Transpose
|
||||
assertEquals('⊖', translator.mapAPL(0xB4)); // Circle bar
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScreenBufferAPLCharacterSetIntegration() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator();
|
||||
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
|
||||
|
||||
// Place box-drawing corner and horizontal line with cs=1 (CS_APL)
|
||||
screen.setCellWithCS(0, 0xC5, 1); // Top left ┌
|
||||
screen.setCellWithCS(1, 0xA2, 1); // Horizontal ─
|
||||
screen.setCellWithCS(2, 0xD5, 1); // Top right ┐
|
||||
|
||||
screen.translateToUnicode();
|
||||
|
||||
assertEquals('┌', (char) screen.getCell(0).ucs4);
|
||||
assertEquals('─', (char) screen.getCell(1).ucs4);
|
||||
assertEquals('┐', (char) screen.getCell(2).ucs4);
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 8 custom user-defined character translation override tables.
|
||||
*/
|
||||
public class CustomTranslationOverridePhase8Test {
|
||||
|
||||
@Test
|
||||
public void testSingleOverride() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("037");
|
||||
assertFalse(translator.hasCustomOverrides());
|
||||
|
||||
// Default CP037 mapping for 0xC1 is 'A'
|
||||
assertEquals('A', translator.ebcdicToUnicode(0xC1));
|
||||
assertEquals(0xC1, translator.unicodeToEbcdic('A'));
|
||||
|
||||
// Override 0xC1 to '★' (\u2605)
|
||||
translator.setCustomOverride(0xC1, '★');
|
||||
assertTrue(translator.hasCustomOverrides());
|
||||
assertEquals('★', translator.ebcdicToUnicode(0xC1));
|
||||
assertEquals(0xC1, translator.unicodeToEbcdic('★'));
|
||||
|
||||
// Non-overridden characters should continue using default CP037
|
||||
assertEquals('B', translator.ebcdicToUnicode(0xC2));
|
||||
assertEquals(0xC2, translator.unicodeToEbcdic('B'));
|
||||
|
||||
// Safe conversion
|
||||
assertEquals((byte) 0xC1, translator.unicodeToEbcdicSafe('★'));
|
||||
|
||||
// Remove override
|
||||
translator.removeCustomOverride(0xC1);
|
||||
assertFalse(translator.hasCustomOverrides());
|
||||
assertEquals('A', translator.ebcdicToUnicode(0xC1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkOverrides() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("037");
|
||||
|
||||
Map<Integer, Character> ebcToUni = new HashMap<>();
|
||||
ebcToUni.put(0x81, 'α'); // replace 'a' with alpha
|
||||
ebcToUni.put(0x82, 'β'); // replace 'b' with beta
|
||||
|
||||
translator.setCustomOverrides(ebcToUni, null);
|
||||
assertTrue(translator.hasCustomOverrides());
|
||||
|
||||
assertEquals('α', translator.ebcdicToUnicode(0x81));
|
||||
assertEquals('β', translator.ebcdicToUnicode(0x82));
|
||||
assertEquals('c', translator.ebcdicToUnicode(0x83));
|
||||
|
||||
assertEquals(0x81, translator.unicodeToEbcdic('α'));
|
||||
assertEquals(0x82, translator.unicodeToEbcdic('β'));
|
||||
|
||||
// Clear overrides
|
||||
translator.clearCustomOverrides();
|
||||
assertFalse(translator.hasCustomOverrides());
|
||||
assertEquals('a', translator.ebcdicToUnicode(0x81));
|
||||
assertEquals('b', translator.ebcdicToUnicode(0x82));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringTranslationWithOverrides() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator("037");
|
||||
translator.setCustomOverride(0xBA, '❮'); // override '[' with '❮'
|
||||
translator.setCustomOverride(0xBB, '❯'); // override ']' with '❯'
|
||||
|
||||
byte[] input = new byte[] { (byte) 0xBA, (byte) 0xC8, (byte) 0xC9, (byte) 0xBB }; // [HI] in CP037
|
||||
String decoded = translator.ebcdicToString(input, 0, input.length);
|
||||
assertEquals("❮HI❯", decoded);
|
||||
|
||||
byte[] reEncoded = translator.stringToEbcdic("❮HI❯");
|
||||
assertArrayEquals(input, reEncoded);
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 8 DBCS SO/SI escape sequence handling, shift preservation, and UTF-8 conversion.
|
||||
*/
|
||||
public class DBCSSOSIEscapingPhase8Test {
|
||||
|
||||
@Test
|
||||
public void testSOSIPreserveMode() {
|
||||
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
|
||||
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
|
||||
cp.registerDbcsPair(0x4342, '\u4EAC'); // '京'
|
||||
|
||||
// EBCDIC: 'I'(0xC9) + SO(0x0E) + 東(0x4341) + 京(0x4342) + SI(0x0F) + '1'(0xF1)
|
||||
byte[] ebcdic = new byte[] {
|
||||
(byte) 0xC9,
|
||||
0x0E, (byte) 0x43, (byte) 0x41, (byte) 0x43, (byte) 0x42, 0x0F,
|
||||
(byte) 0xF1
|
||||
};
|
||||
|
||||
// Standard mode strips SO/SI from Unicode string
|
||||
String standard = cp.ebcdicToString(ebcdic, 0, ebcdic.length, false);
|
||||
assertEquals("I東京1", standard);
|
||||
|
||||
// Preserved mode keeps \u000E and \u000F in Unicode string
|
||||
String preserved = cp.ebcdicToString(ebcdic, 0, ebcdic.length, true);
|
||||
assertEquals("I\u000E東京\u000F1", preserved);
|
||||
|
||||
// Utility helper
|
||||
String utf8WithSosi = cp.ebcdicToUtf8WithSOSI(ebcdic);
|
||||
assertEquals(preserved, utf8WithSosi);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmbeddedSOSIStringConversion() {
|
||||
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
|
||||
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
|
||||
cp.registerDbcsPair(0x4342, '\u4EAC'); // '京'
|
||||
|
||||
// Encode string that already contains embedded \u000E and \u000F
|
||||
String inputWithSOSI = "I\u000E東京\u000F1";
|
||||
byte[] encoded = cp.stringToEbcdic(inputWithSOSI, true);
|
||||
|
||||
byte[] expected = new byte[] {
|
||||
(byte) 0xC9,
|
||||
0x0E, (byte) 0x43, (byte) 0x41, (byte) 0x43, (byte) 0x42, 0x0F,
|
||||
(byte) 0xF1
|
||||
};
|
||||
assertArrayEquals(expected, encoded);
|
||||
|
||||
// Utility helper
|
||||
byte[] helperEncoded = cp.utf8WithSOSIToEbcdic(inputWithSOSI);
|
||||
assertArrayEquals(expected, helperEncoded);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoCloseUnclosedDBCSShift() {
|
||||
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
|
||||
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
|
||||
|
||||
// Text ending with a DBCS character without explicit SI
|
||||
String s = "A東京";
|
||||
byte[] encoded = cp.stringToEbcdic(s, false);
|
||||
|
||||
// Must end with SI (0x0F)
|
||||
assertTrue(encoded.length > 0);
|
||||
assertEquals(0x0F, encoded[encoded.length - 1] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedundantSOSISuppression() {
|
||||
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
|
||||
cp.registerDbcsPair(0x4341, '\u6771');
|
||||
|
||||
// Input containing redundant duplicate \u000E \u000E
|
||||
String s = "\u000E\u000E東\u000F\u000F";
|
||||
byte[] encoded = cp.stringToEbcdic(s, true);
|
||||
|
||||
// Should contain only one SO (0x0E) and one SI (0x0F)
|
||||
assertEquals(4, encoded.length);
|
||||
assertEquals(0x0E, encoded[0] & 0xFF);
|
||||
assertEquals(0x43, encoded[1] & 0xFF);
|
||||
assertEquals(0x41, encoded[2] & 0xFF);
|
||||
assertEquals(0x0F, encoded[3] & 0xFF);
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS).
|
||||
*/
|
||||
public class ExtendedCodePagesPhase8Test {
|
||||
|
||||
@Test
|
||||
public void testArabicCp420() {
|
||||
CodePage cp = CodePageRegistry.getCodePage("420");
|
||||
assertNotNull(cp);
|
||||
assertEquals("420", cp.getCodePageId());
|
||||
assertEquals(420, cp.getCpgid());
|
||||
assertFalse(cp.isDBCS());
|
||||
|
||||
// Test alias resolution
|
||||
assertEquals("420", CodePageRegistry.getCodePage("ar").getCodePageId());
|
||||
assertEquals("420", CodePageRegistry.getCodePage("arabic").getCodePageId());
|
||||
assertEquals("420", CodePageRegistry.getCodePage("ebcdic-cp-ar").getCodePageId());
|
||||
assertEquals("420", CodePageRegistry.getCodePage("IBM420").getCodePageId());
|
||||
|
||||
// Basic ASCII and Arabic character mapping
|
||||
assertEquals('A', cp.ebcdicToUnicode(0xC1));
|
||||
assertEquals(0xC1, cp.unicodeToEbcdic('A'));
|
||||
assertEquals(' ', cp.ebcdicToUnicode(0x40));
|
||||
assertEquals(0x40, cp.unicodeToEbcdic(' '));
|
||||
|
||||
// Arabic characters (Hamza, Alef, Yeh, etc.)
|
||||
int ebcHamza = cp.unicodeToEbcdic('\u0621');
|
||||
assertTrue(ebcHamza >= 0);
|
||||
assertEquals('\u0621', cp.ebcdicToUnicode(ebcHamza));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHebrewCp424AndCp803() {
|
||||
CodePage cp424 = CodePageRegistry.getCodePage("424");
|
||||
assertNotNull(cp424);
|
||||
assertEquals("424", cp424.getCodePageId());
|
||||
assertEquals(424, cp424.getCpgid());
|
||||
assertEquals(941, cp424.getCgcsgid());
|
||||
|
||||
assertEquals("424", CodePageRegistry.getCodePage("he").getCodePageId());
|
||||
assertEquals("424", CodePageRegistry.getCodePage("hebrew").getCodePageId());
|
||||
|
||||
// Hebrew Alef \u05D0 in Cp424
|
||||
int ebcAlef424 = cp424.unicodeToEbcdic('\u05D0');
|
||||
assertTrue(ebcAlef424 >= 0);
|
||||
assertEquals('\u05D0', cp424.ebcdicToUnicode(ebcAlef424));
|
||||
|
||||
CodePage cp803 = CodePageRegistry.getCodePage("803");
|
||||
assertNotNull(cp803);
|
||||
assertEquals("803", cp803.getCodePageId());
|
||||
assertEquals(803, cp803.getCpgid());
|
||||
assertEquals(1147, cp803.getCgcsgid());
|
||||
|
||||
assertEquals("803", CodePageRegistry.getCodePage("hebrew-old").getCodePageId());
|
||||
assertEquals("803", CodePageRegistry.getCodePage("israel").getCodePageId());
|
||||
assertEquals("803", CodePageRegistry.getCodePage("iw").getCodePageId());
|
||||
|
||||
// Hebrew Alef in Cp803
|
||||
int ebcAlef803 = cp803.unicodeToEbcdic('\u05D0');
|
||||
assertTrue(ebcAlef803 >= 0);
|
||||
assertEquals('\u05D0', cp803.ebcdicToUnicode(ebcAlef803));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThaiCp838AndCp1160() {
|
||||
CodePage cp838 = CodePageRegistry.getCodePage("838");
|
||||
assertNotNull(cp838);
|
||||
assertEquals("838", cp838.getCodePageId());
|
||||
assertEquals(838, cp838.getCpgid());
|
||||
assertEquals(1176, cp838.getCgcsgid());
|
||||
|
||||
assertEquals("838", CodePageRegistry.getCodePage("th").getCodePageId());
|
||||
assertEquals("838", CodePageRegistry.getCodePage("thai").getCodePageId());
|
||||
|
||||
// Thai character Ko Kai \u0E01
|
||||
int ebcKoKai = cp838.unicodeToEbcdic('\u0E01');
|
||||
assertTrue(ebcKoKai >= 0);
|
||||
assertEquals('\u0E01', cp838.ebcdicToUnicode(ebcKoKai));
|
||||
|
||||
CodePage cp1160 = CodePageRegistry.getCodePage("1160");
|
||||
assertNotNull(cp1160);
|
||||
assertEquals("1160", cp1160.getCodePageId());
|
||||
assertEquals(1160, cp1160.getCpgid());
|
||||
assertEquals('€', cp1160.ebcdicToUnicode(0x9F));
|
||||
assertEquals(0x9F, cp1160.unicodeToEbcdic('€'));
|
||||
assertEquals("1160", CodePageRegistry.getCodePage("thai-euro").getCodePageId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCyrillicCp1025Cp1123Cp1154Cp880() {
|
||||
CodePage cp1025 = CodePageRegistry.getCodePage("1025");
|
||||
assertNotNull(cp1025);
|
||||
assertEquals(1025, cp1025.getCpgid());
|
||||
assertEquals(1150, cp1025.getCgcsgid());
|
||||
assertEquals("1025", CodePageRegistry.getCodePage("ru").getCodePageId());
|
||||
assertEquals("1025", CodePageRegistry.getCodePage("cyrillic").getCodePageId());
|
||||
assertEquals("1025", CodePageRegistry.getCodePage("russian").getCodePageId());
|
||||
|
||||
// Cyrillic Capital Letter A \u0410 and small a \u0430
|
||||
int ebcA1025 = cp1025.unicodeToEbcdic('\u0410');
|
||||
assertTrue(ebcA1025 >= 0);
|
||||
assertEquals('\u0410', cp1025.ebcdicToUnicode(ebcA1025));
|
||||
|
||||
// Cp1123 Ukraine
|
||||
CodePage cp1123 = CodePageRegistry.getCodePage("1123");
|
||||
assertNotNull(cp1123);
|
||||
assertEquals(1123, cp1123.getCpgid());
|
||||
assertEquals(1399, cp1123.getCgcsgid());
|
||||
assertEquals("1123", CodePageRegistry.getCodePage("ukraine").getCodePageId());
|
||||
assertEquals("1123", CodePageRegistry.getCodePage("ukrainian").getCodePageId());
|
||||
|
||||
// Cp1154 Cyrillic with Euro
|
||||
CodePage cp1154 = CodePageRegistry.getCodePage("1154");
|
||||
assertNotNull(cp1154);
|
||||
assertEquals(1154, cp1154.getCpgid());
|
||||
assertEquals(1305, cp1154.getCgcsgid());
|
||||
assertEquals('€', cp1154.ebcdicToUnicode(0x9F));
|
||||
assertEquals("1154", CodePageRegistry.getCodePage("cyrillic-euro").getCodePageId());
|
||||
|
||||
// Cp880 Cyrillic Russian
|
||||
CodePage cp880 = CodePageRegistry.getCodePage("880");
|
||||
assertNotNull(cp880);
|
||||
assertEquals(880, cp880.getCpgid());
|
||||
assertEquals(960, cp880.getCgcsgid());
|
||||
assertEquals("880", CodePageRegistry.getCodePage("cyrillic-russian").getCodePageId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTurkishCp1155AndCp905() {
|
||||
CodePage cp1155 = CodePageRegistry.getCodePage("1155");
|
||||
assertNotNull(cp1155);
|
||||
assertEquals(1155, cp1155.getCpgid());
|
||||
assertEquals(1306, cp1155.getCgcsgid());
|
||||
assertEquals('€', cp1155.ebcdicToUnicode(0x9F));
|
||||
assertEquals("1155", CodePageRegistry.getCodePage("turkish-euro").getCodePageId());
|
||||
|
||||
CodePage cp905 = CodePageRegistry.getCodePage("905");
|
||||
assertNotNull(cp905);
|
||||
assertEquals(905, cp905.getCpgid());
|
||||
assertEquals(1151, cp905.getCgcsgid());
|
||||
assertEquals("905", CodePageRegistry.getCodePage("turkish-latin3").getCodePageId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtendedChineseDBCSCp1388AndCp1371() {
|
||||
CodePage cp1388 = CodePageRegistry.getCodePage("1388");
|
||||
assertNotNull(cp1388);
|
||||
assertTrue(cp1388.isDBCS());
|
||||
assertEquals(1388, cp1388.getCpgid());
|
||||
assertEquals(1175, cp1388.getCgcsgid());
|
||||
assertEquals("1388", CodePageRegistry.getCodePage("chinese-ext-simplified").getCodePageId());
|
||||
assertEquals("1388", CodePageRegistry.getCodePage("zh-simplified-ext").getCodePageId());
|
||||
|
||||
CodePage cp1371 = CodePageRegistry.getCodePage("1371");
|
||||
assertNotNull(cp1371);
|
||||
assertTrue(cp1371.isDBCS());
|
||||
assertEquals(1371, cp1371.getCpgid());
|
||||
assertEquals(1174, cp1371.getCgcsgid());
|
||||
assertEquals("1371", CodePageRegistry.getCodePage("chinese-ext-traditional").getCodePageId());
|
||||
assertEquals("1371", CodePageRegistry.getCodePage("zh-traditional-ext").getCodePageId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasCodePageQuery() {
|
||||
assertTrue(CodePageRegistry.hasCodePage("037"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1047"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("420"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("424"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("803"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("838"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1160"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1025"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1123"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1154"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("880"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1155"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("905"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1388"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("1371"));
|
||||
|
||||
assertTrue(CodePageRegistry.hasCodePage("ar"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("hebrew"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("thai"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("russian"));
|
||||
assertTrue(CodePageRegistry.hasCodePage("ukraine"));
|
||||
|
||||
assertFalse(CodePageRegistry.hasCodePage("nonexistent-cp-9999"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Comprehensive Unit Tests for Phase 10: ECL API & Session Architecture Facade.
|
||||
*/
|
||||
public class ECLSessionPhase10Test {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
private ECLPS ps;
|
||||
private ECLOIA oia;
|
||||
private Telnet3270Client client;
|
||||
private ECLSession session;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
ConnectionConfig config = new ConnectionConfig("mainframe.example.com", 23, TerminalModel.IBM_3279_4);
|
||||
config.setLuName("LU3270A");
|
||||
config.setCodePage("037");
|
||||
|
||||
client = new Telnet3270Client(config);
|
||||
session = new ECLSession(client);
|
||||
ps = session.GetPS();
|
||||
oia = session.GetOIA();
|
||||
screen = client.getScreenBuffer();
|
||||
translator = client.getTranslator();
|
||||
input = client.getInputProcessor();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLSessionInstantiationWithConfigAndProperties() {
|
||||
// Test default constructor
|
||||
ECLSession defSession = new ECLSession();
|
||||
assertNotNull(defSession.GetPS());
|
||||
assertNotNull(defSession.GetOIA());
|
||||
assertNotNull(defSession.GetConnection());
|
||||
assertNotNull(defSession.GetXfer());
|
||||
assertNotNull(defSession.GetFieldList());
|
||||
assertNotNull(defSession.GetClient());
|
||||
assertEquals(23, defSession.GetConnection().GetPort());
|
||||
|
||||
// Test explicit host/port/model constructor
|
||||
ECLSession hpSession = new ECLSession("tso.test.org", 992, TerminalModel.IBM_3279_2, true);
|
||||
assertEquals("tso.test.org", hpSession.GetConnection().GetHost());
|
||||
assertEquals(992, hpSession.GetConnection().GetPort());
|
||||
assertEquals(TerminalModel.IBM_3279_2, hpSession.GetConnection().GetModel());
|
||||
assertTrue(hpSession.GetConnection().IsSSL());
|
||||
|
||||
// Test Properties constructor
|
||||
Properties props = new Properties();
|
||||
props.setProperty(ECLSession.SESSION_HOST, "mvs.corp.net");
|
||||
props.setProperty(ECLSession.SESSION_PORT, "2023");
|
||||
props.setProperty(ECLSession.SESSION_CODE_PAGE, "1047");
|
||||
props.setProperty(ECLSession.SESSION_MODEL, "5"); // 27x132
|
||||
props.setProperty(ECLSession.SESSION_SSL, "true");
|
||||
props.setProperty(ECLSession.SESSION_LU_NAME, "TSOLU01");
|
||||
props.setProperty(ECLSession.SESSION_TN3270E, "true");
|
||||
|
||||
ECLSession propSession = new ECLSession(props);
|
||||
assertEquals("mvs.corp.net", propSession.GetConnection().GetHost());
|
||||
assertEquals(2023, propSession.GetConnection().GetPort());
|
||||
assertEquals("1047", propSession.GetConnection().GetCodePage());
|
||||
assertEquals(TerminalModel.IBM_3279_5, propSession.GetConnection().GetModel());
|
||||
assertTrue(propSession.GetConnection().IsSSL());
|
||||
assertEquals("TSOLU01", propSession.GetConnection().GetLUName());
|
||||
assertEquals("TN3270E", propSession.GetConnection().GetConnType());
|
||||
|
||||
// Verify Properties sync and retrieval
|
||||
Properties retrieved = propSession.GetProperties();
|
||||
assertEquals("mvs.corp.net", retrieved.getProperty(ECLSession.SESSION_HOST));
|
||||
assertEquals("2023", retrieved.getProperty(ECLSession.SESSION_PORT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLConnectionAttributesAndState() {
|
||||
ECLConnection conn = session.GetConnection();
|
||||
assertNotNull(conn);
|
||||
assertEquals("mainframe.example.com", conn.GetHost());
|
||||
assertEquals(23, conn.GetPort());
|
||||
assertEquals("037", conn.GetCodePage());
|
||||
assertEquals(TerminalModel.IBM_3279_4, conn.GetModel());
|
||||
assertEquals("LU3270A", conn.GetLUName());
|
||||
assertEquals("LU3270A", conn.GetDevName());
|
||||
assertFalse(conn.IsSSL());
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, conn.GetState());
|
||||
assertEquals(0, conn.GetStateCode());
|
||||
assertFalse(conn.IsConnected());
|
||||
assertFalse(conn.IsReady());
|
||||
assertTrue(conn.IsDisconnecting());
|
||||
|
||||
// Verify state mappings
|
||||
assertEquals(0, ConnectionState.NOT_CONNECTED.toHoDStateCode());
|
||||
assertEquals(1, ConnectionState.TELNET_PENDING.toHoDStateCode());
|
||||
assertEquals(2, ConnectionState.CONNECTED_NVT.toHoDStateCode());
|
||||
assertEquals(2, ConnectionState.CONNECTED_UNBOUND.toHoDStateCode());
|
||||
assertEquals(3, ConnectionState.CONNECTED_3270.toHoDStateCode());
|
||||
assertEquals(3, ConnectionState.CONNECTED_TN3270E.toHoDStateCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLScreenDescMatching() {
|
||||
// Setup screen buffer with sample formatted screen
|
||||
screen.clear();
|
||||
screen.setCursorAddress(0);
|
||||
// Write: SF (prot), "TSO/E LOGON", SF (unprot), "USER01", SF (prot), "PASSWORD", SF (unprot), " "
|
||||
screen.setCellFA(0, (byte) 0x60); // SF protected
|
||||
String text1 = "TSO/E LOGON";
|
||||
for (int i = 0; i < text1.length(); i++) {
|
||||
screen.setChar(0, i + 1, text1.charAt(i));
|
||||
}
|
||||
|
||||
int userFaPos = 20;
|
||||
screen.setCellFA(userFaPos, (byte) 0x40); // SF unprotected
|
||||
String userVal = "USER01";
|
||||
for (int i = 0; i < userVal.length(); i++) {
|
||||
screen.setChar(0, userFaPos + 1 + i, userVal.charAt(i));
|
||||
}
|
||||
|
||||
int passFaPos = 40;
|
||||
screen.setCellFA(passFaPos, (byte) 0x60); // SF protected
|
||||
String passLabel = "PASSWORD";
|
||||
for (int i = 0; i < passLabel.length(); i++) {
|
||||
screen.setChar(0, passFaPos + 1 + i, passLabel.charAt(i));
|
||||
}
|
||||
|
||||
int passInputPos = 60;
|
||||
screen.setCellFA(passInputPos, (byte) 0x40); // SF unprot hidden
|
||||
|
||||
ps.setCursorPos(userFaPos + 1);
|
||||
|
||||
// 1. Test match string at 1-based row/col
|
||||
ECLScreenDesc desc1 = new ECLScreenDesc();
|
||||
desc1.AddString("TSO/E LOGON", 1, 2, true);
|
||||
assertTrue(desc1.Matches(session));
|
||||
assertTrue(desc1.Matches(ps, oia));
|
||||
assertTrue(desc1.Matches(client));
|
||||
|
||||
// Test case sensitivity
|
||||
ECLScreenDesc descCaseMismatch = new ECLScreenDesc();
|
||||
descCaseMismatch.AddString("tso/e logon", 1, 2, true);
|
||||
assertFalse(descCaseMismatch.Matches(session));
|
||||
|
||||
ECLScreenDesc descCaseInsensitive = new ECLScreenDesc();
|
||||
descCaseInsensitive.AddString("tso/e logon", 1, 2, false);
|
||||
assertTrue(descCaseInsensitive.Matches(session));
|
||||
|
||||
// 2. Test match string anywhere on screen
|
||||
ECLScreenDesc descAnywhere = new ECLScreenDesc();
|
||||
descAnywhere.AddString("PASSWORD");
|
||||
assertTrue(descAnywhere.Matches(session));
|
||||
|
||||
ECLScreenDesc descAnywhereFail = new ECLScreenDesc();
|
||||
descAnywhereFail.AddString("CICS TRANSACTION");
|
||||
assertFalse(descAnywhereFail.Matches(session));
|
||||
|
||||
// 3. Test match string in rectangular region
|
||||
ECLScreenDesc descRect = new ECLScreenDesc();
|
||||
descRect.AddStringInRect("USER01", 1, 1, 2, 80, true);
|
||||
assertTrue(descRect.Matches(session));
|
||||
|
||||
ECLScreenDesc descRectFail = new ECLScreenDesc();
|
||||
descRectFail.AddStringInRect("USER01", 2, 1, 5, 80, true);
|
||||
assertFalse(descRectFail.Matches(session));
|
||||
|
||||
// 4. Test match cursor position
|
||||
ECLScreenDesc descCursor = new ECLScreenDesc();
|
||||
descCursor.AddCursorPos(1, userFaPos + 2);
|
||||
assertTrue(descCursor.Matches(session));
|
||||
|
||||
ECLScreenDesc descCursorFail = new ECLScreenDesc();
|
||||
descCursorFail.AddCursorPos(10, 10);
|
||||
assertFalse(descCursorFail.Matches(session));
|
||||
|
||||
// 5. Test field counts
|
||||
ECLScreenDesc descFields = new ECLScreenDesc();
|
||||
descFields.AddNumFields(4);
|
||||
descFields.AddNumInputFields(2);
|
||||
assertTrue(descFields.Matches(session));
|
||||
|
||||
ECLScreenDesc descFieldsFail = new ECLScreenDesc();
|
||||
descFieldsFail.AddNumFields(10);
|
||||
assertFalse(descFieldsFail.Matches(session));
|
||||
|
||||
// 6. Test OIA status
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
ECLScreenDesc descOia = new ECLScreenDesc();
|
||||
descOia.AddOIAStatus(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
assertTrue(descOia.Matches(session));
|
||||
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
|
||||
assertFalse(descOia.Matches(session));
|
||||
|
||||
ECLScreenDesc descOiaLocked = new ECLScreenDesc();
|
||||
descOiaLocked.AddOIAStatus(ECLConstants.INHIBIT_SYSTEM_LOCK);
|
||||
assertTrue(descOiaLocked.Matches(session));
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
|
||||
// 7. Test Clear and composite descriptor
|
||||
ECLScreenDesc composite = new ECLScreenDesc();
|
||||
composite.AddString("TSO/E LOGON", 1, 2);
|
||||
composite.AddString("PASSWORD");
|
||||
composite.AddNumFields(4);
|
||||
assertEquals(3, composite.getConditionCount());
|
||||
assertTrue(composite.Matches(session));
|
||||
|
||||
composite.Clear();
|
||||
assertEquals(0, composite.getConditionCount());
|
||||
assertTrue(composite.Matches(session)); // Empty descriptor matches everything
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLPSListenerAndEventDispatching() {
|
||||
AtomicInteger updateCount = new AtomicInteger(0);
|
||||
AtomicInteger cursorCount = new AtomicInteger(0);
|
||||
AtomicInteger alarmCount = new AtomicInteger(0);
|
||||
AtomicInteger resizeCount = new AtomicInteger(0);
|
||||
AtomicReference<ECLPSEvent> lastEvent = new AtomicReference<>();
|
||||
|
||||
ECLPSListener listener = new ECLPSListener() {
|
||||
@Override
|
||||
public void psChanged(ECLPSEvent event) {
|
||||
updateCount.incrementAndGet();
|
||||
lastEvent.set(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void psCursorMoved(ECLPSEvent event) {
|
||||
cursorCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void psAlarm(ECLPSEvent event) {
|
||||
alarmCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void psResized(ECLPSEvent event) {
|
||||
resizeCount.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
ps.RegisterPSEvent(listener);
|
||||
|
||||
// 1. Trigger PS Update
|
||||
ps.notifyPSUpdate(2, 5, 2, 20, false);
|
||||
assertEquals(1, updateCount.get());
|
||||
assertNotNull(lastEvent.get());
|
||||
assertEquals(ECLPSEvent.PS_UPDATE, lastEvent.get().getEventType());
|
||||
assertEquals(2, lastEvent.get().getStartRow());
|
||||
assertEquals(5, lastEvent.get().getStartCol());
|
||||
assertEquals(2, lastEvent.get().getEndRow());
|
||||
assertEquals(20, lastEvent.get().getEndCol());
|
||||
assertFalse(lastEvent.get().isFullUpdate());
|
||||
assertSame(ps, lastEvent.get().getPS());
|
||||
|
||||
// 2. Trigger Cursor Move
|
||||
ps.notifyCursorMoved(0, 85);
|
||||
assertEquals(2, updateCount.get());
|
||||
assertEquals(1, cursorCount.get());
|
||||
assertEquals(ECLPSEvent.PS_CURSOR, lastEvent.get().getEventType());
|
||||
assertEquals(85, lastEvent.get().getNewCursorAddress());
|
||||
|
||||
// 3. Trigger Alarm
|
||||
ps.notifyAlarm();
|
||||
assertEquals(3, updateCount.get());
|
||||
assertEquals(1, alarmCount.get());
|
||||
assertEquals(ECLPSEvent.PS_ALARM, lastEvent.get().getEventType());
|
||||
|
||||
// 4. Trigger Screen Resize
|
||||
ps.notifyScreenResized(32, 80);
|
||||
assertEquals(4, updateCount.get());
|
||||
assertEquals(1, resizeCount.get());
|
||||
assertEquals(ECLPSEvent.PS_RESIZE, lastEvent.get().getEventType());
|
||||
assertEquals(32, lastEvent.get().getRows());
|
||||
assertEquals(80, lastEvent.get().getCols());
|
||||
|
||||
// 5. Unregister listener
|
||||
ps.UnregisterPSEvent(listener);
|
||||
ps.notifyPSUpdate(0, 0, 23, 79, true);
|
||||
assertEquals(4, updateCount.get()); // Count should not change after unregister
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLOIAListenerAndEventDispatching() {
|
||||
AtomicInteger changeCount = new AtomicInteger(0);
|
||||
AtomicInteger lockCount = new AtomicInteger(0);
|
||||
AtomicReference<ECLOIAEvent> lastOiaEvent = new AtomicReference<>();
|
||||
|
||||
ECLOIAListener listener = new ECLOIAListener() {
|
||||
@Override
|
||||
public void oiaChanged(ECLOIAEvent event) {
|
||||
changeCount.incrementAndGet();
|
||||
lastOiaEvent.set(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void oiaLockStateChanged(ECLOIAEvent event) {
|
||||
lockCount.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
oia.RegisterOIAEvent(listener);
|
||||
|
||||
// 1. Change inhibit state
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_PROTECTED_FIELD);
|
||||
assertEquals(1, changeCount.get());
|
||||
assertNotNull(lastOiaEvent.get());
|
||||
assertEquals(ECLConstants.INHIBIT_PROTECTED_FIELD, lastOiaEvent.get().getInputInhibited());
|
||||
assertEquals("X-PROT", lastOiaEvent.get().getStatusString());
|
||||
assertTrue(lastOiaEvent.get().isInputInhibited());
|
||||
assertSame(oia, lastOiaEvent.get().getOIA());
|
||||
|
||||
// 2. Change inhibit back to normal
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
assertEquals(2, changeCount.get());
|
||||
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, lastOiaEvent.get().getInputInhibited());
|
||||
assertEquals("READY", lastOiaEvent.get().getStatusString());
|
||||
assertFalse(lastOiaEvent.get().isInputInhibited());
|
||||
|
||||
// 3. Test uppercase wait methods
|
||||
assertTrue(oia.WaitForInput(100));
|
||||
assertTrue(oia.WaitForSystemAvailable(100));
|
||||
assertTrue(oia.WaitForAppAvailable(100));
|
||||
|
||||
// 4. Unregister listener
|
||||
oia.UnregisterOIAEvent(listener);
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_NUMERIC_ONLY);
|
||||
assertEquals(2, changeCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLCommListenerAndEventDispatching() {
|
||||
ECLConnection conn = session.GetConnection();
|
||||
AtomicInteger commCount = new AtomicInteger(0);
|
||||
AtomicInteger errorCount = new AtomicInteger(0);
|
||||
AtomicBoolean notifyConnected = new AtomicBoolean(false);
|
||||
AtomicReference<ECLCommEvent> lastCommEvent = new AtomicReference<>();
|
||||
|
||||
ECLCommListener listener = new ECLCommListener() {
|
||||
@Override
|
||||
public void commEvent(ECLCommEvent event) {
|
||||
commCount.incrementAndGet();
|
||||
lastCommEvent.set(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commError(ECLCommEvent event) {
|
||||
errorCount.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
conn.RegisterCommEvent(listener);
|
||||
conn.RegisterCommEvent(connected -> notifyConnected.set(connected), false);
|
||||
|
||||
// Simulate connection state change via listener
|
||||
client.getTelnetFSM().onError("Connection reset by peer");
|
||||
assertEquals(1, commCount.get());
|
||||
assertEquals(1, errorCount.get());
|
||||
assertNotNull(lastCommEvent.get());
|
||||
assertEquals(ECLCommEvent.COMM_ERROR, lastCommEvent.get().getEventType());
|
||||
assertEquals("Connection reset by peer", lastCommEvent.get().getErrorMessage());
|
||||
|
||||
// Unregister
|
||||
conn.UnregisterCommEvent(listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysWithCoordinates() {
|
||||
screen.clear();
|
||||
screen.setCursorAddress(0);
|
||||
// Setup an unprotected field at row 5, col 10 (0-based: row 4, col 9)
|
||||
int faPos = 4 * 80 + 8; // 328
|
||||
screen.setCellFA(faPos, (byte) 0x40); // Unprotected
|
||||
|
||||
// Send keys with 1-based (row 5, col 10)
|
||||
session.SendKeys("IBM3270", 5, 10);
|
||||
assertEquals("IBM3270", ps.getString(faPos + 1, 7));
|
||||
|
||||
// Send bracketed mnemonics
|
||||
ps.setCursorPos(1, 1);
|
||||
ps.SendKeys("A[tab]B", 1, 1);
|
||||
assertNotNull(ps.getString(0, 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWaitForScreenWithDescriptor() {
|
||||
screen.clear();
|
||||
screen.setChar(0, 0, 'L');
|
||||
screen.setChar(0, 1, 'O');
|
||||
screen.setChar(0, 2, 'G');
|
||||
screen.setChar(0, 3, 'O');
|
||||
screen.setChar(0, 4, 'N');
|
||||
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.AddString("LOGON", 1, 1);
|
||||
|
||||
assertTrue(session.WaitForScreen(desc, 100));
|
||||
assertTrue(ps.WaitForScreen(desc, 100));
|
||||
|
||||
ECLScreenDesc descFail = new ECLScreenDesc();
|
||||
descFail.AddString("NONEXISTENT", 1, 1);
|
||||
assertFalse(session.WaitForScreen(descFail, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIBMHoDPackageCompatibility() {
|
||||
// Instantiate using haus.nightmare.lib3270j.eNetwork.ECL.* package hierarchy
|
||||
haus.nightmare.lib3270j.eNetwork.ECL.ECLSession hodSession = new haus.nightmare.lib3270j.eNetwork.ECL.ECLSession();
|
||||
assertNotNull(hodSession.GetPS());
|
||||
assertNotNull(hodSession.GetOIA());
|
||||
assertNotNull(hodSession.GetConnection());
|
||||
assertNotNull(hodSession.GetXfer());
|
||||
assertNotNull(hodSession.GetFieldList());
|
||||
|
||||
haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc hodDesc = new haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc();
|
||||
hodDesc.AddString("TEST");
|
||||
assertEquals(1, hodDesc.getConditionCount());
|
||||
|
||||
AtomicBoolean hodPsReceived = new AtomicBoolean(false);
|
||||
hodSession.GetPS().RegisterPSEvent(new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSListener() {
|
||||
@Override
|
||||
public void psChanged(haus.nightmare.lib3270j.ecl.ECLPSEvent event) {
|
||||
hodPsReceived.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
hodSession.GetPS().notifyPSUpdate(0, 0, 10, 10, true);
|
||||
assertTrue(hodPsReceived.get());
|
||||
|
||||
// Test exception
|
||||
haus.nightmare.lib3270j.eNetwork.ECL.ECLException ex = new haus.nightmare.lib3270j.eNetwork.ECL.ECLException(haus.nightmare.lib3270j.eNetwork.ECL.ECLErrors.ECL_ERR_COMM_TIMEOUT, "Timeout");
|
||||
assertEquals(haus.nightmare.lib3270j.eNetwork.ECL.ECLErrors.ECL_ERR_COMM_TIMEOUT, ex.getErrorCode());
|
||||
assertEquals("Timeout", ex.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -88,4 +88,51 @@ public class ECLXferTest {
|
||||
assertTrue(loaded[0]);
|
||||
assertEquals(1, fileList.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilesBatchDownloadCallback() {
|
||||
boolean[] loaded = new boolean[1];
|
||||
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", "/tmp/nonexistent_test_dir", new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
|
||||
@Override
|
||||
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
|
||||
loaded[0] = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectoryError(String errorMessage) {}
|
||||
});
|
||||
|
||||
assertTrue(loaded[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilesVectorOverload() {
|
||||
java.util.Vector<String> localFiles = new java.util.Vector<>();
|
||||
java.util.Vector<String> hostFiles = new java.util.Vector<>();
|
||||
boolean[] loaded = new boolean[1];
|
||||
|
||||
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", 0, 0, localFiles, hostFiles, new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
|
||||
@Override
|
||||
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
|
||||
loaded[0] = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectoryError(String errorMessage) {}
|
||||
});
|
||||
|
||||
assertTrue(loaded[0]);
|
||||
assertEquals(1, hostFiles.size());
|
||||
assertEquals("PROFILE EXEC A1", hostFiles.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCMSPrintXferInstantiation() {
|
||||
assertNotNull(xfer.getCMSPrintXfer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResendInboundDataBufferToHostDelegation() {
|
||||
assertFalse(xfer.resendInboundDataBufferToHost());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class CMSPrintXferTest {
|
||||
|
||||
@Test
|
||||
public void testAsaCarriageControlTranslation() {
|
||||
// ASA: ' ' = single space, '0' = double space, '-' = triple space, '1' = form feed, '+' = overstrike
|
||||
String input = "1PAGE 1 HEADER\n" +
|
||||
" --------------------\n" +
|
||||
"0ITEM 1 DESCRIPTION\n" +
|
||||
"-ITEM 2 AFTER 3 LINES\n" +
|
||||
"+ITEM 2 UNDERLINED\n" +
|
||||
"1PAGE 2 HEADER\n";
|
||||
|
||||
String expected = "\fPAGE 1 HEADER\n" +
|
||||
"--------------------\n\n" +
|
||||
"ITEM 1 DESCRIPTION\n\n\n" +
|
||||
"ITEM 2 AFTER 3 LINES\r" +
|
||||
"ITEM 2 UNDERLINED\n\f" +
|
||||
"PAGE 2 HEADER";
|
||||
|
||||
String result = CMSPrintXfer.convertAsaCarriageControl(input);
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsaCarriageControlBytesConversion() {
|
||||
String input = " REPORT TITLE\n0SUBTITLE\n";
|
||||
byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] converted = CMSPrintXfer.convertAsaCarriageControl(inputBytes, false);
|
||||
String convertedStr = new String(converted, StandardCharsets.UTF_8);
|
||||
|
||||
assertEquals("REPORT TITLE\n\nSUBTITLE", convertedStr);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMachineCarriageControlTranslation() {
|
||||
// 0x09: Write and advance 1 line
|
||||
// 0x11: Write and advance 2 lines
|
||||
// 0x89: Write and skip to channel 1 (form feed)
|
||||
byte[] input = new byte[] {
|
||||
0x09, 'H', 'E', 'L', 'L', 'O', '\n',
|
||||
0x11, 'W', 'O', 'R', 'L', 'D', '\n',
|
||||
(byte) 0x89, 'N', 'E', 'X', 'T', ' ', 'P', 'A', 'G', 'E', '\n'
|
||||
};
|
||||
|
||||
byte[] output = CMSPrintXfer.convertMachineCarriageControl(input);
|
||||
String outputStr = new String(output, StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(outputStr.startsWith("HELLO\n"));
|
||||
assertTrue(outputStr.contains("WORLD\n\n"));
|
||||
assertTrue(outputStr.contains("NEXT PAGE\n\f"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseQueryReaderOutput() {
|
||||
String sampleRdrOutput =
|
||||
"ORIGINID FILE CLASS RECORDS CPY HOLD DATE TIME NAME TYPE DIST\n" +
|
||||
"MAINT 0042 A RDR 00000120 001 NONE 08/31 14:15:22 PROFILE EXEC SYSTEM\n" +
|
||||
"OPERATOR 0099 B RDR 00000500 002 USER 08/31 14:20:00 PAYROLL DATA FINANCE\n";
|
||||
|
||||
List<CMSPrintXfer.SpoolFileEntry> entries = CMSPrintXfer.parseQueryReaderOutput(sampleRdrOutput);
|
||||
assertEquals(2, entries.size());
|
||||
|
||||
CMSPrintXfer.SpoolFileEntry entry1 = entries.get(0);
|
||||
assertEquals(42, entry1.getSpoolId());
|
||||
assertEquals("MAINT", entry1.getOwner());
|
||||
assertEquals("A", entry1.getSpoolClass());
|
||||
assertEquals(120, entry1.getRecords());
|
||||
assertEquals(1, entry1.getCopies());
|
||||
assertEquals("NONE", entry1.getHoldStatus());
|
||||
assertEquals("08/31", entry1.getDate());
|
||||
assertEquals("14:15:22", entry1.getTime());
|
||||
assertEquals("PROFILE", entry1.getFileName());
|
||||
assertEquals("EXEC", entry1.getFileType());
|
||||
assertEquals("RDR", entry1.getDeviceType());
|
||||
|
||||
CMSPrintXfer.SpoolFileEntry entry2 = entries.get(1);
|
||||
assertEquals(99, entry2.getSpoolId());
|
||||
assertEquals("OPERATOR", entry2.getOwner());
|
||||
assertEquals("PAYROLL", entry2.getFileName());
|
||||
assertEquals("DATA", entry2.getFileType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseQueryPrinterOutput() {
|
||||
String samplePrtOutput =
|
||||
"ORIGINID FILE CLASS RECORDS CPY HOLD DATE TIME NAME TYPE DIST\n" +
|
||||
"USER01 0105 A PRT 00000045 001 USER 08/31 15:00:00 REPORT LISTING OFFICE\n";
|
||||
|
||||
List<CMSPrintXfer.SpoolFileEntry> entries = CMSPrintXfer.parseQueryPrinterOutput(samplePrtOutput);
|
||||
assertEquals(1, entries.size());
|
||||
assertEquals(105, entries.get(0).getSpoolId());
|
||||
assertEquals("USER01", entries.get(0).getOwner());
|
||||
assertEquals("REPORT", entries.get(0).getFileName());
|
||||
assertEquals("LISTING", entries.get(0).getFileType());
|
||||
assertEquals("PRT", entries.get(0).getDeviceType());
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,49 @@ public class FTConfigTest {
|
||||
|
||||
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseOptionsComprehensive() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("MY.DATASET");
|
||||
config.parseOptions("TSO ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(10,5) TRACKS REPLACE CODEPAGE(CP037) BUFFERSIZE(8192)");
|
||||
|
||||
assertEquals(FTConfig.HostType.TSO, config.getHostType());
|
||||
assertEquals(FTConfig.TransferMode.ASCII, config.getTransferMode());
|
||||
assertEquals(FTConfig.CrAction.REMOVE, config.getCrAction());
|
||||
assertEquals(FTConfig.RecordFormat.FIXED, config.getRecfm());
|
||||
assertEquals(80, config.getLrecl());
|
||||
assertEquals(3120, config.getBlksize());
|
||||
assertEquals(10, config.getPrimarySpace());
|
||||
assertEquals(5, config.getSecondarySpace());
|
||||
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
|
||||
assertEquals(FTConfig.ExistAction.REPLACE, config.getExistAction());
|
||||
assertEquals("CP037", config.getCodePage());
|
||||
assertEquals(8192, config.getDftBufferSize());
|
||||
|
||||
String cmd = config.buildCommand();
|
||||
assertTrue(cmd.contains("RECFM(F)"));
|
||||
assertTrue(cmd.contains("LRECL(80)"));
|
||||
assertTrue(cmd.contains("BLKSIZE(3120)"));
|
||||
assertTrue(cmd.contains("SPACE(10,5)"));
|
||||
assertTrue(cmd.contains("TRACKS"));
|
||||
assertTrue(cmd.contains("REPLACE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCicsCommandGeneration() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.CICS);
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("FILE01");
|
||||
config.parseOptions("CICS BINARY NOCRLF REPLACE");
|
||||
|
||||
assertEquals(FTConfig.HostType.CICS, config.getHostType());
|
||||
assertEquals(FTConfig.TransferMode.BINARY, config.getTransferMode());
|
||||
assertEquals(FTConfig.ExistAction.REPLACE, config.getExistAction());
|
||||
|
||||
String cmd = config.buildCommand();
|
||||
assertEquals("IND$FILE PUT FILE01 (BINARY NOCRLF REPLACE", cmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,4 +500,80 @@ public class FTDftTest {
|
||||
|
||||
assertArrayEquals(firstSent, retransmitted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDataPacketDirectWithMtuSplitting() {
|
||||
ftDft.setMTUSize(512);
|
||||
assertEquals(512, ftDft.getMTUSize());
|
||||
|
||||
// Send a 1000-byte payload, which should be split into 3 structured field chunks (each max 512 - 27 = 485 bytes)
|
||||
byte[] testPayload = new byte[1000];
|
||||
for (int i = 0; i < testPayload.length; i++) {
|
||||
testPayload[i] = (byte) (i & 0xFF);
|
||||
}
|
||||
|
||||
ftDft.sendDataPacket(testPayload);
|
||||
|
||||
assertEquals(3, inputProcessor.sentStructuredFields.size());
|
||||
assertEquals(1000, ftDft.getBytesTransferred());
|
||||
|
||||
// Verify that each structured field is SF_TRANSFER_DATA (0xD0) and has TR_GET_REPLY (0x4605)
|
||||
for (byte[] sf : inputProcessor.sentStructuredFields) {
|
||||
assertEquals(AID_SF, sf[0] & 0xFF);
|
||||
assertEquals(SF_TRANSFER_DATA, sf[3] & 0xFF);
|
||||
assertEquals(0x46, sf[4] & 0xFF);
|
||||
assertEquals(0x05, sf[5] & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCicsHostMessageHandling() {
|
||||
// DFH0500 File transfer completed successfully
|
||||
String successMsg = "DFH0500 File transfer completed successfully";
|
||||
byte[] msgBytes = successMsg.getBytes(StandardCharsets.ISO_8859_1);
|
||||
int totalPayloadLen = msgBytes.length + 5;
|
||||
|
||||
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
|
||||
sfOut.write(0); sfOut.write(0);
|
||||
sfOut.write(0xD0);
|
||||
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
|
||||
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
|
||||
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
|
||||
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
|
||||
sfOut.write(FTConstants.TR_BEGIN_DATA);
|
||||
sfOut.write((totalPayloadLen >> 8) & 0xFF);
|
||||
sfOut.write(totalPayloadLen & 0xFF);
|
||||
sfOut.write(msgBytes, 0, msgBytes.length);
|
||||
|
||||
byte[] insertData = sfOut.toByteArray();
|
||||
int sfLen = insertData.length;
|
||||
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
|
||||
insertData[1] = (byte) (sfLen & 0xFF);
|
||||
|
||||
// Open message stream first
|
||||
byte[] openReq = new byte[] {
|
||||
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
|
||||
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
|
||||
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
|
||||
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20
|
||||
};
|
||||
ftDft.processStructuredField(openReq, 0, openReq.length);
|
||||
|
||||
// Deliver success message
|
||||
ftDft.processStructuredField(insertData, 0, insertData.length);
|
||||
assertTrue(listener.completeCalled);
|
||||
assertFalse(listener.abortCalled);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResendInboundDataBufferToHost() {
|
||||
assertFalse(ftDft.resendInboundDataBufferToHost());
|
||||
|
||||
byte[] payload = "TEST BUFFER".getBytes(StandardCharsets.UTF_8);
|
||||
ftDft.sendDataPacket(payload);
|
||||
|
||||
assertEquals(1, inputProcessor.sentStructuredFields.size());
|
||||
assertTrue(ftDft.resendInboundDataBufferToHost());
|
||||
assertEquals(2, inputProcessor.sentStructuredFields.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,7 +453,7 @@ public class InputProcessorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAidAlwaysSendsStandard3270StreamEvenIfGraphicCursorActive() {
|
||||
public void testSendAidWhenGraphicCursorActiveSendsGraphicInputStructuredField() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
screen.getCell(1).ec = (byte) 0xC1; // 'A'
|
||||
@@ -462,6 +462,55 @@ public class InputProcessorTest {
|
||||
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
|
||||
goca.setGraphicsCursorActive(true);
|
||||
goca.setGraphicCursorPosition(100, -50);
|
||||
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
protected void sendAidResponse(byte[] data) {
|
||||
sent.set(data);
|
||||
}
|
||||
};
|
||||
input.setGocaDecoder(goca);
|
||||
|
||||
input.sendAid(AID_ENTER);
|
||||
|
||||
byte[] result = sent.get();
|
||||
assertNotNull(result);
|
||||
// Format: AID_SF (1) + SF (56) + AID_ENTER (1) + Cursor Addr (2) + SBA (1) + Field Addr (2) + Data 'A' (1) = 64 bytes
|
||||
assertEquals(64, result.length);
|
||||
assertEquals((byte) AID_SF, result[0]);
|
||||
assertEquals(0x00, result[1]);
|
||||
assertEquals(0x34, result[2]); // SF len = 52
|
||||
assertEquals(0x0F, result[3]);
|
||||
assertEquals(0x0F, result[4]);
|
||||
// Coordinates in SF at index 1 + 24 = 25
|
||||
int gx = (result[25] << 8) | (result[26] & 0xFF);
|
||||
int gy = (result[27] << 8) | (result[28] & 0xFF);
|
||||
assertEquals(100, (short) gx);
|
||||
assertEquals(-50, (short) gy);
|
||||
// Keyboard AID trigger class constant (0x07) at index 1 + 31 = 32
|
||||
assertEquals(0x07, result[32]);
|
||||
assertEquals(0x07, result[34]);
|
||||
assertEquals((byte) 0xFF, result[35]);
|
||||
assertEquals((byte) AID_ENTER, result[36]);
|
||||
|
||||
// Trailing AID at index 57
|
||||
assertEquals((byte) AID_ENTER, result[57]);
|
||||
assertEquals((byte) ORDER_SBA, result[60]);
|
||||
assertEquals((byte) 0xC1, result[63]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAidWhenGraphicCursorNotActiveSendsStandard3270Stream() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
screen.getCell(1).ec = (byte) 0xC1; // 'A'
|
||||
screen.setCursorAddress(2);
|
||||
|
||||
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
|
||||
goca.setGraphicsCursorActive(false);
|
||||
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
package haus.nightmare.lib3270j.nvt;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Comprehensive Unit Tests for Phase 9: NVT Mode (VT100 / VT220 / VT320 / ANSI Emulation).
|
||||
*/
|
||||
public class NvtProcessorPhase9Test {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private NvtProcessor processor;
|
||||
private ByteArrayOutputStream output;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); // 24x80
|
||||
processor = new NvtProcessor(screen, translator);
|
||||
output = new ByteArrayOutputStream();
|
||||
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
|
||||
}
|
||||
|
||||
private void feed(String s) {
|
||||
byte[] b = s.getBytes(StandardCharsets.US_ASCII);
|
||||
processor.processNVTData(b, 0, b.length);
|
||||
}
|
||||
|
||||
private void feedUtf8(String s) {
|
||||
byte[] b = s.getBytes(StandardCharsets.UTF_8);
|
||||
processor.processNVTData(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeviceStatusReportExtendedDSR() {
|
||||
output.reset();
|
||||
// DSR 5 (Status OK)
|
||||
feed("\u001B[5n");
|
||||
assertEquals("\u001B[0n", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// DSR 6 (Cursor Position Report) -> at row 10, col 20 (0-indexed: 9, 19)
|
||||
feed("\u001B[10;20H");
|
||||
feed("\u001B[6n");
|
||||
assertEquals("\u001B[10;20R", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// DSR ?6 (DECXCPR Extended Cursor Position Report)
|
||||
feed("\u001B[?6n");
|
||||
assertEquals("\u001B[?10;20;1R", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// DSR ?15 (Printer status)
|
||||
feed("\u001B[?15n");
|
||||
assertEquals("\u001B[?13n", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// DSR ?26 (Keyboard dialect status)
|
||||
feed("\u001B[?26n");
|
||||
assertEquals("\u001B[?27;1n", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// DSR ?62 / ?63 (Macro/Memory Checksum)
|
||||
feed("\u001B[?62n");
|
||||
assertEquals("\u001B[?63;0n", output.toString(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeviceAttributesPrimaryAndSecondaryDA() {
|
||||
output.reset();
|
||||
// Default Primary DA (VT100 with AVO): ESC [ c
|
||||
feed("\u001B[c");
|
||||
assertEquals("\u001B[?1;2c", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// Configure VT220 Primary DA
|
||||
processor.setPrimaryDeviceAttributes("\u001B[?62;1;2;6;7;8;9c");
|
||||
feed("\u001B[c");
|
||||
assertEquals("\u001B[?62;1;2;6;7;8;9c", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
feed("\u001B[0c");
|
||||
assertEquals("\u001B[?62;1;2;6;7;8;9c", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// Secondary DA: ESC [ > c
|
||||
feed("\u001B[>c");
|
||||
assertEquals("\u001B[>1;10;0c", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// Secondary DA: ESC [ > 0 c
|
||||
feed("\u001B[>0c");
|
||||
assertEquals("\u001B[>1;10;0c", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// DECREQTPARM: ESC [ 1 x
|
||||
feed("\u001B[1x");
|
||||
assertEquals("\u001B[3;1;1;120;120;1;0x", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// DECREQTPARM: ESC [ 0 x
|
||||
feed("\u001B[0x");
|
||||
assertEquals("\u001B[2;1;1;120;120;1;0x", output.toString(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecSoftTerminalResetDECSTR() {
|
||||
// Set non-standard modes
|
||||
processor.setCursorVisible(false);
|
||||
processor.setApplicationKeypad(true);
|
||||
processor.setApplicationCursorKeys(true);
|
||||
processor.setOriginMode(true);
|
||||
processor.setAutoWrap(false);
|
||||
processor.setInsertMode(true);
|
||||
|
||||
// Set scroll region 3..10
|
||||
feed("\u001B[3;10r");
|
||||
|
||||
// Soft Reset: ESC [ ! p
|
||||
feed("\u001B[!p");
|
||||
|
||||
assertTrue(processor.isCursorVisible());
|
||||
assertFalse(processor.isApplicationKeypad());
|
||||
assertFalse(processor.isApplicationCursorKeys());
|
||||
assertFalse(processor.isOriginMode());
|
||||
assertTrue(processor.isAutoWrap());
|
||||
assertFalse(processor.isInsertMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOriginModeDECOM() {
|
||||
// Set scroll region lines 5..15: ESC [ 5 ; 15 r
|
||||
feed("\u001B[5;15r");
|
||||
|
||||
// Enable Origin Mode: ESC [ ? 6 h
|
||||
feed("\u001B[?6h");
|
||||
assertTrue(processor.isOriginMode());
|
||||
|
||||
// Move to line 1, col 1 in origin mode -> corresponds to row 5 (0-indexed: 4), col 0
|
||||
feed("\u001B[1;1H");
|
||||
assertEquals(4 * 80, screen.getCursorAddress());
|
||||
|
||||
// Move to line 3, col 10 in origin mode -> corresponds to row 7 (0-indexed: 6), col 9
|
||||
feed("\u001B[3;10H");
|
||||
assertEquals(6 * 80 + 9, screen.getCursorAddress());
|
||||
|
||||
// Check DSR extended CPR in origin mode: should report row 3, col 10 relative to top margin
|
||||
output.reset();
|
||||
feed("\u001B[?6n");
|
||||
assertEquals("\u001B[?3;10;1R", output.toString(StandardCharsets.US_ASCII));
|
||||
|
||||
// Disable Origin Mode: ESC [ ? 6 l
|
||||
feed("\u001B[?6l");
|
||||
assertFalse(processor.isOriginMode());
|
||||
|
||||
// Move to line 1, col 1 in absolute mode -> row 0, col 0
|
||||
feed("\u001B[1;1H");
|
||||
assertEquals(0, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterSetSCSandShifts() {
|
||||
// Designate G0 as UK (ESC ( A), G1 as Line Drawing (ESC ) 0), G2 as German (ESC * K)
|
||||
feed("\u001B(A\u001B)0\u001B*K");
|
||||
assertEquals('A', processor.getG0Charset());
|
||||
assertEquals('0', processor.getG1Charset());
|
||||
assertEquals('K', processor.getG2Charset());
|
||||
|
||||
// In G0 (UK): '#' maps to '£'
|
||||
feed("\u001B[1;1H#");
|
||||
assertEquals('£', screen.getCell(0).ucs4);
|
||||
|
||||
// Shift-Out (0x0E -> G1 Line Drawing)
|
||||
processor.processNVTData(new byte[] { 0x0E, (byte) 'q' }, 0, 2);
|
||||
assertEquals('─', screen.getCell(1).ucs4);
|
||||
|
||||
// Shift-In (0x0F -> G0 UK)
|
||||
processor.processNVTData(new byte[] { 0x0F, (byte) '#' }, 0, 2);
|
||||
assertEquals('£', screen.getCell(2).ucs4);
|
||||
|
||||
// Single Shift SS2 (ESC N -> G2 German for next character only)
|
||||
// In German: '@' -> '§', '{' -> 'ä'
|
||||
feed("\u001BN@#");
|
||||
assertEquals('§', screen.getCell(3).ucs4); // Single shift applied to '@'
|
||||
assertEquals('£', screen.getCell(4).ucs4); // Returned to G0 UK for '#'
|
||||
|
||||
// Locking Shift LS2 (ESC n -> G2 German active)
|
||||
feed("\u001Bn{|}~");
|
||||
assertEquals('ä', screen.getCell(5).ucs4);
|
||||
assertEquals('ö', screen.getCell(6).ucs4);
|
||||
assertEquals('ü', screen.getCell(7).ucs4);
|
||||
assertEquals('ß', screen.getCell(8).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOscWindowTitleAndIcon() {
|
||||
AtomicReference<String> titleRef = new AtomicReference<>("");
|
||||
processor.addTitleListener(titleRef::set);
|
||||
|
||||
// OSC 0 (BEL terminated): ESC ] 0 ; Main Terminal Window BEL
|
||||
feedUtf8("\u001B]0;Main Terminal Window\u0007");
|
||||
|
||||
assertEquals("Main Terminal Window", processor.getTerminalTitle());
|
||||
assertEquals("Main Terminal Window", titleRef.get());
|
||||
|
||||
// OSC 2 (ST terminated): ESC ] 2 ; IBM Host Session ESC \
|
||||
feedUtf8("\u001B]2;IBM Host Session\u001B\\");
|
||||
|
||||
assertEquals("IBM Host Session", processor.getTerminalTitle());
|
||||
assertEquals("IBM Host Session", titleRef.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOsc52ClipboardReadAndWrite() {
|
||||
AtomicReference<String> clipboardStorage = new AtomicReference<>("Initial Clipboard");
|
||||
processor.setClipboardHandler(new NvtProcessor.ClipboardHandler() {
|
||||
@Override
|
||||
public String getClipboardText() {
|
||||
return clipboardStorage.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClipboardText(String text) {
|
||||
clipboardStorage.set(text);
|
||||
}
|
||||
});
|
||||
|
||||
// OSC 52 Write: Base64 of "Hello from Host" is "SGVsbG8gZnJvbSBIb3N0"
|
||||
feed("\u001B]52;c;SGVsbG8gZnJvbSBIb3N0\u0007");
|
||||
|
||||
assertEquals("Hello from Host", clipboardStorage.get());
|
||||
|
||||
// OSC 52 Read Query: ESC ] 52 ; c ; ? BEL
|
||||
output.reset();
|
||||
feed("\u001B]52;c;?\u0007");
|
||||
|
||||
String expectedB64 = java.util.Base64.getEncoder().encodeToString("Hello from Host".getBytes(StandardCharsets.UTF_8));
|
||||
String response = output.toString(StandardCharsets.US_ASCII);
|
||||
assertTrue(response.contains(expectedB64));
|
||||
assertTrue(response.startsWith("\u001B]52;c;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplicationKeypadModeDECKPAM() {
|
||||
assertFalse(processor.isApplicationKeypad());
|
||||
|
||||
// Enable Keypad Application Mode: ESC =
|
||||
feed("\u001B=");
|
||||
assertTrue(processor.isApplicationKeypad());
|
||||
|
||||
// Test keypad mapping
|
||||
assertEquals("\u001BOp", processor.mapKeypadKey(0x60)); // NUMPAD0
|
||||
assertEquals("\u001BOq", processor.mapKeypadKey(0x61)); // NUMPAD1
|
||||
assertEquals("\u001BOr", processor.mapKeypadKey(0x62)); // NUMPAD2
|
||||
assertEquals("\u001BOs", processor.mapKeypadKey(0x63)); // NUMPAD3
|
||||
assertEquals("\u001BOt", processor.mapKeypadKey(0x64)); // NUMPAD4
|
||||
assertEquals("\u001BOu", processor.mapKeypadKey(0x65)); // NUMPAD5
|
||||
assertEquals("\u001BOv", processor.mapKeypadKey(0x66)); // NUMPAD6
|
||||
assertEquals("\u001BOw", processor.mapKeypadKey(0x67)); // NUMPAD7
|
||||
assertEquals("\u001BOx", processor.mapKeypadKey(0x68)); // NUMPAD8
|
||||
assertEquals("\u001BOy", processor.mapKeypadKey(0x69)); // NUMPAD9
|
||||
assertEquals("\u001BOm", processor.mapKeypadKey(0x6D)); // MINUS
|
||||
assertEquals("\u001BOn", processor.mapKeypadKey(0x6E)); // DECIMAL
|
||||
assertEquals("\u001BOM", processor.mapKeypadKey(0x0A)); // ENTER
|
||||
|
||||
byte[] bytes = processor.processApplicationKeypad(0x60);
|
||||
assertNotNull(bytes);
|
||||
assertEquals("\u001BOp", new String(bytes, StandardCharsets.US_ASCII));
|
||||
|
||||
// Enable Keypad Numeric Mode: ESC >
|
||||
feed("\u001B>");
|
||||
assertFalse(processor.isApplicationKeypad());
|
||||
assertNull(processor.mapKeypadKey(0x60));
|
||||
assertNull(processor.processApplicationKeypad(0x60));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplicationCursorKeysModeDECCKM() {
|
||||
assertFalse(processor.isApplicationCursorKeys());
|
||||
assertEquals("\u001B[A", processor.mapKey(0x26, '\0', false, false, false)); // VK_UP normal
|
||||
assertEquals("\u001B[B", processor.mapKey(0x28, '\0', false, false, false)); // VK_DOWN normal
|
||||
assertEquals("\u001B[C", processor.mapKey(0x27, '\0', false, false, false)); // VK_RIGHT normal
|
||||
assertEquals("\u001B[D", processor.mapKey(0x25, '\0', false, false, false)); // VK_LEFT normal
|
||||
|
||||
// Enable Application Cursor Keys: ESC [ ? 1 h
|
||||
feed("\u001B[?1h");
|
||||
assertTrue(processor.isApplicationCursorKeys());
|
||||
|
||||
assertEquals("\u001BOA", processor.mapKey(0x26, '\0', false, false, false)); // VK_UP application
|
||||
assertEquals("\u001BOB", processor.mapKey(0x28, '\0', false, false, false)); // VK_DOWN application
|
||||
assertEquals("\u001BOC", processor.mapKey(0x27, '\0', false, false, false)); // VK_RIGHT application
|
||||
assertEquals("\u001BOD", processor.mapKey(0x25, '\0', false, false, false)); // VK_LEFT application
|
||||
|
||||
// Disable Application Cursor Keys: ESC [ ? 1 l
|
||||
feed("\u001B[?1l");
|
||||
assertFalse(processor.isApplicationCursorKeys());
|
||||
assertEquals("\u001B[A", processor.mapKey(0x26, '\0', false, false, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFunctionAndEditingKeyMapping() {
|
||||
// Function keys F1..F4
|
||||
assertEquals("\u001BOP", processor.mapKey(0x70, '\0', false, false, false)); // F1
|
||||
assertEquals("\u001BOQ", processor.mapKey(0x71, '\0', false, false, false)); // F2
|
||||
assertEquals("\u001BOR", processor.mapKey(0x72, '\0', false, false, false)); // F3
|
||||
assertEquals("\u001BOS", processor.mapKey(0x73, '\0', false, false, false)); // F4
|
||||
|
||||
// Function keys F5..F12
|
||||
assertEquals("\u001B[15~", processor.mapKey(0x74, '\0', false, false, false)); // F5
|
||||
assertEquals("\u001B[17~", processor.mapKey(0x75, '\0', false, false, false)); // F6
|
||||
assertEquals("\u001B[24~", processor.mapKey(0x7B, '\0', false, false, false)); // F12
|
||||
|
||||
// Editing keys
|
||||
assertEquals("\u001B[2~", processor.mapKey(0x9B, '\0', false, false, false)); // Insert
|
||||
assertEquals("\u001B[3~", processor.mapKey(0x7F, '\0', false, false, false)); // Delete
|
||||
assertEquals("\u001B[1~", processor.mapKey(0x24, '\0', false, false, false)); // Home
|
||||
assertEquals("\u001B[4~", processor.mapKey(0x23, '\0', false, false, false)); // End
|
||||
assertEquals("\u001B[5~", processor.mapKey(0x21, '\0', false, false, false)); // PageUp
|
||||
assertEquals("\u001B[6~", processor.mapKey(0x22, '\0', false, false, false)); // PageDown
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertModeAndNewLineMode() throws IOException {
|
||||
// Line Feed in standard mode: cursor moves down without returning to col 0
|
||||
processor.setNewLineMode(false);
|
||||
feed("\u001B[1;10H\n");
|
||||
assertEquals(1 * 80 + 9, screen.getCursorAddress()); // Row 2, Col 10
|
||||
|
||||
// Enable New Line Mode: ESC [ 20 h
|
||||
feed("\u001B[20h");
|
||||
assertTrue(processor.isNewLineMode());
|
||||
|
||||
feed("\u001B[1;10H\n");
|
||||
assertEquals(1 * 80, screen.getCursorAddress()); // Row 2, Col 0 (CR + LF)
|
||||
|
||||
// Insert Mode: ESC [ 4 h
|
||||
feed("\u001B[1;1HAC");
|
||||
feed("\u001B[1;2H"); // Move cursor between A and C
|
||||
feed("\u001B[4h"); // Enable Insert Mode
|
||||
assertTrue(processor.isInsertMode());
|
||||
|
||||
feed("B");
|
||||
assertEquals('A', screen.getCell(0).ucs4);
|
||||
assertEquals('B', screen.getCell(1).ucs4);
|
||||
assertEquals('C', screen.getCell(2).ucs4);
|
||||
|
||||
// Reset Insert Mode: ESC [ 4 l
|
||||
feed("\u001B[4l");
|
||||
assertFalse(processor.isInsertMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoWrapAndColumnMode() {
|
||||
// Disable Auto Wrap: ESC [ ? 7 l
|
||||
feed("\u001B[?7l");
|
||||
assertFalse(processor.isAutoWrap());
|
||||
|
||||
// Move to col 79 (0-indexed: 78) on line 1, and write 5 characters
|
||||
feed("\u001B[1;79H12345");
|
||||
// Characters should truncate / overwrite at margin (col 79) and not wrap to next row
|
||||
assertEquals('5', screen.getCell(79).ucs4);
|
||||
assertEquals(79, screen.getCursorAddress());
|
||||
|
||||
// Enable Auto Wrap: ESC [ ? 7 h
|
||||
feed("\u001B[?7h");
|
||||
assertTrue(processor.isAutoWrap());
|
||||
|
||||
// 132-Column Mode switch: ESC [ ? 3 h (clears screen)
|
||||
feed("\u001B[?3h");
|
||||
assertEquals(0, screen.getCursorAddress());
|
||||
assertEquals(0, screen.getCell(0).ucs4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package haus.nightmare.lib3270j.printer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for Phase 7 Printer Definition Table (PDT) profiles, control sequence generation,
|
||||
* character mappings, and GDDM host escapes in PD3270.
|
||||
*/
|
||||
public class PD3270PDTPhase7Test {
|
||||
|
||||
private PrinterConfig config;
|
||||
private PD3270 pd;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
config = new PrinterConfig("localhost", 23);
|
||||
pd = new PD3270(config);
|
||||
pd.openPrinter(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuiltinPDTPresets() {
|
||||
// Plain Text
|
||||
PrinterDefinitionTable textPdt = PrinterDefinitionTable.createPlainTextPDT();
|
||||
assertEquals("PLAIN_TEXT", textPdt.getName());
|
||||
assertTrue(textPdt.hasControlCode(PrinterDefinitionTable.CMD_PAGE_FEED));
|
||||
|
||||
// HP PCL 5
|
||||
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
|
||||
assertEquals("PCL_5", pcl5.getName());
|
||||
assertTrue(pcl5.hasControlCode(PrinterDefinitionTable.CMD_START_BOLD));
|
||||
assertArrayEquals("\033(s3B".getBytes(), pcl5.getControlCode(PrinterDefinitionTable.CMD_START_BOLD));
|
||||
|
||||
// Epson ESC/P
|
||||
PrinterDefinitionTable escp = PrinterDefinitionTable.createEpsonEscPPDT();
|
||||
assertEquals("EPSON_ESC_P", escp.getName());
|
||||
assertTrue(escp.hasControlCode(PrinterDefinitionTable.CMD_START_BOLD));
|
||||
assertArrayEquals("\033E".getBytes(), escp.getControlCode(PrinterDefinitionTable.CMD_START_BOLD));
|
||||
|
||||
// PostScript
|
||||
PrinterDefinitionTable ps = PrinterDefinitionTable.createPostScriptPDT();
|
||||
assertEquals("POSTSCRIPT", ps.getName());
|
||||
assertTrue(ps.hasControlCode(PrinterDefinitionTable.CMD_START_JOB));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPcl5ControlCodeGeneration() {
|
||||
pd.setPDT(PrinterDefinitionTable.createPcl5PDT());
|
||||
|
||||
pd.startJob();
|
||||
pd.setBold(true);
|
||||
pd.print("BOLD TEXT");
|
||||
pd.setBold(false);
|
||||
pd.setUnderline(true);
|
||||
pd.print("UNDERLINED");
|
||||
pd.setUnderline(false);
|
||||
pd.setItalic(true);
|
||||
pd.print("ITALIC");
|
||||
pd.setItalic(false);
|
||||
pd.setCPI(12);
|
||||
pd.setLPI(8);
|
||||
pd.formFeed();
|
||||
pd.endJob();
|
||||
|
||||
String captured = pd.getCapturedText();
|
||||
assertTrue(captured.contains("\033E")); // Reset / start job
|
||||
assertTrue(captured.contains("\033(s3B")); // Bold on
|
||||
assertTrue(captured.contains("\033(s0B")); // Bold off
|
||||
assertTrue(captured.contains("\033&d0D")); // Underline on
|
||||
assertTrue(captured.contains("\033(s1S")); // Italic on
|
||||
assertTrue(captured.contains("\033&k2S")); // 12 CPI
|
||||
assertTrue(captured.contains("\033&l8D")); // 8 LPI
|
||||
assertTrue(captured.contains("\f")); // Page feed
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEpsonEscPControlCodeGeneration() {
|
||||
pd.setPDT(PrinterDefinitionTable.createEpsonEscPPDT());
|
||||
|
||||
pd.startJob();
|
||||
pd.setDoubleStrike(true);
|
||||
pd.print("DOUBLE STRIKE");
|
||||
pd.setDoubleStrike(false);
|
||||
pd.setDoubleWidth(true);
|
||||
pd.print("DOUBLE WIDTH");
|
||||
pd.setDoubleWidth(false);
|
||||
pd.setSuperscript(true);
|
||||
pd.print("SUPER");
|
||||
pd.setSuperscript(false);
|
||||
pd.setSubscript(true);
|
||||
pd.print("SUB");
|
||||
pd.setSubscript(false);
|
||||
pd.setCPI(10);
|
||||
pd.setLPI(6);
|
||||
pd.endJob();
|
||||
|
||||
String captured = pd.getCapturedText();
|
||||
assertTrue(captured.contains("\033@")); // Init
|
||||
assertTrue(captured.contains("\033G")); // Double strike on
|
||||
assertTrue(captured.contains("\033H")); // Double strike off
|
||||
assertTrue(captured.contains("\033W1")); // Double width on
|
||||
assertTrue(captured.contains("\033W0")); // Double width off
|
||||
assertTrue(captured.contains("\033S0")); // Superscript on
|
||||
assertTrue(captured.contains("\033S1")); // Subscript on
|
||||
assertTrue(captured.contains("\033P")); // 10 CPI
|
||||
assertTrue(captured.contains("\0332")); // 6 LPI
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPDTWithCharacterOverrides() {
|
||||
PrinterDefinitionTable customPdt = new PrinterDefinitionTable("CUSTOM", "Custom Test Table");
|
||||
customPdt.setControlCode("TEST_CMD", "[CMD]");
|
||||
customPdt.setCharOverride('@', "[AT]".getBytes());
|
||||
|
||||
pd.setPDT(customPdt);
|
||||
pd.writeControlCode("TEST_CMD");
|
||||
pd.print('@');
|
||||
pd.print("user@ibm.com");
|
||||
|
||||
String captured = pd.getCapturedText();
|
||||
assertTrue(captured.contains("[CMD]"));
|
||||
assertTrue(captured.contains("[AT]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGddmHostPrintEscapePassthrough() {
|
||||
byte[] gddmEscape = new byte[]{0x1B, '&', 'a', '1', '0', '0', 'V'};
|
||||
pd.processGddmEscape(gddmEscape, 0, gddmEscape.length);
|
||||
|
||||
byte[] captured = pd.getCapturedBytes();
|
||||
assertTrue(captured.length >= gddmEscape.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package haus.nightmare.lib3270j.printer;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for Phase 7 PrintPS3270 EOJ completion, auto-flush timer, and 3270 printer data stream formatting.
|
||||
*/
|
||||
public class PrintPS3270Phase7Test {
|
||||
|
||||
private PrinterConfig config;
|
||||
private PD3270 pd;
|
||||
private EbcdicTranslator translator;
|
||||
private PrintPS3270 printPs;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
config = new PrinterConfig("localhost", 23);
|
||||
config.setAutoFlushTimeoutMs(150); // Fast timeout for testing
|
||||
pd = new PD3270(config);
|
||||
translator = new EbcdicTranslator("037");
|
||||
printPs = new PrintPS3270(config, pd, translator);
|
||||
pd.openPrinter(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessPrintCompleteEOJ() {
|
||||
config.setFormFeedAtEoj(true);
|
||||
config.setAutoFlushOnEoj(true);
|
||||
|
||||
byte[] ebcText = translator.stringToEbcdic("LU3 TEST PAGE");
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(DS3270Constants.CMD_WRITE);
|
||||
stream.write(0x00); // WCC (Start Print = false)
|
||||
stream.write(PrinterConstants.ORDER_SBA);
|
||||
stream.write(0x40); // Row 0 Col 0
|
||||
stream.write(0x40);
|
||||
stream.write(ebcText, 0, ebcText.length);
|
||||
|
||||
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
|
||||
|
||||
// Process EOJ completion
|
||||
printPs.processPrintComplete();
|
||||
|
||||
String captured = pd.getCapturedText();
|
||||
assertTrue(captured.contains("LU3 TEST PAGE"));
|
||||
assertEquals(1, pd.getPageCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoFlushTimerTriggersOnIdle() throws InterruptedException {
|
||||
byte[] ebcText = translator.stringToEbcdic("AUTO FLUSH DATA");
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(DS3270Constants.CMD_WRITE);
|
||||
stream.write(0x00); // WCC with Start Print = 0
|
||||
stream.write(PrinterConstants.ORDER_SBA);
|
||||
stream.write(0x40);
|
||||
stream.write(0x40);
|
||||
stream.write(ebcText, 0, ebcText.length);
|
||||
|
||||
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
|
||||
|
||||
// Wait for auto-flush timer (150ms) to trigger
|
||||
Thread.sleep(300);
|
||||
|
||||
String captured = pd.getCapturedText();
|
||||
assertTrue(captured.contains("AUTO FLUSH DATA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartPrintBitImmediateFlush() {
|
||||
byte[] ebcText = translator.stringToEbcdic("IMMEDIATE PRINT");
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(DS3270Constants.CMD_WRITE);
|
||||
stream.write(PrinterConstants.WCC_START_PRINT_BIT); // WCC with Start Print = 1
|
||||
stream.write(PrinterConstants.ORDER_SBA);
|
||||
stream.write(0x40);
|
||||
stream.write(0x40);
|
||||
stream.write(ebcText, 0, ebcText.length);
|
||||
|
||||
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
|
||||
|
||||
String captured = pd.getCapturedText();
|
||||
assertTrue(captured.contains("IMMEDIATE PRINT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelAutoFlush() {
|
||||
printPs.scheduleAutoFlush();
|
||||
printPs.cancelAutoFlush();
|
||||
// Cancel should succeed without exception
|
||||
assertNotNull(printPs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package haus.nightmare.lib3270j.printer;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Comprehensive tests for Phase 7 SCS (SNA Character String) 28-command protocol engine,
|
||||
* Enhanced Highlights, Vertical Channel Select (VCS), Graphic Error Action (GEA),
|
||||
* Index Return (IRS), and Transparent data (TRN).
|
||||
*/
|
||||
public class PrintSCS3270Phase7Test {
|
||||
|
||||
private PrinterConfig config;
|
||||
private PD3270 pd;
|
||||
private EbcdicTranslator translator;
|
||||
private PrintSCS3270 scs;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
config = new PrinterConfig("localhost", 23);
|
||||
pd = new PD3270(config);
|
||||
translator = new EbcdicTranslator("037");
|
||||
scs = new PrintSCS3270(config, pd, translator);
|
||||
pd.openPrinter(null); // In-memory capture
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAll28SCSCommandsDispatch() {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
// 1. NUL (0x00) & NOP (0x03)
|
||||
stream.write(PrinterConstants.SCS_NUL);
|
||||
stream.write(PrinterConstants.SCS_NOP);
|
||||
|
||||
// 2. Text "A" (0xC1 in Cp037)
|
||||
stream.write(0xC1);
|
||||
|
||||
// 3. BS (0x16) & NBS (0x36)
|
||||
stream.write(PrinterConstants.SCS_BS);
|
||||
stream.write(PrinterConstants.SCS_NBS);
|
||||
|
||||
// 4. SP (0x40) & RSP (0x41)
|
||||
stream.write(PrinterConstants.SCS_SP);
|
||||
stream.write(PrinterConstants.SCS_RSP);
|
||||
|
||||
// 5. HT (0x05)
|
||||
stream.write(PrinterConstants.SCS_HT);
|
||||
|
||||
// 6. Text "B" (0xC2)
|
||||
stream.write(0xC2);
|
||||
|
||||
// 7. CR (0x0D) & RCR (0x07)
|
||||
stream.write(PrinterConstants.SCS_CR);
|
||||
stream.write(PrinterConstants.SCS_RCR);
|
||||
|
||||
// 8. LF (0x25) & NL (0x15) & RNL (0x06)
|
||||
stream.write(PrinterConstants.SCS_LF);
|
||||
stream.write(PrinterConstants.SCS_NL);
|
||||
stream.write(PrinterConstants.SCS_RNLS);
|
||||
|
||||
// 9. VT (0x0B)
|
||||
stream.write(PrinterConstants.SCS_VT);
|
||||
|
||||
// 10. SO (0x0E) & SI (0x0F)
|
||||
stream.write(PrinterConstants.SCS_SO);
|
||||
stream.write(PrinterConstants.SCS_SI);
|
||||
|
||||
// 11. INP (0x24) & ENP (0x14)
|
||||
stream.write(PrinterConstants.SCS_INP);
|
||||
stream.write(0xC3); // Should be ignored when presentation inhibited
|
||||
stream.write(PrinterConstants.SCS_ENP);
|
||||
stream.write(0xC3); // 'C'
|
||||
|
||||
// 12. BEL (0x2F) & POC (0x17)
|
||||
stream.write(PrinterConstants.SCS_BEL);
|
||||
stream.write(PrinterConstants.SCS_POC);
|
||||
|
||||
// 13. IRS (0x33)
|
||||
stream.write(PrinterConstants.SCS_IRS);
|
||||
|
||||
// 14. SUB (0x3F)
|
||||
stream.write(PrinterConstants.SCS_SUB);
|
||||
|
||||
// 15. FF (0x0C)
|
||||
stream.write(PrinterConstants.SCS_FF);
|
||||
|
||||
byte[] payload = stream.toByteArray();
|
||||
scs.processRecord(payload);
|
||||
|
||||
String captured = pd.getCapturedText();
|
||||
assertNotNull(captured);
|
||||
assertTrue(captured.contains("C"));
|
||||
assertTrue(captured.contains("?"));
|
||||
assertTrue(pd.getPageCount() >= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessTRNRawBytePassthrough() {
|
||||
// TRN order: 0x35 <len> <rawBytes>
|
||||
byte[] rawPrinterCodes = new byte[]{0x1B, 0x45, 'R', 'A', 'W', 0x1B, 0x46};
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(PrinterConstants.SCS_TRN);
|
||||
stream.write(rawPrinterCodes.length);
|
||||
stream.write(rawPrinterCodes, 0, rawPrinterCodes.length);
|
||||
|
||||
scs.processHostData(stream.toByteArray(), 0, stream.size());
|
||||
|
||||
byte[] captured = pd.getCapturedBytes();
|
||||
assertTrue(captured.length >= rawPrinterCodes.length);
|
||||
|
||||
// Also test direct processTRN method
|
||||
byte[] directBytes = new byte[]{0x1B, 0x2A, 0x62, 0x30, 0x57};
|
||||
scs.processTRN(directBytes);
|
||||
assertTrue(pd.getByteCount() >= rawPrinterCodes.length + directBytes.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessVCSWithChannelTape() {
|
||||
// Setup channel tape: Channel 2 = line 15, Channel 3 = line 30
|
||||
scs.setChannelLine(2, 15);
|
||||
scs.setChannelLine(3, 30);
|
||||
assertEquals(15, scs.getChannelLine(2));
|
||||
assertEquals(30, scs.getChannelLine(3));
|
||||
|
||||
// Start at line 1
|
||||
assertEquals(1, scs.getCurrentRow());
|
||||
|
||||
// Process VCS for Channel 2
|
||||
scs.processVCS(2);
|
||||
assertEquals(15, scs.getCurrentRow());
|
||||
|
||||
// Process VCS for Channel 3
|
||||
scs.processVCS(3);
|
||||
assertEquals(30, scs.getCurrentRow());
|
||||
|
||||
// Process VCS for Channel 1 (line 1, which is < 30, so forms feed to next page)
|
||||
int initialPages = pd.getPageCount();
|
||||
scs.processVCS(1);
|
||||
assertEquals(1, scs.getCurrentRow());
|
||||
assertEquals(initialPages + 1, pd.getPageCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessGEAGraphicErrorActions() {
|
||||
// 1. GEA Default: replacement '?'
|
||||
config.setGraphicErrorAction(PrinterConstants.GEA_NO_SUBSTITUTION);
|
||||
scs.processSUB();
|
||||
scs.flushLineBuffer();
|
||||
assertTrue(pd.getCapturedText().contains("?"));
|
||||
|
||||
// 2. GEA Substitute specified character ('#')
|
||||
pd.resetCapture();
|
||||
scs.processGEA(PrinterConstants.GEA_SUBSTITUTE_SPECIFIED, '#');
|
||||
scs.processSUB();
|
||||
scs.flushLineBuffer();
|
||||
assertTrue(pd.getCapturedText().contains("#"));
|
||||
|
||||
// 3. GEA 0x2B 0xC8 order
|
||||
pd.resetCapture();
|
||||
byte[] geaOrder = new byte[]{
|
||||
(byte) PrinterConstants.SCS_PREFIX_2B,
|
||||
(byte) PrinterConstants.SCS_GEA,
|
||||
0x02,
|
||||
PrinterConstants.GEA_SUBSTITUTE_SPECIFIED,
|
||||
(byte) '*'
|
||||
};
|
||||
scs.processHostData(geaOrder, 0, geaOrder.length);
|
||||
scs.processSUB();
|
||||
scs.flushLineBuffer();
|
||||
assertTrue(pd.getCapturedText().contains("*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnhancedHighlightAttributes() {
|
||||
// Set PDT to PCL5 so we can verify escape sequences generated
|
||||
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
|
||||
pd.setPDT(pcl5);
|
||||
|
||||
// Test Emphasized (Bold)
|
||||
scs.setEnhancedHighlight(PrinterConstants.SEAC_EMPHASIZED);
|
||||
assertEquals(PrinterConstants.SEAC_EMPHASIZED, scs.getActiveHighlight());
|
||||
String text = pd.getCapturedText();
|
||||
assertTrue(text.contains("\033(s3B")); // PCL Bold on
|
||||
|
||||
// Test Italic
|
||||
pd.resetCapture();
|
||||
scs.setEnhancedHighlight(PrinterConstants.SEAC_ITALIC);
|
||||
assertEquals(PrinterConstants.SEAC_ITALIC, scs.getActiveHighlight());
|
||||
text = pd.getCapturedText();
|
||||
assertTrue(text.contains("\033(s1S")); // PCL Italic on
|
||||
|
||||
// Test Underline
|
||||
pd.resetCapture();
|
||||
scs.setEnhancedHighlight(PrinterConstants.SEAC_UNDERLINE);
|
||||
assertEquals(PrinterConstants.SEAC_UNDERLINE, scs.getActiveHighlight());
|
||||
text = pd.getCapturedText();
|
||||
assertTrue(text.contains("\033&d0D")); // PCL Underline on
|
||||
|
||||
// Test Double-Width
|
||||
pd.resetCapture();
|
||||
scs.startDoubleWidthCharacters();
|
||||
assertTrue(scs.isDoubleWidth());
|
||||
text = pd.getCapturedText();
|
||||
assertTrue(text.contains("\033&k1W"));
|
||||
|
||||
scs.endDoubleWidthCharacters();
|
||||
assertFalse(scs.isDoubleWidth());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExplicitSCSNamedMethods() {
|
||||
scs.processNL();
|
||||
scs.processCR();
|
||||
scs.processLF();
|
||||
scs.processFF();
|
||||
scs.processBS();
|
||||
scs.processNBS();
|
||||
scs.processHT();
|
||||
scs.processVT();
|
||||
scs.processRNL();
|
||||
scs.processRCR();
|
||||
scs.processENP();
|
||||
scs.processINP();
|
||||
scs.processBEL();
|
||||
scs.processIRS();
|
||||
scs.processSO();
|
||||
scs.processSI();
|
||||
scs.processSCS(1);
|
||||
scs.processPOC(new byte[]{0x01});
|
||||
|
||||
assertTrue(pd.getPageCount() >= 1);
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package haus.nightmare.lib3270j.printer;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* End-to-end tests for Phase 7 Telnet3270EP printer protocol engine, LU1 and LU3 sessions,
|
||||
* response headers, EOJ handling, listeners, and Telnet3270EPClient integration.
|
||||
*/
|
||||
public class Telnet3270EPPhase7FullTest {
|
||||
|
||||
private PrinterConfig config;
|
||||
private PD3270 pd;
|
||||
private EbcdicTranslator translator;
|
||||
private Telnet3270EP ep;
|
||||
private Telnet3270EPClient client;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
config = new PrinterConfig("localhost", 23, "PRT01");
|
||||
config.setAssociatedDisplayLuName("DSP01");
|
||||
config.setFormFeedAtEoj(true);
|
||||
config.setAutoFlushOnEoj(true);
|
||||
|
||||
pd = new PD3270(config);
|
||||
translator = new EbcdicTranslator("037");
|
||||
ep = new Telnet3270EP(config, pd, translator);
|
||||
client = new Telnet3270EPClient(config);
|
||||
pd.openPrinter(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLU1SCSPrintSessionFlow() {
|
||||
// 1. Process BIND for LU Type 1 (SCS)
|
||||
ep.process_bind(PrinterConstants.LU_TYPE_1_SCS);
|
||||
assertEquals(PrinterConstants.LU_TYPE_1_SCS, ep.getActiveLuType());
|
||||
assertEquals(PrinterConstants.STATUS_PRINTER_READY, ep.getStatusCode());
|
||||
|
||||
// 2. Feed SCS print records
|
||||
byte[] hello = translator.stringToEbcdic("REPORT TITLE");
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(hello, 0, hello.length);
|
||||
stream.write(PrinterConstants.SCS_NL);
|
||||
byte[] data = translator.stringToEbcdic("LINE ITEM 1");
|
||||
stream.write(data, 0, data.length);
|
||||
stream.write(PrinterConstants.SCS_NL);
|
||||
|
||||
byte[] payload = stream.toByteArray();
|
||||
ep.getSCS().processHostData(payload, 0, payload.length);
|
||||
|
||||
// 3. EOJ
|
||||
ep.sendEOJ(true);
|
||||
|
||||
String text = pd.getCapturedText();
|
||||
assertTrue(text.contains("REPORT TITLE"));
|
||||
assertTrue(text.contains("LINE ITEM 1"));
|
||||
assertEquals(1, pd.getPageCount());
|
||||
assertEquals(PrinterConstants.STATUS_JOB_COMPLETE, ep.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLU33270PrintSessionFlow() {
|
||||
// 1. Process BIND for LU Type 3 (3270 Data Stream)
|
||||
ep.process_bind(PrinterConstants.LU_TYPE_3_DS);
|
||||
assertEquals(PrinterConstants.LU_TYPE_3_DS, ep.getActiveLuType());
|
||||
|
||||
// 2. Feed 3270 print DS
|
||||
byte[] hello = translator.stringToEbcdic("LU3 REPORT CONTENT");
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(0xF5); // ERASE WRITE
|
||||
stream.write(0x00); // WCC
|
||||
stream.write(PrinterConstants.ORDER_SBA);
|
||||
stream.write(0x40);
|
||||
stream.write(0x40);
|
||||
stream.write(hello, 0, hello.length);
|
||||
|
||||
byte[] payload = stream.toByteArray();
|
||||
ep.getPrintPS().process3270PrintDS(payload, 0, payload.length);
|
||||
|
||||
// 3. EOJ
|
||||
ep.sendEOJ(true);
|
||||
|
||||
String text = pd.getCapturedText();
|
||||
assertTrue(text.contains("LU3 REPORT CONTENT"));
|
||||
assertEquals(1, pd.getPageCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientFacadePDTAndChannelConfiguration() {
|
||||
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
|
||||
client.setPDT(pcl5);
|
||||
assertEquals("PCL_5", client.getPDT().getName());
|
||||
|
||||
client.setChannelLine(4, 25);
|
||||
assertEquals(25, client.getChannelLine(4));
|
||||
|
||||
byte[] text = translator.stringToEbcdic("CLIENT CONVENIENCE TEST");
|
||||
client.printDirectBytes(text, 0, text.length);
|
||||
client.flush();
|
||||
|
||||
String captured = client.getPD().getCapturedText();
|
||||
assertTrue(captured.contains("CLIENT CONVENIENCE TEST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrintSessionEventListeners() {
|
||||
AtomicBoolean jobStarted = new AtomicBoolean(false);
|
||||
AtomicBoolean jobComplete = new AtomicBoolean(false);
|
||||
AtomicInteger pagesReceived = new AtomicInteger(0);
|
||||
|
||||
PrintSessionListener listener = new PrintSessionListener() {
|
||||
@Override
|
||||
public void onPrintJobStarted(PrintSessionEvent event) {
|
||||
jobStarted.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrintJobData(PrintSessionEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrintJobPageComplete(PrintSessionEvent event) {
|
||||
pagesReceived.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrintJobComplete(PrintSessionEvent event) {
|
||||
jobComplete.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrinterStatusChanged(PrintSessionEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrinterError(PrintSessionEvent event) {
|
||||
}
|
||||
};
|
||||
|
||||
ep.addPrintListener(listener);
|
||||
|
||||
ep.firePrintJobStarted("JOB01");
|
||||
ep.firePrintJobPageComplete(1);
|
||||
ep.sendEOJ(true);
|
||||
|
||||
assertTrue(jobStarted.get());
|
||||
assertTrue(jobComplete.get());
|
||||
assertEquals(1, pagesReceived.get());
|
||||
|
||||
ep.removePrintListener(listener);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user