diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java index f3a029c..bbd25dd 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java @@ -129,14 +129,27 @@ public class ConnectDialog extends JDialog { "870 - Eastern Europe / Latin-2", "871 - Iceland", "875 - Greece (Greek)", - "1026 - Turkey (Turkish)", + "1026 - Turkey (Turkish Latin-5)", + "1155 - Turkey (Turkish Latin-5 Euro \u20AC)", + "905 - Turkey (Turkish Latin-3)", + "420 - Arabic Bilingual", + "424 - Hebrew (with Lowercase)", + "803 - Hebrew Old / Standard", + "838 - Thai", + "1160 - Thai (Euro \u20AC)", + "1025 - Cyrillic Multilingual", + "1123 - Cyrillic Ukraine", + "1154 - Cyrillic Multilingual (Euro \u20AC)", + "880 - Cyrillic Russian", "1140 - US / Canada (Euro \u20AC)", "1141 - Germany / Austria (Euro \u20AC)", "1148 - International (Euro \u20AC)", "930 - Japanese Katakana Mixed DBCS", "939 - Japanese Latin Mixed DBCS", "935 - Simplified Chinese Mixed DBCS", + "1388 - Simplified Chinese Extended Mixed DBCS", "937 - Traditional Chinese Mixed DBCS", + "1371 - Traditional Chinese Extended Mixed DBCS", "933 - Korean Mixed DBCS" }; codePageCombo = new JComboBox<>(commonCodePages); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java index 0a01b8a..5645edc 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -730,6 +730,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } } } else if (e.getID() == KeyEvent.KEY_PRESSED) { + if (client.getNvtProcessor() != null && client.getNvtProcessor().isApplicationKeypad()) { + String kp = client.getNvtProcessor().mapKeypadKey(e.getKeyCode()); + if (kp != null) { + try { + client.sendNVTString(kp); + } catch (Exception ignored) {} + e.consume(); + return; + } + } if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) { int code = e.getKeyCode(); if (code >= KeyEvent.VK_A && code <= KeyEvent.VK_Z) { @@ -838,12 +848,20 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { if (client != null && client.getConnectionState().isFullSession()) { if (client.getConnectionState().isNvt()) { try { - switch (dir) { - case "up": client.sendNVTString("\u001B[A"); break; - case "down": client.sendNVTString("\u001B[B"); break; - case "left": client.sendNVTString("\u001B[D"); break; - case "right": client.sendNVTString("\u001B[C"); break; - case "home": client.sendNVTString("\u001B[H"); break; + int vk = KeyEvent.VK_UP; + if ("down".equals(dir)) vk = KeyEvent.VK_DOWN; + else if ("left".equals(dir)) vk = KeyEvent.VK_LEFT; + else if ("right".equals(dir)) vk = KeyEvent.VK_RIGHT; + else if ("home".equals(dir)) vk = KeyEvent.VK_HOME; + boolean handled = client.sendNVTKey(vk, '\0', false, false, false); + if (!handled) { + switch (dir) { + case "up": client.sendNVTString("\u001B[A"); break; + case "down": client.sendNVTString("\u001B[B"); break; + case "left": client.sendNVTString("\u001B[D"); break; + case "right": client.sendNVTString("\u001B[C"); break; + case "home": client.sendNVTString("\u001B[H"); break; + } } } catch (Exception ignored) {} return; @@ -863,7 +881,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { if (client != null && client.getConnectionState().isFullSession()) { if (client.getConnectionState().isNvt()) { try { - client.sendNVTString(getNvtFunctionKeySequence(n)); + if (client.getNvtProcessor() != null) { + client.sendNVTString(client.getNvtProcessor().getFunctionKeySequence(n)); + } else { + client.sendNVTString(getNvtFunctionKeySequence(n)); + } } catch (Exception ignored) {} return; } @@ -1093,6 +1115,37 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { if (client != null) { setupGraphicsPlaneRenderer(); updateCellSize(); + + client.setNvtClipboardHandler(new haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler() { + @Override + public String getClipboardText() { + try { + return (String) Toolkit.getDefaultToolkit().getSystemClipboard() + .getData(DataFlavor.stringFlavor); + } catch (Exception e) { + return ""; + } + } + + @Override + public void setClipboardText(String text) { + if (text != null) { + try { + StringSelection ss = new StringSelection(text); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null); + } catch (Exception ignored) {} + } + } + }); + + client.addNvtTitleListener(title -> { + SwingUtilities.invokeLater(() -> { + Container top = getTopLevelAncestor(); + if (top instanceof JFrame) { + ((JFrame) top).setTitle(title != null && !title.isEmpty() ? ("j3270 - " + title) : "j3270"); + } + }); + }); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionState.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionState.java index 7fa71e7..e096617 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionState.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionState.java @@ -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; + } + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java index cc2cb01..e235d1f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java @@ -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. */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java index a1e61bf..2f8773a 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java @@ -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); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java index d7155b5..7582986 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java @@ -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. */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1025.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1025.java new file mode 100644 index 0000000..410369e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1025.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1123.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1123.java new file mode 100644 index 0000000..8e04657 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1123.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1154.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1154.java new file mode 100644 index 0000000..05a0c57 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1154.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1155.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1155.java new file mode 100644 index 0000000..034c44c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1155.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1160.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1160.java new file mode 100644 index 0000000..27e1694 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1160.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1371.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1371.java new file mode 100644 index 0000000..834cec9 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1371.java @@ -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"); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1388.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1388.java new file mode 100644 index 0000000..d4b9153 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1388.java @@ -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"); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp420.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp420.java new file mode 100644 index 0000000..c637ec1 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp420.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp424.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp424.java new file mode 100644 index 0000000..3e17f64 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp424.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp803.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp803.java new file mode 100644 index 0000000..ffaa8d8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp803.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp838.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp838.java new file mode 100644 index 0000000..4b64b75 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp838.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp880.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp880.java new file mode 100644 index 0000000..af83641 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp880.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp905.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp905.java new file mode 100644 index 0000000..b5e1c33 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp905.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java index 5bf8e41..b222048 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java @@ -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 customEbcdicToUnicode = new ConcurrentHashMap<>(); + private final Map 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 ebcToUni, Map uniToEbc) { + if (ebcToUni != null) { + for (Map.Entry entry : ebcToUni.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + setCustomOverride(entry.getKey(), entry.getValue()); + } + } + } + if (uniToEbc != null) { + for (Map.Entry 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 getCustomEbcdicToUnicodeOverrides() { + return Collections.unmodifiableMap(new HashMap<>(customEbcdicToUnicode)); + } + + /** + * Get an unmodifiable view of active Unicode-to-EBCDIC custom overrides. + */ + public Map 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); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index 00845db..ae6d525 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -109,6 +109,10 @@ public class DataStreamProcessor { screenListeners.add(l); } + public void removeScreenUpdateListener(ScreenUpdateListener l) { + screenListeners.remove(l); + } + /** * Process a 3270 data stream record. * diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConnection.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConnection.java new file mode 100644 index 0000000..16e08e9 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConnection.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConstants.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConstants.java new file mode 100644 index 0000000..58876de --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConstants.java @@ -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 { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLErrors.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLErrors.java new file mode 100644 index 0000000..8e173a6 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLErrors.java @@ -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 { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLException.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLException.java new file mode 100644 index 0000000..bfed52c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLException.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLField.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLField.java new file mode 100644 index 0000000..d182b91 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLField.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLFieldList.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLFieldList.java new file mode 100644 index 0000000..66ba389 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLFieldList.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIA.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIA.java new file mode 100644 index 0000000..bea2ab5 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIA.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPS.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPS.java new file mode 100644 index 0000000..1115390 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPS.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenDesc.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenDesc.java new file mode 100644 index 0000000..ca80eff --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenDesc.java @@ -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(); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLSession.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLSession.java new file mode 100644 index 0000000..3e9c62b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLSession.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLXfer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLXfer.java new file mode 100644 index 0000000..5a8d77a --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLXfer.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommEvent.java new file mode 100644 index 0000000..b6e59d6 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommEvent.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommListener.java new file mode 100644 index 0000000..2ba9992 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommListener.java @@ -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 { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommNotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommNotify.java new file mode 100644 index 0000000..67a0585 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLCommNotify.java @@ -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 { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIAEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIAEvent.java new file mode 100644 index 0000000..3fb9808 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIAEvent.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIAListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIAListener.java new file mode 100644 index 0000000..1427469 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIAListener.java @@ -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 { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSEvent.java new file mode 100644 index 0000000..83bd4e7 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSEvent.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSListener.java new file mode 100644 index 0000000..7525b89 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSListener.java @@ -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 { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommEvent.java new file mode 100644 index 0000000..b8cdc6e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommEvent.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommListener.java new file mode 100644 index 0000000..617a1e3 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommListener.java @@ -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) {} +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommNotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommNotify.java new file mode 100644 index 0000000..44a2a87 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommNotify.java @@ -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); +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java new file mode 100644 index 0000000..a9ece00 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java @@ -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 commListeners = new CopyOnWriteArrayList<>(); + private final List 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()); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErrors.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErrors.java new file mode 100644 index 0000000..2472262 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErrors.java @@ -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; +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLException.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLException.java new file mode 100644 index 0000000..7b61aa8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLException.java @@ -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() + "]"; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java index 3bbf7c6..1db9643 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java @@ -19,6 +19,7 @@ public class ECLOIA implements ECLConstants { private final InputProcessor inputProcessor; private final TelnetFSM fsm; private final List listeners = new ArrayList<>(); + private final List 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); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAEvent.java new file mode 100644 index 0000000..cb6d957 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAEvent.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAListener.java new file mode 100644 index 0000000..788f7eb --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAListener.java @@ -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) {} +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java index 1d2625c..738acd1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java @@ -365,6 +365,69 @@ public class ECLPS implements ECLConstants { return charsPasted; } + // ========== ECLPS Event Listener Management ========== + + private final java.util.List 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); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java new file mode 100644 index 0000000..9905da6 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSListener.java new file mode 100644 index 0000000..0832127 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSListener.java @@ -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) {} +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java new file mode 100644 index 0000000..fe69072 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java @@ -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 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(); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java new file mode 100644 index 0000000..23fb156 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java @@ -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()); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java index f0a5d71..003d5e2 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java @@ -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) null, callback); + } + + /** + * Batch directory file download into a destination directory. + */ + public void getFiles(String filter, String localDirectory, FileTransferHostDirectoryInterface callback) { + List fileList = new ArrayList<>(); + getFiles(filter, fileList, new FileTransferHostDirectoryInterface() { + @Override + public void onDirectoryLoaded(List 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 localFiles, java.util.Vector hostFiles, + FileTransferHostDirectoryInterface callback) { + List fileList = new ArrayList<>(); + getFiles(hostQuery, fileList, new FileTransferHostDirectoryInterface() { + @Override + public void onDirectoryLoaded(List 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); } /** diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/CMSPrintXfer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/CMSPrintXfer.java new file mode 100644 index 0000000..1e7ddad --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/CMSPrintXfer.java @@ -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 parseQuerySpoolOutput(String text, String defaultDevice) { + List 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 parseQueryReaderOutput(String text) { + return parseQuerySpoolOutput(text, "RDR"); + } + + public static List 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; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java index 01eff83..2e852d0 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java @@ -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 ========== diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java index 3a2964b..78aedd1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java @@ -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. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index e65a3ca..e42b257 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -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()); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java index c0465cd..8beedbb 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java @@ -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 screenListeners = new CopyOnWriteArrayList<>(); + private final List 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 [ ; 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 }); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PD3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PD3270.java index 7a6f0a7..e267402 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PD3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PD3270.java @@ -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(); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java index 5ee318a..8626e3a 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java @@ -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; + } } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java index 4254f45..403eefd 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java @@ -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; } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConfig.java index 5370ee3..e6629d1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConfig.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConfig.java @@ -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(); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConstants.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConstants.java index 26e58fb..fc37d49 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConstants.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConstants.java @@ -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 ) 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 ) + public static final int SCS_IRS = 0x33; // Index Return + public static final int SCS_TRN = 0x35; // Transparent Stream (0x35 ) + 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 ) // 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) diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterDefinitionTable.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterDefinitionTable.java new file mode 100644 index 0000000..d9172cf --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterDefinitionTable.java @@ -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 controlCodes = new HashMap<>(); + private final Map 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; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java index 4f8b98a..c8afbd1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java @@ -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()); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EPClient.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EPClient.java index ccdb594..fd3d3ec 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EPClient.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EPClient.java @@ -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); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/APLTranslationPhase8Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/APLTranslationPhase8Test.java new file mode 100644 index 0000000..397fd00 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/APLTranslationPhase8Test.java @@ -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); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/CustomTranslationOverridePhase8Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/CustomTranslationOverridePhase8Test.java new file mode 100644 index 0000000..86ffe3d --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/CustomTranslationOverridePhase8Test.java @@ -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 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); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/DBCSSOSIEscapingPhase8Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/DBCSSOSIEscapingPhase8Test.java new file mode 100644 index 0000000..94f3c8c --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/DBCSSOSIEscapingPhase8Test.java @@ -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); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/ExtendedCodePagesPhase8Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/ExtendedCodePagesPhase8Test.java new file mode 100644 index 0000000..05e57c9 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/charset/ExtendedCodePagesPhase8Test.java @@ -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")); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLSessionPhase10Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLSessionPhase10Test.java new file mode 100644 index 0000000..884ddf4 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLSessionPhase10Test.java @@ -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 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 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 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()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLXferTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLXferTest.java index 108f0d0..f63cb87 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLXferTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLXferTest.java @@ -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 entries) { + loaded[0] = true; + } + + @Override + public void onDirectoryError(String errorMessage) {} + }); + + assertTrue(loaded[0]); + } + + @Test + public void testGetFilesVectorOverload() { + java.util.Vector localFiles = new java.util.Vector<>(); + java.util.Vector 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 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()); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/CMSPrintXferTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/CMSPrintXferTest.java new file mode 100644 index 0000000..dc1f149 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/CMSPrintXferTest.java @@ -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 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 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()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java index feb6146..9b5b495 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java @@ -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); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java index 1e0b990..6cfbdc0 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java @@ -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()); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java index d900941..5639563 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java @@ -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 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 sent = new java.util.concurrent.atomic.AtomicReference<>(); InputProcessor input = new InputProcessor(screen, translator, null) { diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/nvt/NvtProcessorPhase9Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/nvt/NvtProcessorPhase9Test.java new file mode 100644 index 0000000..8bfccda --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/nvt/NvtProcessorPhase9Test.java @@ -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 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 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); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PD3270PDTPhase7Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PD3270PDTPhase7Test.java new file mode 100644 index 0000000..51bcf25 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PD3270PDTPhase7Test.java @@ -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); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintPS3270Phase7Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintPS3270Phase7Test.java new file mode 100644 index 0000000..d7183b7 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintPS3270Phase7Test.java @@ -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); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintSCS3270Phase7Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintSCS3270Phase7Test.java new file mode 100644 index 0000000..247ee03 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintSCS3270Phase7Test.java @@ -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 + 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); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/Telnet3270EPPhase7FullTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/Telnet3270EPPhase7FullTest.java new file mode 100644 index 0000000..6397846 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/Telnet3270EPPhase7FullTest.java @@ -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); + } +}