From 5049d8df748239c744e5fef3d868bda8f291d9ff Mon Sep 17 00:00:00 2001 From: Rudi Date: Fri, 18 Sep 2026 16:48:49 +0000 Subject: [PATCH] Add facade classes for 3rd party programs --- .../haus/nightmare/j3270/ui/StatusBar.java | 12 + .../nightmare/j3270/ui/StatusBarTest.java | 45 ++ .../com/ibm/eNetwork/ECL/ECLCommEvent.java | 45 ++ .../com/ibm/eNetwork/ECL/ECLCommNotify.java | 10 + .../com/ibm/eNetwork/ECL/ECLConnection.java | 79 +++ .../com/ibm/eNetwork/ECL/ECLConstants.java | 138 ++++ .../java/com/ibm/eNetwork/ECL/ECLErr.java | 54 ++ .../java/com/ibm/eNetwork/ECL/ECLField.java | 137 ++++ .../com/ibm/eNetwork/ECL/ECLFieldList.java | 113 ++++ .../ibm/eNetwork/ECL/ECLHostPrintSession.java | 39 ++ .../java/com/ibm/eNetwork/ECL/ECLOIA.java | 130 ++++ .../com/ibm/eNetwork/ECL/ECLOIAEvent.java | 80 +++ .../com/ibm/eNetwork/ECL/ECLOIANotify.java | 17 + .../main/java/com/ibm/eNetwork/ECL/ECLPS.java | 514 +++++++++++++++ .../com/ibm/eNetwork/ECL/ECLPSNotify.java | 10 + .../com/ibm/eNetwork/ECL/ECLPSUpdate.java | 90 +++ .../com/ibm/eNetwork/ECL/ECLScreenDesc.java | 21 + .../com/ibm/eNetwork/ECL/ECLScreenNotify.java | 8 + .../com/ibm/eNetwork/ECL/ECLScreenReco.java | 78 +++ .../ibm/eNetwork/ECL/ECLScreenRecoEvent.java | 39 ++ .../java/com/ibm/eNetwork/ECL/ECLSession.java | 189 ++++++ .../java/com/ibm/eNetwork/ECL/ECLXfer.java | 54 ++ .../com/ibm/eNetwork/ECL/event/ECLEvent.java | 25 + .../ibm/eNetwork/ECL/event/ECLPSEvent.java | 126 ++++ .../ibm/eNetwork/ECL/event/ECLPSListener.java | 13 + .../ibm/eNetwork/ECL/event/ECLXferEvent.java | 48 ++ .../eNetwork/ECL/event/ECLXferListener.java | 13 + .../eNetwork/ECL/screenreco/ECLSDAttrib.java | 20 + .../eNetwork/ECL/screenreco/ECLSDBlock.java | 24 + .../ECL/screenreco/ECLSDBlockString.java | 16 + .../ECL/screenreco/ECLSDChecksum.java | 20 + .../eNetwork/ECL/screenreco/ECLSDCursor.java | 20 + .../eNetwork/ECL/screenreco/ECLSDFields.java | 20 + .../ECL/screenreco/ECLSDInputFields.java | 20 + .../ibm/eNetwork/ECL/screenreco/ECLSDOIA.java | 24 + .../eNetwork/ECL/screenreco/ECLSDString.java | 36 ++ .../ECL/screenreco/ECLScreenDescriptor.java | 16 + .../nightmare/lib3270j/ecl/ECLConnection.java | 28 + .../haus/nightmare/lib3270j/ecl/ECLErr.java | 67 +- .../haus/nightmare/lib3270j/ecl/ECLField.java | 14 +- .../nightmare/lib3270j/ecl/ECLFieldList.java | 67 +- .../haus/nightmare/lib3270j/ecl/ECLOIA.java | 57 ++ .../haus/nightmare/lib3270j/ecl/ECLPS.java | 480 ++++++++++++-- .../nightmare/lib3270j/ecl/ECLPSEvent.java | 8 + .../nightmare/lib3270j/ecl/ECLScreenDesc.java | 604 ++++++++++++------ .../nightmare/lib3270j/ecl/ECLScreenReco.java | 32 +- .../lib3270j/ecl/screenreco/ECLSDAttrib.java | 126 ++++ .../lib3270j/ecl/screenreco/ECLSDBlock.java | 204 ++++++ .../ecl/screenreco/ECLSDBlockString.java | 40 ++ .../ecl/screenreco/ECLSDChecksum.java | 71 ++ .../lib3270j/ecl/screenreco/ECLSDCursor.java | 77 +++ .../lib3270j/ecl/screenreco/ECLSDFields.java | 47 ++ .../ecl/screenreco/ECLSDInputFields.java | 54 ++ .../lib3270j/ecl/screenreco/ECLSDOIA.java | 92 +++ .../lib3270j/ecl/screenreco/ECLSDString.java | 222 +++++++ .../ecl/screenreco/ECLScreenDescriptor.java | 110 ++++ .../lib3270j/ecl/util/SendKeyTokenizer.java | 156 +++++ .../lib3270j/input/InputProcessor.java | 309 ++++++++- .../nightmare/lib3270j/printer/DS3270P.java | 2 +- .../nightmare/lib3270j/printer/PD3270.java | 13 + .../lib3270j/printer/PrintPS3270.java | 43 +- .../lib3270j/printer/PrintPS3270DB.java | 200 +++++- .../lib3270j/printer/PrintSCS3270.java | 183 +++++- .../lib3270j/printer/PrintSCS3270DB.java | 34 +- .../lib3270j/printer/PrinterConstants.java | 34 +- .../nightmare/lib3270j/ecl/ECLOIATest.java | 47 ++ .../lib3270j/ecl/Phase22HaclFacadesTest.java | 291 +++++++++ .../ecl/Phase23ScreenRecoCompositeTest.java | 324 ++++++++++ .../ecl/SendKeyTokenizerPhase21Test.java | 271 ++++++++ .../printer/Phase20HostPrintingTest.java | 391 ++++++++++++ 70 files changed, 6687 insertions(+), 354 deletions(-) create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommEvent.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommNotify.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConnection.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConstants.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLErr.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLFieldList.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLHostPrintSession.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIA.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIAEvent.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIANotify.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSNotify.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSUpdate.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenDesc.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenNotify.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenReco.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenRecoEvent.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLSession.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLXfer.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLEvent.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSEvent.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSListener.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferEvent.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferListener.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDAttrib.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlock.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlockString.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDChecksum.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDCursor.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDFields.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDInputFields.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDOIA.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDString.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLScreenDescriptor.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDAttrib.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDBlock.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDBlockString.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDChecksum.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDCursor.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDFields.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDInputFields.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDOIA.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDString.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLScreenDescriptor.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/util/SendKeyTokenizer.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase22HaclFacadesTest.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase23ScreenRecoCompositeTest.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/SendKeyTokenizerPhase21Test.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/printer/Phase20HostPrintingTest.java diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java index eb941d9..54876ed 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java @@ -29,6 +29,7 @@ public class StatusBar extends JPanel { private Telnet3270Client client; private TerminalPanel terminalPanel; + private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener; public StatusBar() { setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); @@ -131,8 +132,15 @@ public class StatusBar extends JPanel { } public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) { + if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) { + this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener); + } this.client = client; this.terminalPanel = terminalPanel; + if (this.client != null && this.client.getOIA() != null) { + this.oiaNotifyListener = event -> updateStatus(); + this.client.getOIA().registerOIAEvent(this.oiaNotifyListener); + } updateStatus(); } @@ -388,4 +396,8 @@ public class StatusBar extends JPanel { cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr)); cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme)); } + + public String getLockStatusText() { + return lockStatus != null ? lockStatus.getText() : ""; + } } diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/StatusBarTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/StatusBarTest.java index 68a46a6..9821f36 100644 --- a/j3270/src/test/java/haus/nightmare/j3270/ui/StatusBarTest.java +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/StatusBarTest.java @@ -59,4 +59,49 @@ public class StatusBarTest { // Ignored in headless environments } } + + @Test + public void testStatusBarXSystemAndXProt() { + try { + ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false); + Telnet3270Client client = new Telnet3270Client(config); + client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270); + + StatusBar statusBar = new StatusBar(); + statusBar.setClient(client, null); + statusBar.updateStatus(); + + // Initially not locked + assertEquals("", statusBar.getLockStatusText()); + + // 1. Setup protected field and type into it -> triggers X PROT + client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT); + client.getScreenBuffer().setCursorAddress(1); + client.typeCharacter('A'); + statusBar.updateStatus(); + assertEquals("X PROT", statusBar.getLockStatusText()); + + // 2. Reset -> clears X PROT + client.reset(); + statusBar.updateStatus(); + assertEquals("", statusBar.getLockStatusText()); + + // 3. Send Enter -> triggers X SYSTEM + client.sendEnter(); + statusBar.updateStatus(); + assertEquals("X SYSTEM", statusBar.getLockStatusText()); + + // 4. Host unlocks keyboard + client.getInputProcessor().setKeyboardLocked(false); + statusBar.updateStatus(); + assertEquals("", statusBar.getLockStatusText()); + + // 5. Subsequent Enter -> triggers X SYSTEM again + client.sendEnter(); + statusBar.updateStatus(); + assertEquals("X SYSTEM", statusBar.getLockStatusText()); + } catch (HeadlessException e) { + // Ignored in headless environments + } + } } diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommEvent.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommEvent.java new file mode 100644 index 0000000..c1e86b6 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommEvent.java @@ -0,0 +1,45 @@ +package com.ibm.eNetwork.ECL; + +import com.ibm.eNetwork.ECL.event.ECLEvent; + +/** + * Communications event matching IBM Host On-Demand ECLCommEvent. + */ +public class ECLCommEvent extends ECLEvent { + + private final boolean status; + private final int eventType; + + public ECLCommEvent(Object object, boolean status) { + super(object); + this.status = status; + this.eventType = status ? 1 : 2; + } + + public ECLCommEvent(Object object, int eventType, boolean status) { + super(object); + this.eventType = eventType; + this.status = status; + } + + public boolean connected() { + return this.status; + } + + public boolean isConnected() { + return this.status; + } + + public int getEventType() { + return this.eventType; + } + + public ECLConnection GetConnection() { + Object src = GetSource(); + if (src instanceof ECLConnection) return (ECLConnection) src; + if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) { + return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src); + } + return null; + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommNotify.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommNotify.java new file mode 100644 index 0000000..1f2c754 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLCommNotify.java @@ -0,0 +1,10 @@ +package com.ibm.eNetwork.ECL; + +/** + * Communications notification interface matching IBM Host On-Demand ECLCommNotify. + */ +public interface ECLCommNotify { + void NotifyEvent(ECLConnection conn, boolean connected); + default void NotifyStop(ECLConnection conn, int reason) {} + default void NotifyError(ECLConnection conn, ECLErr err) {} +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConnection.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConnection.java new file mode 100644 index 0000000..df2f80a --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConnection.java @@ -0,0 +1,79 @@ +package com.ibm.eNetwork.ECL; + +import java.io.IOException; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Communications and connection management facade matching IBM Host On-Demand ECLConnection. + */ +public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants { + + private final List commNotifies = new CopyOnWriteArrayList<>(); + + public ECLConnection() { + super(); + setupListener(); + } + + public ECLConnection(Properties props) { + super(props); + setupListener(); + } + + public ECLConnection(String host, int port) { + super(host, port); + setupListener(); + } + + public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) { + super(session); + setupListener(); + } + + public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) { + super(delegate != null ? delegate.getSession() : null); + setupListener(); + } + + private void setupListener() { + RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() { + @Override + public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) { + boolean conn = event != null && event.isConnected(); + for (ECLCommNotify n : commNotifies) { + try { + n.NotifyEvent(ECLConnection.this, conn); + } catch (Exception ignored) {} + } + } + }); + } + + public void RegisterCommEvent(ECLCommNotify notify) { + if (notify != null && !commNotifies.contains(notify)) { + commNotifies.add(notify); + } + } + + public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); } + + public void UnregisterCommEvent(ECLCommNotify notify) { + if (notify != null) { + commNotifies.remove(notify); + } + } + + public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); } + + @Override + public void StartCommunication() throws IOException { + super.StartCommunication(); + } + + @Override + public void StopCommunication() { + super.StopCommunication(); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConstants.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConstants.java new file mode 100644 index 0000000..c66e810 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLConstants.java @@ -0,0 +1,138 @@ +package com.ibm.eNetwork.ECL; + +/** + * IBM Host Access Class Library (HACL) common constants. + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants. + */ +public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants { + + // Presentation space logical planes (HoD bitmask values) + int TEXT_PLANE = 1; + int COLOR_PLANE = 2; + int DBCS_PLANE = 4; + int GRID_PLANE = 8; + int FIELD_PLANE = 16; + int EXFIELD_PLANE = 32; + int NLS_PLANE = 64; + int HOST_PLANE = 128; + int RAWTEXT_PLANE = 256; + int COLOR_ATTRIBUTES_PLANE = 512; + int ALL_PLANES = 319; + + // Search directions + int SEARCH_FORWARD = 1; + int SEARCH_BACKWARD = 2; + + // Field filters + int ALL_FIELDS = 0; + int MODIFIED_FIELD = 1; + int ALPHANUM_FIELD = 2; + int HILIGHT_FIELD = 16; + int PROTECTED_FIELD = 32; + int DISPLAYABLE_FIELD = 64; + int PENDETECTABLE_FIELD = 128; + int UNMODIFIED_FIELD = 256; + int NUMERIC_FIELD = 512; + int LOLIGHT_FIELD = 4096; + int UNPROTECTED_FIELD = 8192; + int NON_DISPLAYABLE_FIELD = 16384; + int NON_PENDETECTABLE_FIELD = 32768; + + // Standard Mnemonics Constants + String ENTER_STR = "[enter]"; + String CLEAR_STR = "[clear]"; + String RESET_STR = "[reset]"; + String TAB_STR = "[tab]"; + String FWDTAB_STR = "[tab]"; + String BACKTAB_STR = "[backtab]"; + String BACKSP_STR = "[backspace]"; + String DELCHAR_STR = "[delete]"; + String INSERT_STR = "[insert]"; + String HOME_STR = "[home]"; + String NEWLINE_STR = "[newline]"; + String ENDLINE_STR = "[eof]"; + String ERASEEOF_STR = "[eraseeof]"; + String ERASEFLD_STR = "[erasefld]"; + String ERASEINPUT_STR = "[erinp]"; + String ATTN_STR = "[attn]"; + String SYSREQ_STR = "[sysreq]"; + String DUP_STR = "[dup]"; + String FLDMRK_STR = "[fieldmark]"; + String PA1_STR = "[pa1]"; + String PA2_STR = "[pa2]"; + String PA3_STR = "[pa3]"; + String CURUP_STR = "[up]"; + String CURDOWN_STR = "[down]"; + String CURLEFT_STR = "[left]"; + String CURRIGHT_STR = "[right]"; + String PAGEUP_STR = "[pageup]"; + String PAGEDWN_STR = "[pagedn]"; + String COPY_STR = "[copy]"; + String PASTE_STR = "[paste]"; + String CUT_STR = "[cut]"; + String SELECTALL_STR = "[selectall]"; + + // Function keys F1 - F24 + String F1_STR = "[pf1]"; + String F2_STR = "[pf2]"; + String F3_STR = "[pf3]"; + String F4_STR = "[pf4]"; + String F5_STR = "[pf5]"; + String F6_STR = "[pf6]"; + String F7_STR = "[pf7]"; + String F8_STR = "[pf8]"; + String F9_STR = "[pf9]"; + String F10_STR = "[pf10]"; + String F11_STR = "[pf11]"; + String F12_STR = "[pf12]"; + String F13_STR = "[pf13]"; + String F14_STR = "[pf14]"; + String F15_STR = "[pf15]"; + String F16_STR = "[pf16]"; + String F17_STR = "[pf17]"; + String F18_STR = "[pf18]"; + String F19_STR = "[pf19]"; + String F20_STR = "[pf20]"; + String F21_STR = "[pf21]"; + String F22_STR = "[pf22]"; + String F23_STR = "[pf23]"; + String F24_STR = "[pf24]"; + + // Shift F1 - F24 + String SHIFT_F1_STR = "[Spf1]"; + String SHIFT_F2_STR = "[Spf2]"; + String SHIFT_F3_STR = "[Spf3]"; + String SHIFT_F4_STR = "[Spf4]"; + String SHIFT_F5_STR = "[Spf5]"; + String SHIFT_F6_STR = "[Spf6]"; + String SHIFT_F7_STR = "[Spf7]"; + String SHIFT_F8_STR = "[Spf8]"; + String SHIFT_F9_STR = "[Spf9]"; + String SHIFT_F10_STR = "[Spf10]"; + String SHIFT_F11_STR = "[Spf11]"; + String SHIFT_F12_STR = "[Spf12]"; + String SHIFT_F13_STR = "[Spf13]"; + String SHIFT_F14_STR = "[Spf14]"; + String SHIFT_F15_STR = "[Spf15]"; + String SHIFT_F16_STR = "[Spf16]"; + String SHIFT_F17_STR = "[Spf17]"; + String SHIFT_F18_STR = "[Spf18]"; + String SHIFT_F19_STR = "[Spf19]"; + String SHIFT_F20_STR = "[Spf20]"; + + // Field attributes + short ATTR_PROTECTED = 32; + short ATTR_ALPHANUM = 16; + short ATTR_DISPLAY1 = 8; + short ATTR_DISPLAY2 = 4; + short ATTR_MDT = 1; + + // Extended attributes + short EXT_ATTR_3270_UNDERLINE = 192; + short EXT_ATTR_3270_REVERSE = 128; + short EXT_ATTR_3270_BLINK = 64; + short EXT_ATTR_3270_EBCDIC = 0; + + // Timeout + int NO_TIMEOUT = -1; +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLErr.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLErr.java new file mode 100644 index 0000000..2b06a3d --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLErr.java @@ -0,0 +1,54 @@ +package com.ibm.eNetwork.ECL; + +/** + * IBM Host Access Class Library (HACL) ECLErr checked exception facade. + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr. + */ +public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr { + + private static final long serialVersionUID = 1L; + + public ECLErr() { + super(); + } + + public ECLErr(String text) { + super(text); + } + + public ECLErr(String location, String msgNum) { + super(location, msgNum); + } + + public ECLErr(String location, String msgNum, String insert) { + super(location, msgNum, insert); + } + + public ECLErr(String location, String msgNum, String insert1, String insert2) { + super(location, msgNum, insert1, insert2); + } + + public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) { + super(location, msgNum, insert1, insert2, log); + } + + public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) { + super(location, msgNum, insert1, insert2, insert3); + } + + public ECLErr(String location, String msgNum, int reasonCode) { + super(location, msgNum, reasonCode); + } + + public ECLErr(String location, String msgNum, String insert1, int reasonCode) { + super(location, msgNum, insert1, reasonCode); + } + + public ECLErr(Throwable cause) { + super(cause); + } + + public ECLErr(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java new file mode 100644 index 0000000..8b28284 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java @@ -0,0 +1,137 @@ +package com.ibm.eNetwork.ECL; + +/** + * Discrete 3270 field facade matching IBM Host On-Demand ECLField. + */ +public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants { + + private final haus.nightmare.lib3270j.ecl.ECLField delegate; + + public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) { + super(delegate != null ? delegate.getPS() : null, + delegate != null ? delegate.getStart() : 0, + delegate != null ? delegate.getDataStart() : 0, + delegate != null ? delegate.getEnd() : 0, + delegate != null ? delegate.getLength() : 0, + (byte) (delegate != null ? delegate.getAttribute() : 0)); + this.delegate = delegate; + } + + public int GetStart() { + return delegate != null ? delegate.GetStart() + 1 : 1; + } + + public int getStart() { return GetStart(); } + + public int GetStartRow() { + return delegate != null ? delegate.GetStartRow() + 1 : 1; + } + + public int getStartRow() { return GetStartRow(); } + + public int GetStartCol() { + return delegate != null ? delegate.GetStartCol() + 1 : 1; + } + + public int getStartCol() { return GetStartCol(); } + + public int GetEnd() { + return delegate != null ? delegate.GetEnd() + 1 : 1; + } + + public int getEnd() { return GetEnd(); } + + public int GetEndRow() { + return delegate != null ? delegate.GetEndRow() + 1 : 1; + } + + public int getEndRow() { return GetEndRow(); } + + public int GetEndCol() { + return delegate != null ? delegate.GetEndCol() + 1 : 1; + } + + public int getEndCol() { return GetEndCol(); } + + public int GetLength() { + return delegate != null ? delegate.GetLength() : 0; + } + + public int getLength() { return GetLength(); } + + public String GetString() { + return delegate != null ? delegate.getString() : ""; + } + + public String getString() { return GetString(); } + + public int GetString(char[] buffer, int len) throws ECLErr { + if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1"); + String text = GetString(); + int count = Math.min(len, Math.min(buffer.length, text.length())); + for (int i = 0; i < count; i++) { + buffer[i] = text.charAt(i); + } + return count; + } + + public void SetString(String text) throws ECLErr { + if (delegate != null) { + delegate.setText(text); + } + } + + public void SetText(String text) throws ECLErr { + SetString(text); + } + + public void setText(String text) { + if (delegate != null) { + delegate.setText(text); + } + } + + public boolean IsModified() { + return delegate != null && delegate.isModified(); + } + + public boolean isModified() { return IsModified(); } + + public boolean IsProtected() { + return delegate != null && delegate.isProtected(); + } + + public boolean isProtected() { return IsProtected(); } + + public boolean IsNumeric() { + return delegate != null && delegate.isNumeric(); + } + + public boolean isNumeric() { return IsNumeric(); } + + public boolean IsHighIntensity() { + return delegate != null && delegate.isHighIntensity(); + } + + public boolean isHighIntensity() { return IsHighIntensity(); } + + public boolean IsPenDetectable() { + return delegate != null && delegate.isPenDetectable(); + } + + public boolean isPenDetectable() { return IsPenDetectable(); } + + public boolean IsDisplay() { + return delegate != null && delegate.isDisplay(); + } + + public boolean isDisplay() { return IsDisplay(); } + + public short GetAttribute() { + return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0; + } + + public short getAttribute() { return GetAttribute(); } + + public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLFieldList.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLFieldList.java new file mode 100644 index 0000000..cea6e7c --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLFieldList.java @@ -0,0 +1,113 @@ +package com.ibm.eNetwork.ECL; + +import java.util.Vector; + +/** + * Collection of 3270 fields matching IBM Host On-Demand ECLFieldList. + */ +public class ECLFieldList extends Vector implements ECLConstants { + + private static final long serialVersionUID = 1L; + + private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate; + private final ECLPS ps; + + public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) { + this.delegate = delegate; + this.ps = ps; + refreshFields(); + } + + public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) { + this(delegate, null); + } + + private void refreshFields() { + clear(); + if (delegate != null) { + delegate.refresh(); + for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) { + add(new ECLField(f)); + } + } + } + + public void Refresh() { + refreshFields(); + } + + public void Refresh(int planes) throws ECLErr { + refreshFields(); + } + + public void refresh() { + refreshFields(); + } + + public int GetFieldCount() { + return size(); + } + + public int getFieldCount() { return GetFieldCount(); } + + public ECLField GetFirstField() { + return GetFirstField(0); + } + + public ECLField GetFirstField(int filter) { + for (ECLField f : this) { + if (matchesFilter(f, filter)) return f; + } + return null; + } + + public ECLField GetNextField(ECLField current) { + return GetNextField(current, 0); + } + + public ECLField GetNextField(ECLField current, int filter) { + if (current == null) return GetFirstField(filter); + int idx = indexOf(current); + if (idx >= 0) { + for (int i = idx + 1; i < size(); i++) { + ECLField f = get(i); + if (matchesFilter(f, filter)) return f; + } + } + return null; + } + + public ECLField FindField(int pos) throws ECLErr { + if (ps != null && (pos < 1 || pos > ps.GetSize())) { + throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos)); + } + for (ECLField f : this) { + int start = f.GetStart(); + int end = f.GetEnd(); + if (start <= end) { + if (pos >= start && pos <= end) return f; + } else { + // Wrapped field + if (pos >= start || pos <= end) return f; + } + } + return null; + } + + public ECLField FindField(int row, int col) throws ECLErr { + if (ps == null) return null; + int pos = ps.ConvertRowColToPos(row, col); + return FindField(pos); + } + + private boolean matchesFilter(ECLField f, int filter) { + if (filter == 0 || filter == ALL_FIELDS) return true; + if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false; + if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false; + if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false; + if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false; + if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false; + if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false; + return true; + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLHostPrintSession.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLHostPrintSession.java new file mode 100644 index 0000000..41262bb --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLHostPrintSession.java @@ -0,0 +1,39 @@ +package com.ibm.eNetwork.ECL; + +import java.util.Properties; + +/** + * Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession. + */ +public class ECLHostPrintSession extends ECLSession { + + public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = ""; + public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5"; + public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = ""; + public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23"; + public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = ""; + public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true"; + public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true"; + public static final String SESSION_LU_TYPE = "printerLUType"; + + public ECLHostPrintSession(Properties properties) throws ECLErr { + super(properties); + } + + public ECLHostPrintSession() throws ECLErr { + super(); + } + + public void cancelPrintJob() { + ECLPS ps = GetPS(); + if (ps != null) { + try { + ps.SendKeys("[cancelpjob]"); + } catch (Exception ignored) {} + } + } + + public void CancelPrintJob() { + cancelPrintJob(); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIA.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIA.java new file mode 100644 index 0000000..d87129f --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIA.java @@ -0,0 +1,130 @@ +package com.ibm.eNetwork.ECL; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Operator Information Area facade matching IBM Host On-Demand ECLOIA. + */ +public class ECLOIA implements ECLConstants { + + private final haus.nightmare.lib3270j.ecl.ECLOIA delegate; + private final List oiaNotifies = new CopyOnWriteArrayList<>(); + + public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) { + this.delegate = delegate; + if (delegate != null) { + delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() { + @Override + public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) { + ECLOIAEvent facadeEvt = new ECLOIAEvent(event); + for (ECLOIANotify n : oiaNotifies) { + try { + n.OIANotifyEvent(facadeEvt); + } catch (Exception ignored) {} + } + } + }); + } + } + + public void RegisterOIAEvent(ECLOIANotify notify) { + if (notify != null && !oiaNotifies.contains(notify)) { + oiaNotifies.add(notify); + } + } + + public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); } + + public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) { + if (delegate != null) { + delegate.RegisterOIAEvent(listener); + } + } + + public void UnregisterOIAEvent(ECLOIANotify notify) { + if (notify != null) { + oiaNotifies.remove(notify); + } + } + + public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); } + + public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) { + if (delegate != null) { + delegate.UnregisterOIAEvent(listener); + } + } + + public boolean IsAlphanumeric() { + return delegate != null && delegate.IsAlphanumeric(); + } + + public boolean isAlphanumeric() { return IsAlphanumeric(); } + + public boolean IsNumeric() { + return delegate != null && delegate.IsNumeric(); + } + + public boolean isNumeric() { return IsNumeric(); } + + public boolean IsDBCS() { + return delegate != null && delegate.IsDBCS(); + } + + public boolean isDBCS() { return IsDBCS(); } + + public boolean IsInsertMode() { + return delegate != null && delegate.IsInsertMode(); + } + + public boolean isInsertMode() { return IsInsertMode(); } + + public boolean IsCommError() { + return delegate != null && delegate.IsCommError(); + } + + public boolean isCommError() { return IsCommError(); } + + public boolean IsKeyLocked() { + return delegate != null && delegate.isKeyLocked(); + } + + public boolean isKeyLocked() { return IsKeyLocked(); } + + public boolean IsMessageWaiting() { + return delegate != null && delegate.IsMessageWaiting(); + } + + public boolean isMessageWaiting() { return IsMessageWaiting(); } + + public int GetInputInhibited() { + return delegate != null ? delegate.getInputInhibited() : 0; + } + + public int getInputInhibited() { return GetInputInhibited(); } + + public int GetAlphanumericType() { + return delegate != null ? delegate.getAlphanumericType() : 0; + } + + public int getAlphanumericType() { return GetAlphanumericType(); } + + public boolean WaitForInput(long timeout) { + return delegate != null && delegate.WaitForInput(timeout); + } + + public boolean WaitForSystemAvailable(long timeout) { + return delegate != null && delegate.WaitForSystemAvailable(timeout); + } + + public boolean WaitForAppAvailable(long timeout) { + return delegate != null && delegate.WaitForAppAvailable(timeout); + } + + public boolean WaitForTransition(long timeout) { + return delegate != null && delegate.WaitForTransition(timeout); + } + + public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIAEvent.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIAEvent.java new file mode 100644 index 0000000..8145f2f --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIAEvent.java @@ -0,0 +1,80 @@ +package com.ibm.eNetwork.ECL; + +import com.ibm.eNetwork.ECL.event.ECLEvent; + +/** + * Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD. + */ +public class ECLOIAEvent extends ECLEvent { + + public String data; + public long newState; + public long changedMask; + + private int inputInhibited = 0; + private int alphanumericType = 0; + private boolean insertMode = false; + + public ECLOIAEvent(Object source, long newState, long changedMask, String data) { + super(source); + this.newState = newState; + this.changedMask = changedMask; + this.data = data != null ? data : ""; + } + + public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType, + boolean insertMode, String statusString) { + super(source); + this.inputInhibited = inputInhibited; + this.alphanumericType = alphanumericType; + this.insertMode = insertMode; + this.data = statusString != null ? statusString : ""; + this.newState = inputInhibited; + this.changedMask = eventType; + } + + public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) { + super(delegate != null ? delegate.getSource() : null); + if (delegate != null) { + this.inputInhibited = delegate.getInputInhibited(); + this.alphanumericType = delegate.getAlphanumericType(); + this.insertMode = delegate.isInsertMode(); + this.data = delegate.getStatusString(); + this.newState = delegate.getInputInhibited(); + this.changedMask = delegate.getEventType(); + } + } + + public ECLOIA GetOIA() { + Object src = GetSource(); + if (src instanceof ECLOIA) { + return (ECLOIA) src; + } else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) { + return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src); + } + return null; + } + + public ECLOIA getOIA() { return GetOIA(); } + + public int GetInputInhibited() { return inputInhibited; } + public int getInputInhibited() { return inputInhibited; } + + public int GetAlphanumericType() { return alphanumericType; } + public int getAlphanumericType() { return alphanumericType; } + + public boolean IsInsertMode() { return insertMode; } + public boolean isInsertMode() { return insertMode; } + + public boolean IsInputInhibited() { return inputInhibited != 0; } + public boolean isInputInhibited() { return IsInputInhibited(); } + + public String GetStatusString() { return data; } + public String getStatusString() { return data; } + + @Override + public String toString() { + return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) + + ", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data; + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIANotify.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIANotify.java new file mode 100644 index 0000000..5e1fcb3 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLOIANotify.java @@ -0,0 +1,17 @@ +package com.ibm.eNetwork.ECL; + +/** + * Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify. + */ +public interface ECLOIANotify { + void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text); + default void NotifyStop(ECLOIA oia, int reason) {} + default void NotifyError(ECLOIA oia, ECLErr err) {} + default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); } + default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); } + default void OIANotifyEvent(ECLOIAEvent event) { + if (event != null) { + NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, ""); + } + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java new file mode 100644 index 0000000..a8135e3 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java @@ -0,0 +1,514 @@ +package com.ibm.eNetwork.ECL; + +import com.ibm.eNetwork.ECL.event.ECLPSListener; +import java.io.Serializable; + +/** + * Presentation Space facade matching IBM Host On-Demand ECLPS. + * Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics. + */ +public class ECLPS implements ECLConstants, Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + private final haus.nightmare.lib3270j.ecl.ECLPS delegate; + + public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) { + this.delegate = delegate; + } + + public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) { + this(session != null ? session.GetPS() : null); + } + + public ECLPS(ECLSession session) { + this(session != null ? session.getDelegate() : null); + } + + public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() { + return delegate; + } + + // ========== Dimensions & Position Conversion (1-based) ========== + + public int GetSize() { + return delegate != null ? delegate.GetSize() : 0; + } + + public int getSize() { return GetSize(); } + + public int GetRows() { + return delegate != null ? delegate.GetRows() : 0; + } + + public int getRows() { return GetRows(); } + + public int GetCols() { + return delegate != null ? delegate.GetCols() : 0; + } + + public int getCols() { return GetCols(); } + + public int GetSizeRows() { return GetRows(); } + public int GetSizeCols() { return GetCols(); } + + public int ConvertRowColToPos(int row, int col) throws ECLErr { + if (delegate != null) { + try { + return delegate.ConvertRowColToPos(row, col); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return 1; + } + + public int convertRowColToPos(int row, int col) throws ECLErr { + return ConvertRowColToPos(row, col); + } + + public int ConvertPosToRow(int pos) throws ECLErr { + if (delegate != null) { + try { + return delegate.ConvertPosToRow(pos); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return 1; + } + + public int convertPosToRow(int pos) throws ECLErr { + return ConvertPosToRow(pos); + } + + public int ConvertPosToCol(int pos) throws ECLErr { + if (delegate != null) { + try { + return delegate.ConvertPosToCol(pos); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return 1; + } + + public int convertPosToCol(int pos) throws ECLErr { + return ConvertPosToCol(pos); + } + + public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr { + if (delegate != null) { + try { + delegate.ConvertPosToRowCol(pos, rowCol); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + } + + public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr { + ConvertPosToRowCol(pos, rowCol); + } + + public int[] ConvertPosToRowCol(int pos) throws ECLErr { + if (delegate != null) { + try { + return delegate.ConvertPosToRowCol(pos); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return new int[]{1, 1}; + } + + public int[] convertPosToRowCol(int pos) throws ECLErr { + return ConvertPosToRowCol(pos); + } + + // ========== Cursor Operations (1-based) ========== + + public int GetCursorPos() { + return delegate != null ? delegate.GetCursorPos() : 1; + } + + public int getCursorPos() { return GetCursorPos(); } + + public int GetCursorRow() { + return delegate != null ? delegate.GetCursorRow() : 1; + } + + public int getCursorRow() { return GetCursorRow(); } + + public int GetCursorCol() { + return delegate != null ? delegate.GetCursorCol() : 1; + } + + public int getCursorCol() { return GetCursorCol(); } + + public void SetCursorPos(int pos) throws ECLErr { + if (delegate != null) { + try { + delegate.SetCursorPos(pos); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + } + + public void setCursorPos(int pos) throws ECLErr { + SetCursorPos(pos); + } + + public void SetCursorPos(int row, int col) throws ECLErr { + if (delegate != null) { + try { + delegate.SetCursorPos(row, col); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + } + + public void setCursorPos(int row, int col) throws ECLErr { + SetCursorPos(row, col); + } + + // ========== GetString & SetString Operations (1-based) ========== + + public int GetString(char[] buffer, int len, int pos) throws ECLErr { + return GetString(buffer, len, pos, len); + } + + public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr { + if (delegate != null) { + try { + return delegate.GetString(buffer, len, pos, length); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return 0; + } + + public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr { + return GetString(buffer, len, ConvertRowColToPos(row, col), length); + } + + public String GetString(int pos, int length) throws ECLErr { + if (delegate != null) { + try { + return delegate.GetString(pos, length); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return ""; + } + + public String getString(int pos, int length) throws ECLErr { + return GetString(pos, length); + } + + public String GetString(int row, int col, int length) throws ECLErr { + return GetString(ConvertRowColToPos(row, col), length); + } + + public String getString(int row, int col, int length) throws ECLErr { + return GetString(row, col, length); + } + + public void SetString(String text, int pos) throws ECLErr { + if (delegate != null) { + try { + delegate.SetString(text, pos); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + } + + public void setString(String text, int pos) throws ECLErr { + SetString(text, pos); + } + + public void SetString(String text, int row, int col) throws ECLErr { + SetString(text, ConvertRowColToPos(row, col)); + } + + public void setString(String text, int row, int col) throws ECLErr { + SetString(text, row, col); + } + + public void SetString(String text) throws ECLErr { + SetString(text, GetCursorPos()); + } + + public void setString(String text) throws ECLErr { + SetString(text); + } + + public void SetText(String text, int pos) throws ECLErr { + SetString(text, pos); + } + + public void SetText(String text, int row, int col) throws ECLErr { + SetString(text, row, col); + } + + public void setText(String text, int pos) { + if (delegate != null) delegate.setText(text, pos); + } + + public void setText(String text, int row, int col) { + if (delegate != null) delegate.setText(text, row, col); + } + + // ========== Search Operations (1-based) ========== + + public int SearchText(String text, int dir, int startPos) { + return delegate != null ? delegate.SearchText(text, dir, startPos) : 0; + } + + public int SearchText(String text, int dir) { + return delegate != null ? delegate.SearchText(text, dir) : 0; + } + + public int SearchText(String text) { + return delegate != null ? delegate.SearchText(text) : 0; + } + + public int SearchText(String text, int row, int col, int dir) throws ECLErr { + return SearchText(text, dir, ConvertRowColToPos(row, col)); + } + + public int SearchString(String text, int dir, int startPos) { + return SearchText(text, dir, startPos); + } + + public int SearchString(String text, int dir) { + return SearchText(text, dir); + } + + public int SearchString(String text) { + return SearchText(text); + } + + public int SearchString(String text, int row, int col, int dir) throws ECLErr { + return SearchText(text, row, col, dir); + } + + public int SearchPS(String text) { + return delegate != null ? delegate.SearchPS(text) : 0; + } + + public int SearchPS(String text, int startPos) { + return delegate != null ? delegate.SearchPS(text, startPos) : 0; + } + + public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) { + return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0; + } + + // ========== Keystrokes & Mnemonics ========== + + public void SendKeys(String keys) { + if (delegate != null) delegate.SendKeys(keys); + } + + public void sendKeys(String keys) { SendKeys(keys); } + + public void SendKeys(String keys, int pos) { + if (delegate != null) delegate.SendKeys(keys, pos); + } + + public void sendKeys(String keys, int pos) { SendKeys(keys, pos); } + + public void SendKeys(String keys, int row, int col) { + if (delegate != null) delegate.SendKeys(keys, row, col); + } + + public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); } + + public void SendCharacters(String keys) { + if (delegate != null) delegate.SendCharacters(keys); + } + + public void sendCharacters(String keys) { SendCharacters(keys); } + + public void SendCharacters(String keys, int delayMs) { + if (delegate != null) delegate.SendCharacters(keys, delayMs); + } + + public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); } + + public void SendCharacters(String keys, int pos, int delayMs) { + if (delegate != null) delegate.SendCharacters(keys, pos, delayMs); + } + + public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); } + + public void SendCharacters(String keys, int row, int col, int delayMs) { + if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs); + } + + public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); } + + public String[] GetSendKeyMnemonics() { + return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0]; + } + + public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); } + + // ========== Field Operations ========== + + public ECLFieldList GetFieldList() { + haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null; + return new ECLFieldList(fl, this); + } + + public ECLFieldList getFieldList() { return GetFieldList(); } + + public ECLField GetField(int pos) throws ECLErr { + return GetFieldList().FindField(pos); + } + + public ECLField getField(int pos) throws ECLErr { return GetField(pos); } + + public ECLField GetField(int row, int col) throws ECLErr { + return GetFieldList().FindField(row, col); + } + + public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); } + + public ECLField GetFirstField() { + return GetFieldList().GetFirstField(); + } + + public ECLField getFirstField() { return GetFirstField(); } + + public ECLField GetNextField(ECLField prev) { + return GetFieldList().GetNextField(prev); + } + + public ECLField getNextField(ECLField prev) { return GetNextField(prev); } + + // ========== Listener Overloads for HoD Interfaces ========== + + public void RegisterPSEvent(ECLPSNotify notify) { + if (delegate != null) delegate.RegisterPSEvent(notify); + } + + public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); } + + public void RegisterPSEvent(ECLPSNotify notify, int eventType) { + if (delegate != null) delegate.RegisterPSEvent(notify, eventType); + } + + public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); } + + public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { + if (delegate != null) delegate.RegisterPSEvent(notify, desc); + } + + public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); } + + public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { + if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType); + } + + public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); } + + public void UnregisterPSEvent(ECLPSNotify notify) { + if (delegate != null) delegate.UnregisterPSEvent(notify); + } + + public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); } + + public void RegisterPSEvent(ECLPSListener listener) { + if (delegate != null) delegate.RegisterPSEvent(listener); + } + + public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); } + + public void UnregisterPSEvent(ECLPSListener listener) { + if (delegate != null) delegate.UnregisterPSEvent(listener); + } + + public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); } + + // ========== Synchronization & Waits ========== + + public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) { + return delegate != null && delegate.WaitForScreen(desc, timeoutMs); + } + + public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); } + + public boolean WaitForScreen(ECLScreenDesc desc) { + return delegate != null && delegate.WaitForScreen(desc); + } + + public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); } + + public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) { + return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs); + } + + public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); } + + public boolean WaitWhileScreen(ECLScreenDesc desc) { + return delegate != null && delegate.WaitWhileScreen(desc); + } + + public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); } + + public boolean WaitForString(String text, long timeoutMs) { + return delegate != null && delegate.WaitForString(text, timeoutMs); + } + + public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); } + + public boolean WaitForString(String text) { + return delegate != null && delegate.WaitForString(text); + } + + public boolean waitForString(String text) { return WaitForString(text); } + + public boolean WaitForString(String text, int row, int col, long timeoutMs) { + return delegate != null && delegate.WaitForString(text, row, col, timeoutMs); + } + + public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); } + + public boolean WaitForCursor(int row, int col, long timeoutMs) { + return delegate != null && delegate.WaitForCursor(row, col, timeoutMs); + } + + public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); } + + // ========== Screen Update Inquiries ========== + + public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr { + return GetScreen(startRow, startCol, length, false); + } + + public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr { + int pos = ConvertRowColToPos(startRow, startCol); + char[] buf = new char[length]; + GetString(buf, length, pos, length); + if (fillNulls) { + for (int i = 0; i < buf.length; i++) { + if (buf[i] == '\0') buf[i] = ' '; + } + } + int endPos = pos + length - 1; + int endRow = ConvertPosToRow(endPos); + int endCol = ConvertPosToCol(endPos); + return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf)); + } + + public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) { + return delegate.GetScreen(buffer, len, row, col, length, plane); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSNotify.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSNotify.java new file mode 100644 index 0000000..9a85bb0 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSNotify.java @@ -0,0 +1,10 @@ +package com.ibm.eNetwork.ECL; + +/** + * Presentation space notification interface matching IBM Host On-Demand ECLPSNotify. + */ +public interface ECLPSNotify { + void NotifyEvent(ECLPS ps); + default void NotifyStop(ECLPS ps, int reason) {} + default void NotifyError(ECLPS ps, ECLErr err) {} +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSUpdate.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSUpdate.java new file mode 100644 index 0000000..4f6e8a0 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPSUpdate.java @@ -0,0 +1,90 @@ +package com.ibm.eNetwork.ECL; + +import java.io.Serializable; + +/** + * Conforms to IBM Host On-Demand ECLPSUpdate. + */ +public class ECLPSUpdate implements Serializable { + + private static final long serialVersionUID = 1L; + + private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate; + private final int startRow; + private final int startCol; + private final int endRow; + private final int endCol; + private final int start; + private final int end; + private final boolean fullUpdate; + private final String text; + private final transient ECLPS ps; + + public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, + int start, int end, boolean fullUpdate, String text) { + this.ps = ps; + this.startRow = startRow; + this.startCol = startCol; + this.endRow = endRow; + this.endCol = endCol; + this.start = start; + this.end = end; + this.fullUpdate = fullUpdate; + this.text = text != null ? text : ""; + this.delegate = null; + } + + public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) { + this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text); + } + + public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) { + this(null, startRow, startCol, endRow, endCol, 0, 0, false, text); + } + + public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) { + this.delegate = delegate; + this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null; + this.startRow = delegate != null ? delegate.getStartRow() : 0; + this.startCol = delegate != null ? delegate.getStartCol() : 0; + this.endRow = delegate != null ? delegate.getEndRow() : 0; + this.endCol = delegate != null ? delegate.getEndCol() : 0; + this.start = delegate != null ? delegate.getStart() : 0; + this.end = delegate != null ? delegate.getEnd() : 0; + this.fullUpdate = delegate != null && delegate.isFullUpdate(); + this.text = delegate != null ? delegate.getText() : ""; + } + + public ECLPS getPS() { return ps; } + public ECLPS GetPS() { return ps; } + + public int getStartRow() { return startRow; } + public int GetStartRow() { return startRow + 1; } + + public int getStartCol() { return startCol; } + public int GetStartCol() { return startCol + 1; } + + public int getEndRow() { return endRow; } + public int GetEndRow() { return endRow + 1; } + + public int getEndCol() { return endCol; } + public int GetEndCol() { return endCol + 1; } + + public int getStart() { return start; } + public int GetStart() { return start + 1; } + + public int getEnd() { return end; } + public int GetEnd() { return end + 1; } + + public boolean isFullUpdate() { return fullUpdate; } + public boolean IsFullUpdate() { return fullUpdate; } + + public String getText() { return text; } + public String GetText() { return text; } + + @Override + public String toString() { + return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]", + startRow, startCol, endRow, endCol, fullUpdate); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenDesc.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenDesc.java new file mode 100644 index 0000000..5d3e435 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenDesc.java @@ -0,0 +1,21 @@ +package com.ibm.eNetwork.ECL; + +import java.io.Serializable; + +/** + * Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc. + */ +public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable { + + private static final long serialVersionUID = 1L; + + public ECLScreenDesc() { + super(); + } + + public boolean Matches(ECLPS ps, ECLOIA oia) { + haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null; + haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null; + return super.Matches(p, o); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenNotify.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenNotify.java new file mode 100644 index 0000000..3c2d8a9 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenNotify.java @@ -0,0 +1,8 @@ +package com.ibm.eNetwork.ECL; + +/** + * Listener interface for ECLScreenReco matching notifications matching IBM HoD. + */ +public interface ECLScreenNotify { + void screenMatching(ECLScreenRecoEvent event); +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenReco.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenReco.java new file mode 100644 index 0000000..a81910d --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenReco.java @@ -0,0 +1,78 @@ +package com.ibm.eNetwork.ECL; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Screen recognition facade matching IBM Host On-Demand ECLScreenReco. + */ +public class ECLScreenReco { + + private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate; + private final ECLPS ps; + private final Map notifyMap = new ConcurrentHashMap<>(); + + public ECLScreenReco() { + this((ECLPS) null); + } + + public ECLScreenReco(ECLSession session) { + this(session != null ? session.GetPS() : null); + } + + public ECLScreenReco(ECLPS ps) { + this(ps, (ECLOIA) null); + } + + public ECLScreenReco(ECLPS ps, ECLOIA oia) { + this.ps = ps; + this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null, + oia != null ? oia.getDelegate() : null); + } + + public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) { + this.delegate = delegate; + this.ps = null; + } + + public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) { + if (desc != null && notify != null) { + notifyMap.put(desc, notify); + if (delegate != null) { + delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps))); + } + } + } + + public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) { + RegisterScreen(desc, notify); + } + + public void UnregisterScreen(ECLScreenDesc desc) { + if (desc != null) { + notifyMap.remove(desc); + if (delegate != null) { + delegate.UnregisterScreen(desc); + } + } + } + + public void unregisterScreen(ECLScreenDesc desc) { + UnregisterScreen(desc); + } + + public boolean IsMatch(ECLScreenDesc desc) { + if (delegate != null) { + return delegate.IsMatch(desc); + } + return false; + } + + public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) { + if (ps == null) return false; + if (desc == null) return true; + return desc.Matches(ps, (ECLOIA) null); + } + + public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenRecoEvent.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenRecoEvent.java new file mode 100644 index 0000000..3c6e0d9 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLScreenRecoEvent.java @@ -0,0 +1,39 @@ +package com.ibm.eNetwork.ECL; + +import com.ibm.eNetwork.ECL.event.ECLEvent; + +/** + * Event delivered when an ECLScreenDesc matches the Presentation Space. + */ +public class ECLScreenRecoEvent extends ECLEvent { + + private final ECLScreenReco source; + private final ECLScreenDesc screenDesc; + private final ECLPS ps; + + public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) { + super(source); + this.source = source; + this.screenDesc = screenDesc; + this.ps = ps; + } + + public ECLScreenRecoEvent(Object source, ECLPS ps) { + super(source); + this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null; + this.screenDesc = null; + this.ps = ps; + } + + @Override + public Object GetSource() { return source != null ? source : super.GetSource(); } + + public ECLScreenReco GetScreenReco() { return source; } + public ECLScreenReco getScreenReco() { return source; } + + public ECLScreenDesc GetScreenDesc() { return screenDesc; } + public ECLScreenDesc getScreenDesc() { return screenDesc; } + + public ECLPS GetPS() { return ps; } + public ECLPS getPS() { return ps; } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLSession.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLSession.java new file mode 100644 index 0000000..78f87c6 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLSession.java @@ -0,0 +1,189 @@ +package com.ibm.eNetwork.ECL; + +import java.io.IOException; +import java.util.Properties; + +/** + * Top-level session facade matching IBM Host On-Demand ECLSession. + * Extends ECLConnection and provides access to Presentation Space (ECLPS), + * Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco). + */ +public class ECLSession extends ECLConnection implements ECLConstants { + + public static final String SESSION_NAME = "SESSION_NAME"; + public static final String SESSION_LABEL = "SESSION_LABEL"; + public static final String SESSION_TYPE = "SESSION_TYPE"; + public static final String SESSION_HOST = "SESSION_HOST"; + public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT"; + public static final String SESSION_PORT = "SESSION_PORT"; + public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE"; + public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE"; + public static final String SESSION_SSL = "SESSION_SSL"; + public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL"; + public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion"; + public static final String SESSION_LU_NAME = "SESSION_LU_NAME"; + public static final String SESSION_TN3270E = "SESSION_TN3270E"; + public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT"; + public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT"; + public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES"; + public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE"; + public static final String KEY_KEEPALIVE_TYPE = "keepAliveType"; + public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout"; + public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock"; + public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel"; + public static final String PASTE_TAB_OPTIONS = "pasteTabOptions"; + public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine"; + public static final String BLOCK_PASTE = "blockPaste"; + public static final String PASTE_FIELD_WRAP = "pasteFieldWrap"; + public static final String PASTE_LINE_WRAP = "pasteLineWrap"; + + private final haus.nightmare.lib3270j.ecl.ECLSession delegate; + private final ECLPS ps; + private final ECLOIA oia; + private final ECLXfer xfer; + private final ECLScreenReco screenReco; + + public ECLSession(Properties properties) throws ECLErr { + super(properties); + this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties); + this.ps = new ECLPS(delegate.getPS()); + this.oia = new ECLOIA(delegate.getOIA()); + this.xfer = new ECLXfer(delegate.getXfer()); + this.screenReco = new ECLScreenReco(this.ps, this.oia); + } + + public ECLSession() throws ECLErr { + this(new Properties()); + } + + public ECLSession(String host, int port) throws ECLErr { + super(host, port); + Properties props = new Properties(); + if (host != null) props.setProperty(SESSION_HOST, host); + props.setProperty(SESSION_PORT, String.valueOf(port)); + this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props); + this.ps = new ECLPS(delegate.getPS()); + this.oia = new ECLOIA(delegate.getOIA()); + this.xfer = new ECLXfer(delegate.getXfer()); + this.screenReco = new ECLScreenReco(this.ps, this.oia); + } + + public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) { + super(delegate); + this.delegate = delegate; + this.ps = new ECLPS(delegate != null ? delegate.getPS() : null); + this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null); + this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null); + this.screenReco = new ECLScreenReco(this.ps, this.oia); + } + + public ECLPS GetPS() { + return ps; + } + + public ECLPS getPS() { + return ps; + } + + public ECLOIA GetOIA() { + return oia; + } + + public ECLOIA getOIA() { + return oia; + } + + public ECLXfer GetXfer() { + return xfer; + } + + public ECLXfer getXfer() { + return xfer; + } + + public ECLScreenReco GetScreenReco() { + return screenReco; + } + + public ECLScreenReco getScreenReco() { + return screenReco; + } + + public ECLConnection GetConnection() { + return this; + } + + public ECLConnection getConnection() { + return this; + } + + public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() { + return delegate; + } + + @Override + public void StartCommunication() throws IOException { + if (delegate != null) { + delegate.StartCommunication(); + } else { + super.StartCommunication(); + } + } + + @Override + public void StopCommunication() { + if (delegate != null) { + delegate.StopCommunication(); + } else { + super.StopCommunication(); + } + } + + public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr { + return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null); + } + + public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr { + return StartCommunicationWithBlocking(timeout); + } + + public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr { + if (delegate != null) { + try { + return delegate.StartCommunicationWithBlocking(timeout, desc); + } catch (IOException e) { + throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage()); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return false; + } + + public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr { + return StartCommunicationWithBlocking(timeout, desc); + } + + public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr { + if (delegate != null) { + try { + return delegate.RestartCommunicationWithBlocking(timeout); + } catch (IOException e) { + throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage()); + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + throw new ECLErr(e.getMessage()); + } + } + return false; + } + + public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr { + return RestartCommunicationWithBlocking(timeout); + } + + public void dispose() { + if (delegate != null) { + delegate.dispose(); + } + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLXfer.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLXfer.java new file mode 100644 index 0000000..1594baa --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLXfer.java @@ -0,0 +1,54 @@ +package com.ibm.eNetwork.ECL; + +import com.ibm.eNetwork.ECL.event.ECLXferListener; + +/** + * File transfer facade matching IBM Host On-Demand ECLXfer. + */ +public class ECLXfer { + + private final haus.nightmare.lib3270j.ecl.ECLXfer delegate; + + public ECLXfer(ECLSession session, Boolean bidi) { + this.delegate = (session != null && session.getDelegate() != null) + ? session.getDelegate().GetXfer() : null; + } + + public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) { + this.delegate = delegate; + } + + public void SendFile(String localFile, String hostFile, String options) throws ECLErr { + if (delegate != null) { + int rc = delegate.SendFile(localFile, hostFile, options); + if (rc != 0) { + throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc)); + } + } + } + + public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr { + if (delegate != null) { + int rc = delegate.ReceiveFile(localFile, hostFile, options); + if (rc != 0) { + throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc)); + } + } + } + + public void Cancel() { + if (delegate != null) { + delegate.Cancel(); + } + } + + public void RegisterXferEvent(ECLXferListener listener) { + // Can bridge listener if needed + } + + public void UnregisterXferEvent(ECLXferListener listener) { + // Can bridge listener if needed + } + + public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLEvent.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLEvent.java new file mode 100644 index 0000000..6172a83 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLEvent.java @@ -0,0 +1,25 @@ +package com.ibm.eNetwork.ECL.event; + +/** + * Base event class matching IBM Host On-Demand ECLEvent. + */ +public class ECLEvent { + private final Object originator; + + public ECLEvent(Object object) { + this.originator = object; + } + + public Object GetSource() { + return this.originator; + } + + public Object getSource() { + return this.originator; + } + + @Override + public String toString() { + return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null"); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSEvent.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSEvent.java new file mode 100644 index 0000000..833691e --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSEvent.java @@ -0,0 +1,126 @@ +package com.ibm.eNetwork.ECL.event; + +import com.ibm.eNetwork.ECL.ECLPS; +import com.ibm.eNetwork.ECL.ECLPSUpdate; + +/** + * Presentation Space event class matching IBM Host On-Demand ECLPSEvent. + */ +public class ECLPSEvent extends ECLEvent { + + public static final int USER_EVENTS = 1; + public static final int HOST_EVENTS = 2; + public static final int ALL_EVENTS = 3; + + private int type = 0; + private int start = 1; + private int end = 1; + private int startRow = 1; + private int startCol = 1; + private int endRow = 1; + private int endCol = 1; + private int oldCursorPos = 1; + private int newCursorPos = 1; + private boolean fullUpdate = true; + private boolean cursorVisible = true; + private int ringCounter = 0; + private ECLPSUpdate psUpdate = null; + private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null; + + public ECLPSEvent(Object source) { + super(source); + if (source instanceof ECLPS) { + ECLPS ps = (ECLPS) source; + this.end = ps.GetSize(); + this.endRow = ps.GetRows(); + this.endCol = ps.GetCols(); + this.newCursorPos = ps.GetCursorPos(); + } else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) { + haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source; + this.end = ps.GetSize(); + this.endRow = ps.GetRows(); + this.endCol = ps.GetCols(); + this.newCursorPos = ps.GetCursorPos(); + } + } + + public ECLPSEvent(Object source, int type) { + this(source); + this.type = type; + } + + public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) { + super(delegate != null ? delegate.getSource() : null); + this.delegate = delegate; + if (delegate != null) { + this.type = delegate.getType(); + this.start = delegate.getStart(); + this.end = delegate.getEnd(); + this.startRow = delegate.getStartRow(); + this.startCol = delegate.getStartCol(); + this.endRow = delegate.getEndRow(); + this.endCol = delegate.getEndCol(); + this.oldCursorPos = delegate.getOldCursorAddress() + 1; + this.newCursorPos = delegate.getNewCursorAddress() + 1; + this.fullUpdate = delegate.isFullUpdate(); + this.cursorVisible = delegate.isCursorVisible(); + this.ringCounter = delegate.getRingCounter(); + if (delegate.getPSUpdate() != null) { + this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate()); + } + } + } + + public int GetType() { return type; } + public int getType() { return type; } + + public ECLPS GetPS() { + Object src = GetSource(); + if (src instanceof ECLPS) { + return (ECLPS) src; + } else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) { + return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src); + } + return null; + } + + public void SetStart(int start) { this.start = start; } + public int GetStart() { return start; } + public int getStart() { return start; } + + public int GetStartRow() { return startRow; } + public int getStartRow() { return startRow; } + + public int GetStartCol() { return startCol; } + public int getStartCol() { return startCol; } + + public void SetEnd(int end) { this.end = end; } + public int GetEnd() { return end; } + public int getEnd() { return end; } + + public int GetEndRow() { return endRow; } + public int getEndRow() { return endRow; } + + public int GetEndCol() { return endCol; } + public int getEndCol() { return endCol; } + + public int GetOldCursorPos() { return oldCursorPos; } + public int getOldCursorPos() { return oldCursorPos; } + + public int GetNewCursorPos() { return newCursorPos; } + public int getNewCursorPos() { return newCursorPos; } + + public boolean IsFullUpdate() { return fullUpdate; } + public boolean isFullUpdate() { return fullUpdate; } + + public boolean IsCursorVisible() { return cursorVisible; } + public boolean isCursorVisible() { return cursorVisible; } + + public int GetRingCounter() { return ringCounter; } + public int getRingCounter() { return ringCounter; } + + public ECLPSUpdate GetPSUpdate() { return psUpdate; } + public ECLPSUpdate getPSUpdate() { return psUpdate; } + + public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSListener.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSListener.java new file mode 100644 index 0000000..6bead58 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLPSListener.java @@ -0,0 +1,13 @@ +package com.ibm.eNetwork.ECL.event; + +import com.ibm.eNetwork.ECL.ECLErr; +import com.ibm.eNetwork.ECL.ECLPS; + +/** + * Presentation Space listener interface matching IBM Host On-Demand ECLPSListener. + */ +public interface ECLPSListener { + void PSNotifyEvent(ECLPSEvent event); + default void PSNotifyStop(ECLPS ps, int reason) {} + default void PSNotifyError(ECLPS ps, ECLErr err) {} +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferEvent.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferEvent.java new file mode 100644 index 0000000..c9dcb20 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferEvent.java @@ -0,0 +1,48 @@ +package com.ibm.eNetwork.ECL.event; + +import com.ibm.eNetwork.ECL.ECLXfer; + +/** + * File transfer event matching IBM Host On-Demand ECLXferEvent. + */ +public class ECLXferEvent extends ECLEvent { + + private final String sourceFile; + private final String targetFile; + private final boolean receive; + private final long unitsDone; + private final long unitsToDo; + + public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) { + this(source, unitsDone, -1L, sourceFile, targetFile, receive); + } + + public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) { + super(source); + this.unitsDone = unitsDone; + this.unitsToDo = unitsToDo; + this.sourceFile = sourceFile; + this.targetFile = targetFile; + this.receive = receive; + } + + public ECLXfer GetECLXfer() { + Object src = GetSource(); + return (src instanceof ECLXfer) ? (ECLXfer) src : null; + } + + public String GetSourceFile() { return sourceFile; } + public String getSourceFile() { return sourceFile; } + + public String GetTargetFile() { return targetFile; } + public String getTargetFile() { return targetFile; } + + public long GetBytesSent() { return unitsDone; } + public long getBytesSent() { return unitsDone; } + + public long GetBytesToSend() { return unitsToDo; } + public long getBytesToSend() { return unitsToDo; } + + public boolean IsReceive() { return receive; } + public boolean isReceive() { return receive; } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferListener.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferListener.java new file mode 100644 index 0000000..23291c5 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/event/ECLXferListener.java @@ -0,0 +1,13 @@ +package com.ibm.eNetwork.ECL.event; + +import com.ibm.eNetwork.ECL.ECLErr; +import com.ibm.eNetwork.ECL.ECLXfer; + +/** + * File transfer listener interface matching IBM Host On-Demand ECLXferListener. + */ +public interface ECLXferListener { + void XferNotifyEvent(ECLXferEvent event); + default void XferNotifyStop(ECLXfer xfer, int reason) {} + default void XferNotifyError(ECLXfer xfer, ECLErr err) {} +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDAttrib.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDAttrib.java new file mode 100644 index 0000000..322b38a --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDAttrib.java @@ -0,0 +1,20 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDAttrib drop-in facade. + */ +public class ECLSDAttrib extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib { + private static final long serialVersionUID = 1L; + + public ECLSDAttrib() { + super(); + } + + public ECLSDAttrib(char attrib, int row, int col, int plane) { + super(attrib, row, col, plane); + } + + public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) { + super(attrib, row, col, plane, optional); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlock.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlock.java new file mode 100644 index 0000000..2334d82 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlock.java @@ -0,0 +1,24 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDBlock drop-in facade. + */ +public class ECLSDBlock extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock { + private static final long serialVersionUID = 1L; + + public ECLSDBlock() { + super(); + } + + public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) { + super(srow, scol, caseSense, optional); + } + + public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) { + super(srow, scol, erow, ecol, caseSense, optional); + } + + public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) { + super(textLines, srow, scol, caseSense); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlockString.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlockString.java new file mode 100644 index 0000000..97f88b8 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDBlockString.java @@ -0,0 +1,16 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDBlockString drop-in facade. + */ +public class ECLSDBlockString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlockString { + private static final long serialVersionUID = 1L; + + public ECLSDBlockString() { + super(); + } + + public ECLSDBlockString(String text) { + super(text); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDChecksum.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDChecksum.java new file mode 100644 index 0000000..afeeb81 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDChecksum.java @@ -0,0 +1,20 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDChecksum drop-in facade. + */ +public class ECLSDChecksum extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum { + private static final long serialVersionUID = 1L; + + public ECLSDChecksum() { + super(); + } + + public ECLSDChecksum(int value) { + super(value); + } + + public ECLSDChecksum(int value, boolean optional) { + super(value, optional); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDCursor.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDCursor.java new file mode 100644 index 0000000..4b97ce6 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDCursor.java @@ -0,0 +1,20 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDCursor drop-in facade. + */ +public class ECLSDCursor extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor { + private static final long serialVersionUID = 1L; + + public ECLSDCursor() { + super(); + } + + public ECLSDCursor(int row, int col) { + super(row, col); + } + + public ECLSDCursor(int row, int col, boolean optional) { + super(row, col, optional); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDFields.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDFields.java new file mode 100644 index 0000000..2797479 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDFields.java @@ -0,0 +1,20 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDFields drop-in facade. + */ +public class ECLSDFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields { + private static final long serialVersionUID = 1L; + + public ECLSDFields() { + super(); + } + + public ECLSDFields(int num) { + super(num); + } + + public ECLSDFields(int num, boolean optional) { + super(num, optional); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDInputFields.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDInputFields.java new file mode 100644 index 0000000..e333b45 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDInputFields.java @@ -0,0 +1,20 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDInputFields drop-in facade. + */ +public class ECLSDInputFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields { + private static final long serialVersionUID = 1L; + + public ECLSDInputFields() { + super(); + } + + public ECLSDInputFields(int num) { + super(num); + } + + public ECLSDInputFields(int num, boolean optional) { + super(num, optional); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDOIA.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDOIA.java new file mode 100644 index 0000000..2dcd9cb --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDOIA.java @@ -0,0 +1,24 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDOIA drop-in facade. + */ +public class ECLSDOIA extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDOIA { + private static final long serialVersionUID = 1L; + + public ECLSDOIA() { + super(); + } + + public ECLSDOIA(int type) { + super(type); + } + + public ECLSDOIA(int type, boolean optional) { + super(type, optional); + } + + public ECLSDOIA(String status, boolean optional) { + super(status, optional); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDString.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDString.java new file mode 100644 index 0000000..b91caa4 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLSDString.java @@ -0,0 +1,36 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLSDString drop-in facade. + */ +public class ECLSDString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDString { + private static final long serialVersionUID = 1L; + + public ECLSDString() { + super(); + } + + public ECLSDString(String text) { + super(text); + } + + public ECLSDString(String text, int row, int col) { + super(text, row, col); + } + + public ECLSDString(String text, int row, int col, boolean caseSense) { + super(text, row, col, caseSense); + } + + public ECLSDString(String text, int row, int col, boolean caseSense, boolean optional) { + super(text, row, col, caseSense, optional); + } + + public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) { + super(text, srow, scol, erow, ecol, caseSense, optional); + } + + public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional, boolean wrap) { + super(text, srow, scol, erow, ecol, caseSense, optional, wrap); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLScreenDescriptor.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLScreenDescriptor.java new file mode 100644 index 0000000..800ea76 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/screenreco/ECLScreenDescriptor.java @@ -0,0 +1,16 @@ +package com.ibm.eNetwork.ECL.screenreco; + +/** + * IBM Host On-Demand ECLScreenDescriptor drop-in facade. + */ +public abstract class ECLScreenDescriptor extends haus.nightmare.lib3270j.ecl.screenreco.ECLScreenDescriptor { + private static final long serialVersionUID = 1L; + + protected ECLScreenDescriptor() { + super(); + } + + protected ECLScreenDescriptor(boolean optional) { + super(optional); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java index 3e2f3ce..7460859 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java @@ -77,6 +77,34 @@ public class ECLConnection { if (props != null) { this.properties.putAll(props); convertData(this.properties); + + String h = this.properties.getProperty(ECLSession.SESSION_HOST); + if (h == null) h = this.properties.getProperty("host"); + if (h == null) h = this.properties.getProperty("Host"); + if (h == null) h = this.properties.getProperty("SESSION_PRINT_SESSION_HOST"); + if (h != null && !h.trim().isEmpty()) this.host = h.trim(); + + String p = this.properties.getProperty(ECLSession.SESSION_PORT); + if (p == null) p = this.properties.getProperty("port"); + if (p == null) p = this.properties.getProperty("Port"); + if (p == null) p = this.properties.getProperty("SESSION_HOST_PORT"); + if (p == null) p = this.properties.getProperty("SESSION_PRINT_SESSION_HOST_PORT"); + if (p != null && !p.trim().isEmpty()) { + try { this.port = Integer.parseInt(p.trim()); } catch (NumberFormatException ignored) {} + } + + String cp = this.properties.getProperty(ECLSession.SESSION_CODE_PAGE); + if (cp == null) cp = this.properties.getProperty("code_page"); + if (cp == null) cp = this.properties.getProperty("codepage"); + if (cp == null) cp = this.properties.getProperty("CodePage"); + if (cp != null && !cp.trim().isEmpty()) this.codePage = cp.trim(); + + String lu = this.properties.getProperty(ECLSession.SESSION_LU_NAME); + if (lu == null) lu = this.properties.getProperty("lu_name"); + if (lu == null) lu = this.properties.getProperty("luname"); + if (lu == null) lu = this.properties.getProperty("SESSION_PRINT_SESSION_LU_NAME"); + if (lu != null && !lu.trim().isEmpty()) this.luName = lu.trim(); + String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK); if (asu != null) { this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErr.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErr.java index aa11f04..2379cd3 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErr.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErr.java @@ -11,6 +11,8 @@ public class ECLErr extends Exception { private String id = ""; private String text = ""; + private int reasonCode = 0; + public ECLErr() { super(); } @@ -20,6 +22,21 @@ public class ECLErr extends Exception { this.text = text; } + public ECLErr(String location, String msgNum, int reasonCode) { + super(formatMsg(location, msgNum, "Reason " + reasonCode)); + this.tag = location; + this.id = msgNum; + this.text = "Reason " + reasonCode; + this.reasonCode = reasonCode; + } + + public ECLErr(String location, String msgNum) { + super(formatMsg(location, msgNum, "")); + this.tag = location; + this.id = msgNum; + this.text = ""; + } + public ECLErr(String tag, String id, String text) { super(formatMsg(tag, id, text)); this.tag = tag; @@ -27,11 +44,30 @@ public class ECLErr extends Exception { this.text = text; } - public ECLErr(String tag, String id, String text, String extra) { - super(formatMsg(tag, id, text + " " + extra)); - this.tag = tag; - this.id = id; - this.text = text + " " + extra; + public ECLErr(String location, String msgNum, String insert1, int reasonCode) { + super(formatMsg(location, msgNum, insert1 + " (Reason " + reasonCode + ")")); + this.tag = location; + this.id = msgNum; + this.text = insert1 + " (Reason " + reasonCode + ")"; + this.reasonCode = reasonCode; + } + + public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) { + super(formatMsg(location, msgNum, insert1 + " " + insert2)); + this.tag = location; + this.id = msgNum; + this.text = insert1 + " " + insert2; + } + + public ECLErr(String location, String msgNum, String insert1, String insert2) { + this(location, msgNum, insert1, insert2, false); + } + + public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) { + super(formatMsg(location, msgNum, insert1 + " " + insert2 + " " + insert3)); + this.tag = location; + this.id = msgNum; + this.text = insert1 + " " + insert2 + " " + insert3; } public ECLErr(Throwable cause) { @@ -57,14 +93,33 @@ public class ECLErr extends Exception { public String getID() { return id; } public String GetID() { return id; } + public String getLocation() { return tag; } + public String GetLocation() { return tag; } + + public int getReasonCode() { return reasonCode; } + public int GetReasonCode() { return reasonCode; } + + public int getMsgNumber() { + if (id != null && id.length() >= 7 && id.startsWith("ECL")) { + try { + return Integer.parseInt(id.substring(3, 7)); + } catch (NumberFormatException ignored) {} + } + return 0; + } + public int GetMsgNumber() { return getMsgNumber(); } + public String getText() { return text; } public String GetText() { return text; } + public String getMsgText() { return getMessage(); } + public String GetMsgText() { return getMessage(); } + public String getErrorText() { return getMessage(); } public String GetErrorText() { return getMessage(); } @Override public String toString() { - return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + "]"; + return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + ", reasonCode=" + reasonCode + "]"; } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java index 5b1a717..b95bcb0 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java @@ -26,6 +26,10 @@ public class ECLField { this.attribute = attribute; } + public ECLPS getPS() { return ps; } + public ECLPS GetPS() { return ps; } + public byte getAttributeByte() { return attribute; } + /** Buffer address of the field attribute character. */ public int getStart() { return startPos; } public int GetStart() { return getStart(); } @@ -154,6 +158,8 @@ public class ECLField { return ps.getString(dataStart, length); } public String GetText() { return getText(); } + public String getString() { return getText(); } + public String GetString() { return getText(); } /** * Set the text contents of this field. @@ -162,7 +168,13 @@ public class ECLField { if (isProtected() || length <= 0) return; ps.setText(text, dataStart); } - public void SetText(String text) { setText(text); } + public void SetText(String text) throws ECLErr { + if (isProtected()) { + throw new ECLErr("ECLField:SetText", "ECL0015", "Field is protected"); + } + setText(text); + } + public void SetString(String text) throws ECLErr { SetText(text); } /** * Get selector light pen type. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java index 59a0874..cb1b24f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java @@ -7,15 +7,17 @@ import java.util.List; import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ExtendedAttribute; +import java.util.Vector; + /** * Manages the collection of all 3270 fields currently present in the Presentation Space. * Conforms to IBM ECL ECLFieldList specification. */ -public class ECLFieldList { +public class ECLFieldList extends Vector { + private static final long serialVersionUID = 1L; private final ECLPS ps; private final ScreenBuffer screen; - private final List fields = new ArrayList<>(); public ECLFieldList(ECLPS ps, ScreenBuffer screen) { this.ps = ps; @@ -23,11 +25,14 @@ public class ECLFieldList { refresh(); } + public ECLPS getPS() { return ps; } + public ScreenBuffer getScreenBuffer() { return screen; } + /** * Refresh the field list by scanning the presentation space buffer. */ public synchronized void refresh() { - fields.clear(); + clear(); if (screen == null || !screen.isFormatted()) { return; } @@ -66,35 +71,35 @@ public class ECLFieldList { } byte faVal = screen.getCell(startPos).fa; - fields.add(new ECLField(ps, startPos, dataStart, endPos, len, faVal)); + add(new ECLField(ps, startPos, dataStart, endPos, len, faVal)); } } public void Refresh() { refresh(); } public synchronized int getFieldCount() { - return fields.size(); + return size(); } public int GetFieldCount() { return getFieldCount(); } public synchronized List getFields() { - return Collections.unmodifiableList(new ArrayList<>(fields)); + return Collections.unmodifiableList(new ArrayList<>(this)); } public List GetFields() { return getFields(); } public synchronized ECLField getFirstField() { - if (fields.isEmpty()) return null; - return fields.get(0); + if (isEmpty()) return null; + return get(0); } public ECLField GetFirstField() { return getFirstField(); } public synchronized ECLField getNextField(ECLField prev) { - if (prev == null || fields.isEmpty()) return getFirstField(); - for (int i = 0; i < fields.size(); i++) { - ECLField f = fields.get(i); + if (prev == null || isEmpty()) return getFirstField(); + for (int i = 0; i < size(); i++) { + ECLField f = get(i); if (f.equals(prev) || f.getStart() == prev.getStart()) { - if (i + 1 < fields.size()) { - return fields.get(i + 1); + if (i + 1 < size()) { + return get(i + 1); } return null; } @@ -107,12 +112,12 @@ public class ECLFieldList { * Find the field that contains the specified buffer position. */ public synchronized ECLField findField(int pos) { - if (fields.isEmpty() || screen == null) return null; + if (isEmpty() || screen == null) return null; int size = screen.getRows() * screen.getCols(); if (size <= 0) return null; pos = ((pos % size) + size) % size; - for (ECLField f : fields) { + for (ECLField f : this) { int start = f.getStart(); int end = f.getEnd(); if (start <= end) { @@ -140,14 +145,14 @@ public class ECLFieldList { * Get the field preceding the given field in the field list. */ public synchronized ECLField getPreviousField(ECLField next) { - if (next == null || fields.isEmpty()) return null; - for (int i = 0; i < fields.size(); i++) { - ECLField f = fields.get(i); + if (next == null || isEmpty()) return null; + for (int i = 0; i < size(); i++) { + ECLField f = get(i); if (f.equals(next) || f.getStart() == next.getStart()) { if (i > 0) { - return fields.get(i - 1); + return get(i - 1); } else { - return fields.get(fields.size() - 1); + return get(size() - 1); } } } @@ -195,29 +200,29 @@ public class ECLFieldList { * Find field containing the given text string. */ public synchronized ECLField findField(String text, int startPos) { - if (text == null || text.isEmpty() || fields.isEmpty()) return null; + if (text == null || text.isEmpty() || isEmpty()) return null; int size = screen.getRows() * screen.getCols(); if (size <= 0) return null; startPos = ((startPos % size) + size) % size; // Find starting index in field list int startIdx = 0; - for (int i = 0; i < fields.size(); i++) { - if (fields.get(i).getStart() >= startPos) { + for (int i = 0; i < size(); i++) { + if (get(i).getStart() >= startPos) { startIdx = i; break; } } - for (int i = startIdx; i < fields.size(); i++) { - ECLField f = fields.get(i); + for (int i = startIdx; i < size(); i++) { + ECLField f = get(i); if (f.getText().contains(text)) { return f; } } // Wrap around search to beginning of field list for (int i = 0; i < startIdx; i++) { - ECLField f = fields.get(i); + ECLField f = get(i); if (f.getText().contains(text)) { return f; } @@ -265,18 +270,18 @@ public class ECLFieldList { public boolean MatchAttributes(ECLField f, int mask) { return matchAttributes(f, mask); } public synchronized ECLField locateField(int attrMask, ECLField prev) { - if (fields.isEmpty()) return null; + if (isEmpty()) return null; int startIndex = 0; if (prev != null) { - int idx = fields.indexOf(prev); - if (idx >= 0 && idx + 1 < fields.size()) { + int idx = indexOf(prev); + if (idx >= 0 && idx + 1 < size()) { startIndex = idx + 1; } else { return null; } } - for (int i = startIndex; i < fields.size(); i++) { - ECLField f = fields.get(i); + for (int i = startIndex; i < size(); i++) { + ECLField f = get(i); if (matchAttributes(f, attrMask)) { return f; } 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 29a7885..a0f8716 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java @@ -2,6 +2,7 @@ package haus.nightmare.lib3270j.ecl; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @@ -109,6 +110,8 @@ public class ECLOIA implements ECLConstants { session != null && session.getClient() != null ? session.getClient().getTelnetFSM() : null); } + private final List hodOiaNotifies = new CopyOnWriteArrayList<>(); + public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) { if (listener != null && !listeners.contains(listener)) { listeners.add(listener); @@ -132,6 +135,29 @@ public class ECLOIA implements ECLConstants { UnregisterOIAEvent(listener); } + public void RegisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) { + if (notify != null && !hodOiaNotifies.contains(notify)) { + hodOiaNotifies.add(notify); + } + } + + public void registerOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) { + RegisterOIAEvent(notify); + } + + public void UnregisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) { + if (notify != null) { + hodOiaNotifies.remove(notify); + try { + notify.NotifyStop(new com.ibm.eNetwork.ECL.ECLOIA(this), STOP_UNREGISTER); + } catch (Exception ignored) {} + } + } + + public void unregisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) { + UnregisterOIAEvent(notify); + } + public void RegisterOIAEvent(ECLOIAListener listener) { if (listener != null && !oiaListeners.contains(listener)) { oiaListeners.add(listener); @@ -166,6 +192,14 @@ public class ECLOIA implements ECLConstants { l.OIANotifyError(this, err); } catch (Exception ignored) {} } + for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) { + try { + com.ibm.eNetwork.ECL.ECLErr ferr = (err instanceof com.ibm.eNetwork.ECL.ECLErr) + ? (com.ibm.eNetwork.ECL.ECLErr) err + : new com.ibm.eNetwork.ECL.ECLErr(err.getMessage()); + l.NotifyError(new com.ibm.eNetwork.ECL.ECLOIA(this), ferr); + } catch (Exception ignored) {} + } } public void notifyOIAStop(int reason) { @@ -179,6 +213,11 @@ public class ECLOIA implements ECLConstants { l.OIANotifyStop(this, reason); } catch (Exception ignored) {} } + for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) { + try { + l.NotifyStop(new com.ibm.eNetwork.ECL.ECLOIA(this), reason); + } catch (Exception ignored) {} + } } private final ReentrantLock oiaLock = new ReentrantLock(); @@ -210,6 +249,11 @@ public class ECLOIA implements ECLConstants { } } catch (Exception ignored) {} } + for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) { + try { + l.OIANotifyEvent(new com.ibm.eNetwork.ECL.ECLOIAEvent(event)); + } catch (Exception ignored) {} + } } public boolean isInsertMode() { @@ -267,6 +311,11 @@ public class ECLOIA implements ECLConstants { public void setInputInhibited(int reason) { if (this.inhibitOverride != reason) { this.inhibitOverride = reason; + if (reason == INHIBIT_NOT_INHIBITED && inputProcessor != null && inputProcessor.isKeyboardLocked()) { + inputProcessor.setKeyboardLocked(false); + } else if (reason == INHIBIT_SYSTEM_LOCK && inputProcessor != null && !inputProcessor.isKeyboardLocked()) { + inputProcessor.setKeyboardLocked(true); + } notifyOIAChanged(); } } @@ -424,6 +473,14 @@ public class ECLOIA implements ECLConstants { return getInputInhibited(); } + public boolean isKeyLocked() { + return getInputInhibited() != INHIBIT_NOT_INHIBITED; + } + + public boolean IsKeyLocked() { + return isKeyLocked(); + } + /** * Block until the keyboard becomes unlocked and ready for user input, or timeout expires. * @return true if keyboard unlocked, false if timeout occurred. 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 c0f984b..f444b52 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java @@ -5,6 +5,7 @@ import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.listener.ScreenUpdateListener; import haus.nightmare.lib3270j.screen.ExtendedAttribute; import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @@ -155,20 +156,94 @@ public class ECLPS implements ECLConstants { public ECLPSTHAIServices GetECLPSTHAIServices() { return thaiServices; } public ECLPSTHAIServices getTHAIServices() { return thaiServices; } - public int getSize() { return screen.getRows() * screen.getCols(); } - public int getRows() { return screen.getRows(); } - public int getCols() { return screen.getCols(); } - public int getCursorPos(){ return screen.getCursorAddress(); } - public int getCursorRow(){ return screen.getCursorRow(); } - public int getCursorCol(){ return screen.getCursorCol(); } + public int getSize() { return screen != null ? screen.getRows() * screen.getCols() : 0; } + public int getRows() { return screen != null ? screen.getRows() : 0; } + public int getCols() { return screen != null ? screen.getCols() : 0; } + public int GetSize() { return getSize(); } + public int GetRows() { return getRows(); } + public int GetCols() { return getCols(); } + public int GetSizeRows() { return getRows(); } + public int GetSizeCols() { return getCols(); } + + public int getCursorPos(){ return screen != null ? screen.getCursorAddress() : 0; } + public int getCursorRow(){ return screen != null ? screen.getCursorRow() : 0; } + public int getCursorCol(){ return screen != null ? screen.getCursorCol() : 0; } + + public synchronized int GetCursorPos() { + return (screen != null) ? screen.getCursorAddress() + 1 : 1; + } + + public synchronized int GetCursorRow() { + return (screen != null) ? screen.getCursorRow() + 1 : 1; + } + + public synchronized int GetCursorCol() { + return (screen != null) ? screen.getCursorCol() + 1 : 1; + } public void setCursorPos(int pos) { - screen.setCursorAddress(pos); + if (screen != null) { + screen.setCursorAddress(pos); + } } public void setCursorPos(int row, int col) { - int pos = screen.rowColToAddress(row, col); - screen.setCursorAddress(pos); + if (screen != null) { + int pos = screen.rowColToAddress(row, col); + screen.setCursorAddress(pos); + } + } + + public synchronized void SetCursorPos(int pos) throws com.ibm.eNetwork.ECL.ECLErr { + if (pos < 1 || pos > getSize()) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetCursorPos", "ECL0010", "1", String.valueOf(pos)); + } + setCursorPos(pos - 1); + notifyCursorMoved(0, pos - 1); + } + + public synchronized void SetCursorPos(int row, int col) throws com.ibm.eNetwork.ECL.ECLErr { + SetCursorPos(ConvertRowColToPos(row, col)); + } + + public synchronized int ConvertPosToRow(int pos) throws com.ibm.eNetwork.ECL.ECLErr { + if (pos < 1 || pos > getSize()) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToRow", "ECL0010", "1", String.valueOf(pos)); + } + int cols = getCols(); + return (cols > 0) ? ((pos - 1) / cols + 1) : 1; + } + + public synchronized int ConvertPosToCol(int pos) throws com.ibm.eNetwork.ECL.ECLErr { + if (pos < 1 || pos > getSize()) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToCol", "ECL0010", "1", String.valueOf(pos)); + } + int cols = getCols(); + return (cols > 0) ? ((pos - 1) % cols + 1) : 1; + } + + public synchronized int ConvertRowColToPos(int row, int col) throws com.ibm.eNetwork.ECL.ECLErr { + if (row < 1 || row > getRows()) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertRowColToPos", "ECL0010", "\"row\"", String.valueOf(row)); + } + if (col < 1 || col > getCols()) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertRowColToPos", "ECL0010", "\"col\"", String.valueOf(col)); + } + return (row - 1) * getCols() + col; + } + + public synchronized void ConvertPosToRowCol(int pos, int[] rowCol) throws com.ibm.eNetwork.ECL.ECLErr { + if (rowCol == null || rowCol.length < 2) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToRowCol", "ECL0010", "\"rowCol\"", "invalid array"); + } + rowCol[0] = ConvertPosToRow(pos); + rowCol[1] = ConvertPosToCol(pos); + } + + public synchronized int[] ConvertPosToRowCol(int pos) throws com.ibm.eNetwork.ECL.ECLErr { + int[] rc = new int[2]; + ConvertPosToRowCol(pos, rc); + return rc; } /** @@ -277,6 +352,60 @@ public class ECLPS implements ECLConstants { return getString(pos, length); } + public synchronized int GetString(char[] buffer, int len, int pos) throws com.ibm.eNetwork.ECL.ECLErr { + return GetString(buffer, len, pos, len); + } + + public synchronized int GetString(char[] buffer, int len, int pos, int length) throws com.ibm.eNetwork.ECL.ECLErr { + if (buffer == null) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0011", "1"); + } + int size = getSize(); + if (pos < 1 || pos > size) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"position\"", String.valueOf(pos)); + } + if (len <= 0) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"bufferLength\"", String.valueOf(len)); + } + if (length <= 0) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"length\"", String.valueOf(length)); + } + int copyCount = Math.min(Math.min(buffer.length, len), Math.min(length, size - pos + 1)); + char[] temp = new char[copyCount]; + getPlane(PLANE_TEXT, temp, pos - 1, copyCount); + System.arraycopy(temp, 0, buffer, 0, copyCount); + return copyCount; + } + + public synchronized int GetString(char[] buffer, int len, int row, int col, int length) throws com.ibm.eNetwork.ECL.ECLErr { + return GetString(buffer, len, ConvertRowColToPos(row, col), length); + } + + public synchronized String GetString(int pos, int length) throws com.ibm.eNetwork.ECL.ECLErr { + int size = getSize(); + if (pos < 1 || pos > size) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"pos\"", String.valueOf(pos)); + } + return getString(pos - 1, length); + } + + public synchronized String GetString(int row, int col, int length) throws com.ibm.eNetwork.ECL.ECLErr { + return GetString(ConvertRowColToPos(row, col), length); + } + + public synchronized int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) { + if (buffer == null || len <= 0 || length <= 0 || screen == null) return 0; + int r = (row > 0) ? row - 1 : 0; + int c = (col > 0) ? col - 1 : 0; + int pos = screen.rowColToAddress(r, c); + int copyCount = Math.min(Math.min(buffer.length, len), length); + return getPlane(plane, buffer, pos, copyCount); + } + + public int getScreen(char[] buffer, int len, int row, int col, int length, int plane) { + return GetScreen(buffer, len, row, col, length, plane); + } + /** * Insert text directly into unprotected fields in the presentation space starting at pos. */ @@ -319,6 +448,24 @@ public class ECLPS implements ECLConstants { setText(text, p); } + public synchronized void SetString(String text, int pos) throws com.ibm.eNetwork.ECL.ECLErr { + if (text == null) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"text\"", "null"); + } + if (pos < 1 || pos > getSize()) { + throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"pos\"", String.valueOf(pos)); + } + SetText(text, pos); + } + + public synchronized void SetString(String text, int row, int col) throws com.ibm.eNetwork.ECL.ECLErr { + SetString(text, ConvertRowColToPos(row, col)); + } + + public synchronized void SetString(String text) throws com.ibm.eNetwork.ECL.ECLErr { + SetString(text, GetCursorPos()); + } + /** * Search for a string in the presentation space (0-based indexing). * Returns 0-based position, or -1 if not found. @@ -500,6 +647,86 @@ public class ECLPS implements ECLConstants { return SearchPSExt(text, startRow, startCol, endRow, endCol, dir, ignoreCase, wrap); } + // ========== IBM HoD SearchText / SearchString (1-based API) ========== + + public synchronized int SearchText(String text) { + return SearchText(text, 1, SEARCH_FORWARD); + } + + public synchronized int searchText(String text) { + return SearchText(text); + } + + public synchronized int SearchText(String text, int dir) { + return dir == SEARCH_BACKWARD ? SearchText(text, getSize(), dir) : SearchText(text, 1, dir); + } + + public synchronized int searchText(String text, int dir) { + return SearchText(text, dir); + } + + public synchronized int SearchText(String text, int arg1, int arg2) { + int startPos; + int dir; + if (arg1 > 2) { + startPos = arg1; + dir = arg2; + } else if (arg2 > 2) { + dir = arg1; + startPos = arg2; + } else { + dir = arg1; + startPos = arg2; + } + if (text == null || text.isEmpty() || screen == null) return 0; + int size = getSize(); + if (size <= 0) return 0; + if (startPos < 1) startPos = 1; + if (startPos > size) startPos = size; + + char[] fullScreen = new char[size]; + getPlane(PLANE_TEXT, fullScreen, 0, size); + for (int i = 0; i < size; i++) { + if (fullScreen[i] == '\0') fullScreen[i] = ' '; + } + String screenText = new String(fullScreen); + int idx = (dir == SEARCH_BACKWARD) ? screenText.lastIndexOf(text, startPos - 1) + : screenText.indexOf(text, startPos - 1); + return idx == -1 ? 0 : idx + 1; + } + + public synchronized int searchText(String text, int arg1, int arg2) { + return SearchText(text, arg1, arg2); + } + + public synchronized int SearchText(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr { + return SearchText(text, ConvertRowColToPos(row, col), dir); + } + + public synchronized int searchText(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr { + return SearchText(text, row, col, dir); + } + + public synchronized int SearchString(String text) { + return SearchText(text); + } + + public synchronized int searchString(String text, int dir) { + return SearchText(text, dir); + } + + public synchronized int SearchString(String text, int dir) { + return SearchText(text, dir); + } + + public synchronized int SearchString(String text, int arg1, int arg2) { + return SearchText(text, arg1, arg2); + } + + public synchronized int SearchString(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr { + return SearchText(text, row, col, dir); + } + private boolean matchesAt(String screenText, String target, int pos, int size) { int len = target.length(); for (int j = 0; j < len; j++) { @@ -920,6 +1147,10 @@ public class ECLPS implements ECLConstants { // ========== ECLPS Event Listener Management ========== private final java.util.List psListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + private final java.util.List psNotifies = new java.util.concurrent.CopyOnWriteArrayList<>(); + private final java.util.Map notifyEventTypes = new java.util.concurrent.ConcurrentHashMap<>(); + private final java.util.Map notifyDescriptors = new java.util.concurrent.ConcurrentHashMap<>(); + private final java.util.List hodPsListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); public boolean isCursorVisible() { return cursorVisible; } public boolean GetCursorVisible() { return cursorVisible; } @@ -1007,6 +1238,111 @@ public class ECLPS implements ECLConstants { UnregisterPSEvent(listener); } + public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) { + RegisterPSEvent(notify, ALL_EVENTS); + } + + public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) { + RegisterPSEvent(notify); + } + + public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, int eventType) { + if (notify != null) { + notifyEventTypes.put(notify, eventType); + if (!psNotifies.contains(notify)) { + psNotifies.add(notify); + } + } + } + + public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, int eventType) { + RegisterPSEvent(notify, eventType); + } + + public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc) { + RegisterPSEvent(notify, desc, ALL_EVENTS); + } + + public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc) { + RegisterPSEvent(notify, desc); + } + + public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc, int eventType) { + if (notify != null) { + if (desc != null) { + notifyDescriptors.put(notify, desc); + } else { + notifyDescriptors.remove(notify); + } + notifyEventTypes.put(notify, eventType); + if (!psNotifies.contains(notify)) { + psNotifies.add(notify); + } + } + } + + public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc, int eventType) { + RegisterPSEvent(notify, desc, eventType); + } + + public void UnregisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) { + if (notify != null) { + psNotifies.remove(notify); + notifyDescriptors.remove(notify); + notifyEventTypes.remove(notify); + try { + notify.NotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), STOP_UNREGISTER); + } catch (Exception ignored) {} + } + } + + public void unregisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) { + UnregisterPSEvent(notify); + } + + public void RegisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) { + if (listener != null && !hodPsListeners.contains(listener)) { + hodPsListeners.add(listener); + } + } + + public void registerPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) { + RegisterPSEvent(listener); + } + + public void UnregisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) { + if (listener != null) { + hodPsListeners.remove(listener); + } + } + + public void unregisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) { + UnregisterPSEvent(listener); + } + + private static final String[] HACL_MNEMONICS = new String[]{ + "[attn]", "[backspace]", "[backtab]", "[clear]", "[down]", "[left]", "[right]", "[up]", + "[delete]", "[enter]", "[tab]", "[eof]", "[eraseeof]", "[erasefld]", "[erinp]", + "[pf1]", "[pf2]", "[pf3]", "[pf4]", "[pf5]", "[pf6]", "[pf7]", "[pf8]", "[pf9]", + "[pf10]", "[pf11]", "[pf12]", "[pf13]", "[pf14]", "[pf15]", "[pf16]", "[pf17]", + "[pf18]", "[pf19]", "[pf20]", "[pf21]", "[pf22]", "[pf23]", "[pf24]", + "[newline]", "[pa1]", "[pa2]", "[pa3]", "[pagedn]", "[pageup]", "[sysreq]", + "[wordleft]", "[wordright]", "[backtabword]", "[tabword]", "[previousword]", + "[nextword]", "[deleteword]", "[insert]", "[home]", "[reset]", "[dup]", + "[fieldmark]", "[fldext]", "[field+]", "[field-]", "[test]", "[help]", + "[bof]", "[cursel]", "[selfld]", "[deselfld]", "[+cr]", "[altview]", + "[printhost]", "[screenrev]", "[copy]", "[paste]", "[cut]", "[markleft]", + "[markright]", "[markup]", "[markdown]", "[unmark]", "[enterreset]" + }; + + public String[] GetSendKeyMnemonics() { + return HACL_MNEMONICS.clone(); + } + + public String[] getSendKeyMnemonics() { + return GetSendKeyMnemonics(); + } + public void dispatchEvent(ECLPSEvent event) { notifyPSEvent(event); } @@ -1045,6 +1381,26 @@ public class ECLPS implements ECLConstants { } } catch (Exception ignored) {} } + for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) { + Object d = notifyDescriptors.get(n); + if (d instanceof ECLScreenDesc && !((ECLScreenDesc) d).Matches(this, session != null ? session.GetOIA() : null)) { + continue; + } + int filter = notifyEventTypes.getOrDefault(n, ALL_EVENTS); + int evtCategory = event.GetType(); + if (filter != ALL_EVENTS && (filter & evtCategory) == 0) { + continue; + } + try { + n.NotifyEvent(new com.ibm.eNetwork.ECL.ECLPS(this)); + } catch (Exception ignored) {} + } + for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) { + try { + com.ibm.eNetwork.ECL.event.ECLPSEvent facadeEvt = new com.ibm.eNetwork.ECL.event.ECLPSEvent(event); + hl.PSNotifyEvent(facadeEvt); + } catch (Exception ignored) {} + } } public void notifyPSError(ECLErr err) { @@ -1053,6 +1409,19 @@ public class ECLPS implements ECLConstants { l.PSNotifyError(this, err); } catch (Exception ignored) {} } + com.ibm.eNetwork.ECL.ECLErr ferr = (err instanceof com.ibm.eNetwork.ECL.ECLErr) + ? (com.ibm.eNetwork.ECL.ECLErr) err + : new com.ibm.eNetwork.ECL.ECLErr(err != null ? err.getMessage() : ""); + for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) { + try { + n.NotifyError(new com.ibm.eNetwork.ECL.ECLPS(this), ferr); + } catch (Exception ignored) {} + } + for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) { + try { + hl.PSNotifyError(new com.ibm.eNetwork.ECL.ECLPS(this), ferr); + } catch (Exception ignored) {} + } } public void notifyPSStop(int reason) { @@ -1061,6 +1430,16 @@ public class ECLPS implements ECLConstants { l.PSNotifyStop(this, reason); } catch (Exception ignored) {} } + for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) { + try { + n.NotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), reason); + } catch (Exception ignored) {} + } + for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) { + try { + hl.PSNotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), reason); + } catch (Exception ignored) {} + } } public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full) { @@ -1128,6 +1507,25 @@ public class ECLPS implements ECLConstants { sendKeys(keys); } + public void sendKeys(String keys, int pos) { + if (pos >= 1 && pos <= getSize()) { + setCursorPos(pos - 1); + } + sendKeys(keys); + } + + public void SendKeys(String keys, int pos) { + sendKeys(keys, pos); + } + + public void SendKeysNoAid(String string, boolean bl) { + sendKeys(string); + } + + public void SendKeysNoAid(String string, int row, int col, boolean bl) { + sendKeys(string, row, col); + } + /** * Send keystrokes with configurable inter-character or inter-mnemonic delay. */ @@ -1154,6 +1552,17 @@ public class ECLPS implements ECLConstants { sendCharacters(keys, row, col, delayMs); } + public void sendCharacters(String keys, int pos, int delayMs) { + if (pos >= 1 && pos <= getSize()) { + setCursorPos(pos - 1); + } + sendCharacters(keys, delayMs); + } + + public void SendCharacters(String keys, int pos, int delayMs) { + sendCharacters(keys, pos, delayMs); + } + public void sendCharacters(String keys, int delayMs) { if (keys == null || keys.isEmpty()) return; if (delayMs <= 0) { @@ -1161,37 +1570,36 @@ public class ECLPS implements ECLConstants { return; } - int i = 0; - int len = keys.length(); - while (i < len) { - if (keys.charAt(i) == '[') { - if (i + 1 < len && keys.charAt(i + 1) == '[') { - // Escaped bracket "[[" - sendKeys("["); - i += 2; + try { + SendKeyTokenizer tokenizer = new SendKeyTokenizer(keys); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (token == null) break; + + if (token.length() == 1) { + if (inputProcessor != null) inputProcessor.typeCharacter(token.charAt(0)); + if (delayMs > 0 && tokenizer.hasMoreTokens()) { + Thread.sleep(delayMs); + } + } else if (token.startsWith("[") && token.endsWith("]")) { + if (inputProcessor != null) inputProcessor.executeMnemonicToken(token); + if (delayMs > 0 && tokenizer.hasMoreTokens()) { + Thread.sleep(delayMs); + } } else { - int close = keys.indexOf(']', i); - if (close > i) { - String mnemonic = keys.substring(i, close + 1); - sendKeys(mnemonic); - i = close + 1; - } else { - sendKeys(keys.substring(i, i + 1)); - i++; + for (int i = 0; i < token.length(); i++) { + if (inputProcessor != null) inputProcessor.typeCharacter(token.charAt(i)); + if (delayMs > 0 && (i < token.length() - 1 || tokenizer.hasMoreTokens())) { + Thread.sleep(delayMs); + } } } - } else { - sendKeys(keys.substring(i, i + 1)); - i++; } - - if (delayMs > 0 && i < len) { - try { - Thread.sleep(delayMs); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ECLErr e) { + if (inputProcessor != null) { + sendKeys(keys); } } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java index cbc075a..8ce1fad 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java @@ -196,6 +196,14 @@ public class ECLPSEvent extends EventObject { return getECLPSUpdate(); } + public ECLPSUpdate getPSUpdate() { + return getECLPSUpdate(); + } + + public ECLPSUpdate GetPSUpdate() { + return getECLPSUpdate(); + } + public ECLPS getPS() { return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null; } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java index fade815..f7692aa 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java @@ -1,279 +1,467 @@ package haus.nightmare.lib3270j.ecl; -import haus.nightmare.lib3270j.Telnet3270Client; -import haus.nightmare.lib3270j.screen.ScreenBuffer; +import java.io.Serializable; +import java.io.StringReader; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import haus.nightmare.lib3270j.Telnet3270Client; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDOIA; +import haus.nightmare.lib3270j.ecl.screenreco.ECLSDString; +import haus.nightmare.lib3270j.ecl.screenreco.ECLScreenDescriptor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; /** - * Screen descriptor matching engine for IBM Host On-Demand ECL automation. + * Screen descriptor composite 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). + * field counts, OIA status, checksums, and attributes) and supports HACL XML schema serialization. */ -public class ECLScreenDesc { +public class ECLScreenDesc implements Serializable, Cloneable { + private static final long serialVersionUID = 1L; - private final List conditions = new ArrayList<>(); - - private interface ScreenCondition { - boolean matches(ECLPS ps, ECLOIA oia); - } + private final List descriptors = new CopyOnWriteArrayList<>(); + private String name = ""; + private boolean active = true; + private boolean match = false; + public boolean useOIAInhibitStatus = false; public ECLScreenDesc() {} - /** - * Clear all conditions from this descriptor. - */ - public synchronized void Clear() { - conditions.clear(); + public ECLScreenDesc(String xml) { + if (xml != null && !xml.trim().isEmpty()) { + setXML(xml); + } } - public synchronized void clear() { - Clear(); - } + // ========== Descriptor Management ========== - /** - * Add a condition that the given string must appear anywhere on the presentation space. - */ - public synchronized void AddString(String text) { + public void AddDescriptor(ECLScreenDescriptor descriptor) { + if (descriptor != null) { + descriptors.add(descriptor); + } + } + public void addDescriptor(ECLScreenDescriptor descriptor) { AddDescriptor(descriptor); } + + public void RemoveDescriptor(ECLScreenDescriptor descriptor) { + if (descriptor != null) { + descriptors.remove(descriptor); + } + } + public void removeDescriptor(ECLScreenDescriptor descriptor) { RemoveDescriptor(descriptor); } + + public List GetDescriptors() { + return Collections.unmodifiableList(new ArrayList<>(descriptors)); + } + public List getDescriptors() { return GetDescriptors(); } + + public ECLScreenDescriptor Get(int index) { + return descriptors.get(index); + } + public ECLScreenDescriptor get(int index) { return Get(index); } + + public int Size() { return descriptors.size(); } + public int size() { return Size(); } + public int getConditionCount() { return Size(); } + + public void Clear() { descriptors.clear(); } + public void clear() { Clear(); } + + public String GetName() { return name; } + public String getName() { return name; } + public void SetName(String name) { this.name = (name != null) ? name : ""; } + public void setName(String name) { SetName(name); } + + public boolean IsActive() { return active; } + public boolean isActive() { return active; } + public void SetActive(boolean active) { this.active = active; } + public void setActive(boolean active) { SetActive(active); } + + public boolean IsMatch() { return match; } + public boolean isMatch() { return match; } + public void SetMatch(boolean match) { this.match = match; } + public void setMatch(boolean match) { SetMatch(match); } + + // ========== Programmatic Add* Methods ========== + + public void AddString(String text) { if (text == null || text.isEmpty()) return; - conditions.add((ps, oia) -> { - if (ps == null) return false; - return ps.searchString(text) >= 0; - }); + AddDescriptor(new ECLSDString(text)); } + public void addString(String text) { AddString(text); } - 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) { + public void AddString(String text, int row, int col) { AddString(text, row, col, true); } + public void addString(String text, int row, int col) { AddString(text, row, col); } - 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) { + public 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); - }); + AddDescriptor(new ECLSDString(text, row, col, caseSense)); } + public void addString(String text, int row, int col, boolean caseSense) { AddString(text, row, col, caseSense); } - 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) { + public 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); - }); + AddDescriptor(new ECLSDString(text, pos, caseSense)); } + public void addString(String text, int pos, boolean caseSense) { AddString(text, pos, caseSense); } - 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) { + public 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()); - }); + AddDescriptor(new ECLSDString(text, sRow, sCol, eRow, eCol, caseSense, false, false)); } - - public synchronized void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) { + public 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 void AddCursorPos(int row, int col) { + AddDescriptor(new ECLSDCursor(row, col)); } + public void addCursorPos(int row, int col) { AddCursorPos(row, col); } - public synchronized void addCursorPos(int row, int col) { - AddCursorPos(row, col); + public void AddCursorPos(int pos) { + AddDescriptor(new ECLSDCursor(pos)); } + public void addCursorPos(int pos) { AddCursorPos(pos); } - /** - * 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 void AddNumFields(int count) { + AddDescriptor(new ECLSDFields(count)); } + public void addNumFields(int count) { AddNumFields(count); } - public synchronized void addCursorPos(int pos) { - AddCursorPos(pos); + public void AddNumInputFields(int count) { + AddDescriptor(new ECLSDInputFields(count)); } + public void addNumInputFields(int count) { AddNumInputFields(count); } - /** - * 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 void AddOIAStatus(int status) { + ECLSDOIA oia = new ECLSDOIA(); + oia.setExplicitInhibitStatus(status); + AddDescriptor(oia); } + public void addOIAStatus(int status) { AddOIAStatus(status); } - 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) { + public void AddOIAInhibitStatus(int status) { AddOIAStatus(status); } + public void addOIAInhibitStatus(int status) { AddOIAInhibitStatus(status); } + + public void AddAttrib(char c, int row, int col, int plane) { + AddDescriptor(new ECLSDAttrib(c, row, col, plane)); + } + public void addAttrib(char c, int row, int col, int plane) { AddAttrib(c, row, col, plane); } + + public void AddAttrib(char c, int row, int col, int plane, boolean optional) { + AddDescriptor(new ECLSDAttrib(c, row, col, plane, optional)); + } + public void addAttrib(char c, int row, int col, int plane, boolean optional) { + AddAttrib(c, row, col, plane, optional); + } + + public void AddFieldAttr(int pos, int attr) { + AddDescriptor(new ECLScreenDescriptor() { + private static final long serialVersionUID = 1L; + @Override + protected boolean checkMatch(ECLPS ps, ECLOIA oia) { + if (ps == null) return false; + int p0 = (pos > 0) ? pos - 1 : 0; + ECLField f = ps.getField(p0); + if (f == null) return false; + return ps.getFieldList().matchAttributes(f, attr); + } + @Override + public String format(int indent, boolean full) { + return ""; + } + }); + } + public void addFieldAttr(int pos, int attr) { AddFieldAttr(pos, attr); } + + public void AddFieldAttr(int row, int col, int attr) { + AddDescriptor(new ECLScreenDescriptor() { + private static final long serialVersionUID = 1L; + @Override + protected boolean checkMatch(ECLPS ps, ECLOIA oia) { + if (ps == null) return false; + int r = (row > 0) ? row - 1 : 0; + int c = (col > 0) ? col - 1 : 0; + ECLField f = ps.getField(r, c); + if (f == null) return false; + return ps.getFieldList().matchAttributes(f, attr); + } + @Override + public String format(int indent, boolean full) { + return ""; + } + }); + } + public void addFieldAttr(int row, int col, int attr) { AddFieldAttr(row, col, attr); } + + public void AddChecksum(int checksum) { + AddDescriptor(new ECLSDChecksum(checksum)); + } + public void addChecksum(int checksum) { AddChecksum(checksum); } + + public void AddChecksum(int checksum, boolean optional) { + AddDescriptor(new ECLSDChecksum(checksum, optional)); + } + public void addChecksum(int checksum, boolean optional) { AddChecksum(checksum, optional); } + + public void AddBlock(String[] strings, int sRow, int sCol, boolean caseSense) { + AddDescriptor(new ECLSDBlock(strings, sRow, sCol, caseSense)); + } + public void addBlock(String[] strings, int sRow, int sCol, boolean caseSense) { + AddBlock(strings, sRow, sCol, caseSense); + } + + public void AddBlock(ECLSDBlock block) { + AddDescriptor(block); + } + public void addBlock(ECLSDBlock block) { AddBlock(block); } + + // ========== Matching Evaluation ========== - /** - * Check if the current screen state matches all conditions in this descriptor. - */ public synchronized boolean Matches(ECLPS ps, ECLOIA oia) { - if (conditions.isEmpty()) { + if (descriptors.isEmpty()) { + this.match = true; return true; } - for (ScreenCondition cond : conditions) { - if (!cond.matches(ps, oia)) { - return false; + if (ps == null) { + this.match = false; + return false; + } + + boolean hasNonOptional = false; + boolean allNonOptionalMatch = true; + boolean hasOptional = false; + boolean anyOptionalMatch = false; + + for (ECLScreenDescriptor desc : descriptors) { + boolean m = desc.matches(ps, oia); + if (desc.isOptional()) { + hasOptional = true; + if (m) anyOptionalMatch = true; + } else { + hasNonOptional = true; + if (!m) allNonOptionalMatch = false; } } - return true; + + boolean res; + if (hasNonOptional) { + res = allNonOptionalMatch; + } else if (hasOptional) { + res = anyOptionalMatch; + } else { + res = true; + } + + this.match = res; + return res; } - public boolean matches(ECLPS ps, ECLOIA oia) { - return Matches(ps, oia); - } + 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(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 boolean matches(Telnet3270Client client) { - return Matches(client); - } - - /** - * Add a condition on field attributes at a 1-based buffer position. - */ - public synchronized void AddFieldAttr(int pos, int attr) { - conditions.add((ps, oia) -> { - if (ps == null) return false; - int p0 = (pos > 0) ? pos - 1 : 0; - ECLField f = ps.getField(p0); - if (f == null) return false; - return ps.getFieldList().matchAttributes(f, attr); - }); - } - public synchronized void addFieldAttr(int pos, int attr) { - AddFieldAttr(pos, attr); - } - - public synchronized void AddFieldAttr(int row, int col, int attr) { - conditions.add((ps, oia) -> { - if (ps == null) return false; - int r = (row > 0) ? row - 1 : 0; - int c = (col > 0) ? col - 1 : 0; - ECLField f = ps.getField(r, c); - if (f == null) return false; - return ps.getFieldList().matchAttributes(f, attr); - }); - } - public synchronized void addFieldAttr(int row, int col, int attr) { - AddFieldAttr(row, col, attr); - } - - public boolean useOIAInhibitStatus = false; - - /** - * Check if a raw ScreenBuffer matches all criteria in this screen descriptor. - */ public boolean Matches(ScreenBuffer buf) { if (buf == null) return false; return Matches(new ECLPS(buf, null, null), null); } - public boolean matches(ScreenBuffer buf) { - return Matches(buf); + public boolean matches(ScreenBuffer buf) { return Matches(buf); } + + // ========== XML Serialization & Parsing ========== + + public String getXML() { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + for (ECLScreenDescriptor desc : descriptors) { + sb.append(" ").append(desc.format(0, false)).append("\n"); + } + sb.append(""); + return sb.toString(); + } + public String GetXML() { return getXML(); } + + public void setXML(String xml) { + Clear(); + if (xml == null || xml.trim().isEmpty()) { + return; + } + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + try { + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } catch (Throwable ignored) {} + try { + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + } catch (Throwable ignored) {} + try { + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + } catch (Throwable ignored) {} + + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new InputSource(new StringReader(xml.trim()))); + Element root = doc.getDocumentElement(); + + Element descElement = root; + if (!root.getTagName().equalsIgnoreCase("description")) { + NodeList list = root.getElementsByTagName("description"); + if (list.getLength() > 0) { + descElement = (Element) list.item(0); + } + } + + NodeList children = descElement.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + Element elem = (Element) node; + String tag = elem.getTagName().toLowerCase(); + + boolean optional = "true".equalsIgnoreCase(elem.getAttribute("optional")); + boolean invertmatch = "true".equalsIgnoreCase(elem.getAttribute("invertmatch")); + String hostid = elem.getAttribute("hostid"); + + if (tag.equals("string")) { + String value = elem.getAttribute("value"); + int row = parseIntAttr(elem, "row", 0); + int col = parseIntAttr(elem, "col", 0); + int erow = parseIntAttr(elem, "erow", -1); + int ecol = parseIntAttr(elem, "ecol", -1); + boolean casesense = "true".equalsIgnoreCase(elem.getAttribute("casesense")); + boolean wrap = "true".equalsIgnoreCase(elem.getAttribute("wrap")); + + ECLSDString s; + if (row > 0 && col > 0) { + if (erow > 0 || ecol > 0) { + s = new ECLSDString(value, row, col, erow, ecol, casesense, optional, wrap); + } else { + s = new ECLSDString(value, row, col, casesense, optional); + } + } else { + s = new ECLSDString(value); + s.setCaseSense(casesense); + s.setOptional(optional); + } + s.setInvertMatch(invertmatch); + s.setHostid(hostid); + AddDescriptor(s); + } else if (tag.equals("cursor")) { + int row = parseIntAttr(elem, "row", 1); + int col = parseIntAttr(elem, "col", 1); + int pos = parseIntAttr(elem, "position", -1); + ECLSDCursor c; + if (pos > 0) { + c = new ECLSDCursor(pos, optional); + } else { + c = new ECLSDCursor(row, col, optional); + } + c.setInvertMatch(invertmatch); + c.setHostid(hostid); + AddDescriptor(c); + } else if (tag.equals("numfields") || tag.equals("fields")) { + int num = parseIntAttr(elem, elem.hasAttribute("number") ? "number" : "value", 0); + ECLSDFields f = new ECLSDFields(num, optional); + f.setInvertMatch(invertmatch); + f.setHostid(hostid); + AddDescriptor(f); + } else if (tag.equals("numinputfields") || tag.equals("inputfields")) { + int num = parseIntAttr(elem, elem.hasAttribute("number") ? "number" : "value", 0); + ECLSDInputFields inf = new ECLSDInputFields(num, optional); + inf.setInvertMatch(invertmatch); + inf.setHostid(hostid); + AddDescriptor(inf); + } else if (tag.equals("oia")) { + String status = elem.hasAttribute("status") ? elem.getAttribute("status") : + (elem.hasAttribute("input") ? elem.getAttribute("input") : "NOTINHIBITED"); + ECLSDOIA oia = new ECLSDOIA(status, optional); + oia.setInvertMatch(invertmatch); + oia.setHostid(hostid); + AddDescriptor(oia); + } else if (tag.equals("checksum")) { + int value = parseIntAttr(elem, "value", 0); + ECLSDChecksum cs = new ECLSDChecksum(value, optional); + cs.setInvertMatch(invertmatch); + cs.setHostid(hostid); + AddDescriptor(cs); + } else if (tag.equals("attrib")) { + String valStr = elem.getAttribute("value"); + char val = ECLSDAttrib.StringToAttrib(valStr); + int row = parseIntAttr(elem, "row", 1); + int col = parseIntAttr(elem, "col", 1); + int plane = ECLSDAttrib.StringToPlane(elem.getAttribute("plane")); + ECLSDAttrib attr = new ECLSDAttrib(val, row, col, plane, optional); + attr.setInvertMatch(invertmatch); + attr.setHostid(hostid); + AddDescriptor(attr); + } else if (tag.equals("block")) { + int row = parseIntAttr(elem, "row", 1); + int col = parseIntAttr(elem, "col", 1); + int erow = parseIntAttr(elem, "erow", -1); + int ecol = parseIntAttr(elem, "ecol", -1); + boolean casesense = "true".equalsIgnoreCase(elem.getAttribute("casesense")); + + ECLSDBlock blk; + if (erow > 0 || ecol > 0) { + blk = new ECLSDBlock(row, col, erow, ecol, casesense, optional); + } else { + blk = new ECLSDBlock(row, col, casesense, optional); + } + blk.setInvertMatch(invertmatch); + blk.setHostid(hostid); + + NodeList subList = elem.getElementsByTagName("string"); + for (int j = 0; j < subList.getLength(); j++) { + Element strElem = (Element) subList.item(j); + blk.addString(strElem.getAttribute("value")); + } + AddDescriptor(blk); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to parse ECLScreenDesc XML: " + e.getMessage(), e); + } + } + public void SetXML(String xml) { setXML(xml); } + + private static int parseIntAttr(Element elem, String attrName, int defaultVal) { + if (!elem.hasAttribute(attrName)) return defaultVal; + try { + return Integer.parseInt(elem.getAttribute(attrName).trim()); + } catch (NumberFormatException e) { + return defaultVal; + } } - public synchronized int getConditionCount() { - return conditions.size(); + @Override + public String toString() { + return getXML(); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenReco.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenReco.java index 6e3a22d..eb33ebb 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenReco.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenReco.java @@ -11,21 +11,28 @@ public class ECLScreenReco { private final ECLSession session; private final ECLPS ps; + private final ECLOIA oia; private final Map notifyMap = new ConcurrentHashMap<>(); private final Map listenerMap = new ConcurrentHashMap<>(); private final ECLPSListener internalListener; + private final ECLOIANotify internalOiaListener; public ECLScreenReco() { this((ECLPS) null); } public ECLScreenReco(ECLSession session) { - this(session != null ? session.GetPS() : null); + this(session != null ? session.GetPS() : null, session != null ? session.GetOIA() : null); } public ECLScreenReco(ECLPS ps) { + this(ps, (ECLOIA) null); + } + + public ECLScreenReco(ECLPS ps, ECLOIA oia) { this.ps = ps; this.session = (ps != null) ? ps.GetParent() : null; + this.oia = (oia != null) ? oia : ((session != null) ? session.GetOIA() : null); this.internalListener = new ECLPSListener() { @Override @@ -34,9 +41,14 @@ public class ECLScreenReco { } }; + this.internalOiaListener = event -> checkAllScreens(); + if (this.ps != null) { this.ps.RegisterPSEvent(this.internalListener); } + if (this.oia != null) { + this.oia.RegisterOIAEvent(this.internalOiaListener); + } } // ========== Static Recognition Helpers ========== @@ -95,6 +107,16 @@ public class ECLScreenReco { UnregisterScreen(desc, notify); } + public void UnregisterScreen(ECLScreenDesc desc) { + if (desc != null) { + notifyMap.remove(desc); + listenerMap.remove(desc); + } + } + public void unregisterScreen(ECLScreenDesc desc) { + UnregisterScreen(desc); + } + public void RegisterScreen(ECLScreenDesc desc, ECLPSListener listener) { if (desc != null && listener != null) { listenerMap.put(desc, listener); @@ -120,7 +142,10 @@ public class ECLScreenReco { } public boolean IsMatch(ECLScreenDesc desc) { - return IsMatch(ps, desc); + if (desc == null) return true; + if (ps == null) return false; + ECLOIA activeOia = (oia != null) ? oia : ((ps.GetParent() != null) ? ps.GetParent().GetOIA() : null); + return desc.Matches(ps, activeOia); } public boolean isMatch(ECLScreenDesc desc) { return IsMatch(desc); @@ -142,6 +167,9 @@ public class ECLScreenReco { if (ps != null && internalListener != null) { ps.UnregisterPSEvent(internalListener); } + if (oia != null && internalOiaListener != null) { + oia.UnregisterOIAEvent(internalOiaListener); + } notifyMap.clear(); listenerMap.clear(); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDAttrib.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDAttrib.java new file mode 100644 index 0000000..a2a37ed --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDAttrib.java @@ -0,0 +1,126 @@ +package haus.nightmare.lib3270j.ecl.screenreco; + +import haus.nightmare.lib3270j.ecl.ECLOIA; +import haus.nightmare.lib3270j.ecl.ECLPS; + +/** + * Screen descriptor matching field or presentation plane attributes matching IBM Host On-Demand ECLSDAttrib. + */ +public class ECLSDAttrib extends ECLScreenDescriptor { + private static final long serialVersionUID = 1L; + + public static final int TEXT_PLANE = 1; + public static final int COLOR_PLANE = 2; + public static final int DBCS_PLANE = 4; + public static final int GRID_PLANE = 8; + public static final int FIELD_PLANE = 16; + public static final int EXFIELD_PLANE = 32; + + private char attrib = 0x00; + private int row = 1; + private int col = 1; + private int plane = FIELD_PLANE; + + public ECLSDAttrib() { + super(); + } + + public ECLSDAttrib(char attrib, int row, int col, int plane) { + this(attrib, row, col, plane, false); + } + + public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) { + super(optional); + this.attrib = attrib; + this.row = row; + this.col = col; + this.plane = plane; + } + + public char getAttrib() { return attrib; } + public char GetAttrib() { return attrib; } + public void setAttrib(char attrib) { this.attrib = attrib; } + public void SetAttrib(char attrib) { setAttrib(attrib); } + + public int getRow() { return row; } + public int GetRow() { return row; } + public void setRow(int row) { this.row = row; } + public void SetRow(int row) { setRow(row); } + + public int getCol() { return col; } + public int GetCol() { return col; } + public void setCol(int col) { this.col = col; } + public void SetCol(int col) { setCol(col); } + + public int getPlane() { return plane; } + public int GetPlane() { return plane; } + public void setPlane(int plane) { this.plane = plane; } + public void SetPlane(int plane) { setPlane(plane); } + + public static String PlaneToString(int plane) { + switch (plane) { + case TEXT_PLANE: return "TEXT_PLANE"; + case COLOR_PLANE: return "COLOR_PLANE"; + case DBCS_PLANE: return "DBCS_PLANE"; + case GRID_PLANE: return "GRID_PLANE"; + case FIELD_PLANE: return "FIELD_PLANE"; + case EXFIELD_PLANE: return "EXFIELD_PLANE"; + default: return "FIELD_PLANE"; + } + } + + public static int StringToPlane(String str) { + if (str == null) return FIELD_PLANE; + if (str.equalsIgnoreCase("TEXT_PLANE")) return TEXT_PLANE; + if (str.equalsIgnoreCase("COLOR_PLANE")) return COLOR_PLANE; + if (str.equalsIgnoreCase("DBCS_PLANE")) return DBCS_PLANE; + if (str.equalsIgnoreCase("GRID_PLANE")) return GRID_PLANE; + if (str.equalsIgnoreCase("FIELD_PLANE")) return FIELD_PLANE; + if (str.equalsIgnoreCase("EXFIELD_PLANE")) return EXFIELD_PLANE; + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + return FIELD_PLANE; + } + } + + public static String AttribToString(char c) { + return "0x" + Integer.toHexString(c & 0xFFFF); + } + + public static char StringToAttrib(String str) { + if (str == null || str.isEmpty()) return 0; + str = str.trim(); + int xIdx = str.toLowerCase().indexOf('x'); + if (xIdx != -1) { + return (char) Integer.parseInt(str.substring(xIdx + 1), 16); + } + return (char) Integer.parseInt(str); + } + + @Override + protected boolean checkMatch(ECLPS ps, ECLOIA oia) { + if (ps == null) return false; + int totalRows = ps.getRows(); + int totalCols = ps.getCols(); + + if (row < 1 || row > totalRows || col < 1 || col > totalCols) { + return false; + } + + int addr = (row - 1) * totalCols + (col - 1); + char[] buf = new char[1]; + ps.getPlane(plane, buf, addr, 1); + return buf[0] == attrib; + } + + @Override + public String format(int indent, boolean full) { + StringBuilder sb = new StringBuilder(" 0 && scol > 0)) { + for (int i = 0; i < lines.length; i++) { + int r0 = srow + i - 1; + int c0 = scol - 1; + if (r0 < 0 || r0 >= totalRows || c0 < 0 || c0 >= totalCols) return false; + String target = lines[i]; + String onScreen = ps.getString(r0, c0, target.length()); + if (onScreen == null) return false; + boolean eq = caseSense ? target.equals(onScreen) : target.equalsIgnoreCase(onScreen); + if (!eq) return false; + } + return true; + } + + // 2. Rectangular region matching + int sr = (srow > 0) ? srow : 1; + int sc = (scol > 0) ? scol : 1; + int er = (erow > 0) ? Math.min(erow, totalRows) : totalRows; + int ec = (ecol > 0) ? Math.min(ecol, totalCols) : totalCols; + + if (sr > er || sc > ec) return false; + int numLines = lines.length; + if (er - sr + 1 < numLines) return false; + + int rectWidth = ec - sc + 1; + List screenLines = new ArrayList<>(); + for (int r = sr; r <= er; r++) { + String s = ps.getString(r - 1, sc - 1, rectWidth); + screenLines.add(s != null ? s : ""); + } + + String firstTarget = caseSense ? lines[0] : lines[0].toLowerCase(); + for (int i = 0; i <= screenLines.size() - numLines; i++) { + String rowStr = caseSense ? screenLines.get(i) : screenLines.get(i).toLowerCase(); + int matchIdx = rowStr.indexOf(firstTarget); + while (matchIdx != -1) { + boolean allMatch = true; + for (int j = 1; j < numLines; j++) { + String nextRowStr = caseSense ? screenLines.get(i + j) : screenLines.get(i + j).toLowerCase(); + String nextTarget = caseSense ? lines[j] : lines[j].toLowerCase(); + if (matchIdx + nextTarget.length() > nextRowStr.length() || + !nextRowStr.startsWith(nextTarget, matchIdx)) { + allMatch = false; + break; + } + } + if (allMatch) { + return true; + } + matchIdx = rowStr.indexOf(firstTarget, matchIdx + 1); + } + } + return false; + } + + @Override + public String format(int indent, boolean full) { + StringBuilder sb = new StringBuilder(" 0 || full) sb.append("row=\"").append(srow).append("\" "); + if (scol > 0 || full) sb.append("col=\"").append(scol).append("\" "); + if (blockType == BLOCK_INRECT && (erow > 0 || ecol > 0 || full)) { + if (erow > 0 || full) sb.append("erow=\"").append(erow).append("\" "); + if (ecol > 0 || full) sb.append("ecol=\"").append(ecol).append("\" "); + } + if (caseSense || full) sb.append("casesense=\"").append(caseSense).append("\" "); + if (optional || full) sb.append("optional=\"").append(optional).append("\" "); + if (invert || full) sb.append("invertmatch=\"").append(invert).append("\" "); + if (hostid != null && !hostid.isEmpty()) sb.append("hostid=\"").append(escapeChars(hostid)).append("\" "); + sb.append(">\n"); + + for (ECLSDBlockString s : strings) { + sb.append(" ").append(s.format(indent + 1, full)).append("\n"); + } + sb.append(""); + return sb.toString(); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDBlockString.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDBlockString.java new file mode 100644 index 0000000..aeb5b88 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDBlockString.java @@ -0,0 +1,40 @@ +package haus.nightmare.lib3270j.ecl.screenreco; + +import java.io.Serializable; + +/** + * Encapsulates an individual string line within an ECLSDBlock matching IBM Host On-Demand. + */ +public class ECLSDBlockString implements Serializable, Cloneable { + private static final long serialVersionUID = 1L; + + private String text = ""; + + public ECLSDBlockString() {} + + public ECLSDBlockString(String text) { + this.text = (text != null) ? text : ""; + } + + public String getText() { return text; } + public String GetText() { return text; } + public String GetString() { return text; } + public String getString() { return text; } + public void setText(String text) { this.text = (text != null) ? text : ""; } + public void SetText(String text) { setText(text); } + public void SetString(String text) { setText(text); } + public void setString(String text) { setText(text); } + + public String format(int indent, boolean full) { + return ""; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDChecksum.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDChecksum.java new file mode 100644 index 0000000..71fe2d6 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDChecksum.java @@ -0,0 +1,71 @@ +package haus.nightmare.lib3270j.ecl.screenreco; + +import haus.nightmare.lib3270j.ecl.ECLField; +import haus.nightmare.lib3270j.ecl.ECLFieldList; +import haus.nightmare.lib3270j.ecl.ECLOIA; +import haus.nightmare.lib3270j.ecl.ECLPS; + +/** + * Screen descriptor matching screen buffer checksum matching IBM Host On-Demand ECLSDChecksum. + */ +public class ECLSDChecksum extends ECLScreenDescriptor { + private static final long serialVersionUID = 1L; + + private int value = 0; + + public ECLSDChecksum() { + super(); + } + + public ECLSDChecksum(int value) { + this(value, false); + } + + public ECLSDChecksum(int value, boolean optional) { + super(optional); + this.value = value; + } + + public int getValue() { return value; } + public int GetValue() { return value; } + public void setValue(int value) { this.value = value; } + public void SetValue(int value) { setValue(value); } + + public static int CalculateChecksum(ECLFieldList fieldList, int psSize) { + int sum = 0; + if (fieldList != null && fieldList.getFieldCount() > 0) { + int count = fieldList.getFieldCount(); + sum = 17; + ECLField current = null; + for (int i = 0; i < count - 1; i++) { + if (i == 0) { + current = fieldList.getFirstField(); + } + ECLField next = fieldList.getNextField(current); + if (current != null && next != null) { + int delta = next.getStart() - current.getStart() - 1; + sum = sum * 37 + delta; + } + current = next; + } + ECLField last = (current == null) ? fieldList.getFirstField() : current; + int endDelta = (last != null) ? (psSize - last.getStart()) : 0; + sum = sum * 37 + endDelta; + } + return sum; + } + + @Override + protected boolean checkMatch(ECLPS ps, ECLOIA oia) { + if (ps == null) return false; + int calc = CalculateChecksum(ps.getFieldList(), ps.getSize()); + return calc == value; + } + + @Override + public String format(int indent, boolean full) { + StringBuilder sb = new StringBuilder(" 0) { + return (ps.getCursorPos() + 1) == pos; + } + // In ECLPS, getCursorRow() and getCursorCol() return 0-based indices. + int curRow = ps.getCursorRow() + 1; + int curCol = ps.getCursorCol() + 1; + return (curRow == row && curCol == col); + } + + @Override + public String format(int indent, boolean full) { + StringBuilder sb = new StringBuilder(" 0) { + sb.append("position=\"").append(pos).append("\" "); + } else { + sb.append("row=\"").append(row).append("\" "); + sb.append("col=\"").append(col).append("\" "); + } + return formatAttributes(sb.toString(), full); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDFields.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDFields.java new file mode 100644 index 0000000..fed95ac --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLSDFields.java @@ -0,0 +1,47 @@ +package haus.nightmare.lib3270j.ecl.screenreco; + +import haus.nightmare.lib3270j.ecl.ECLFieldList; +import haus.nightmare.lib3270j.ecl.ECLOIA; +import haus.nightmare.lib3270j.ecl.ECLPS; + +/** + * Screen descriptor matching total number of fields matching IBM Host On-Demand ECLSDFields. + */ +public class ECLSDFields extends ECLScreenDescriptor { + private static final long serialVersionUID = 1L; + + private int num = 0; + + public ECLSDFields() { + super(); + } + + public ECLSDFields(int num) { + this(num, false); + } + + public ECLSDFields(int num, boolean optional) { + super(optional); + this.num = num; + } + + public int getNum() { return num; } + public int GetNum() { return num; } + public void setNum(int num) { this.num = num; } + public void SetNum(int num) { setNum(num); } + + @Override + protected boolean checkMatch(ECLPS ps, ECLOIA oia) { + if (ps == null) return false; + ECLFieldList fl = ps.getFieldList(); + if (fl == null) return false; + return fl.getFieldCount() == num; + } + + @Override + public String format(int indent, boolean full) { + StringBuilder sb = new StringBuilder(" 0) { + int p0 = pos - 1; + String onScreen = ps.getString(p0, text.length()); + if (onScreen == null) return false; + return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen); + } + + // 1. Search anywhere on screen if row and col are not set (<= 0) + if (srow <= 0 && scol <= 0 && erow <= 0 && ecol <= 0) { + String fullScreen = ps.getString(0, totalRows * totalCols); + if (fullScreen == null) return false; + return caseSense ? fullScreen.contains(text) : fullScreen.toLowerCase().contains(text.toLowerCase()); + } + + // 2. Absolute match at (srow, scol) + if (stringType == STRING_ABSOLUTE || (erow <= 0 && ecol <= 0 && srow > 0 && scol > 0)) { + int r0 = srow - 1; + int c0 = scol - 1; + if (r0 < 0 || r0 >= totalRows || c0 < 0 || c0 >= totalCols) return false; + String onScreen = ps.getString(r0, c0, text.length()); + if (onScreen == null) return false; + return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen); + } + + // 3. Rectangular region match (srow, scol, erow, ecol) + int sr = (srow > 0) ? srow : 1; + int sc = (scol > 0) ? scol : 1; + int er = (erow > 0) ? Math.min(erow, totalRows) : totalRows; + int ec = (ecol > 0) ? Math.min(ecol, totalCols) : totalCols; + + if (sr > er || sc > ec) return false; + + if (!wrap) { + int len = ec - sc + 1; + for (int r = sr; r <= er; r++) { + String line = ps.getString(r - 1, sc - 1, len); + if (line != null) { + boolean found = caseSense ? line.contains(text) : line.toLowerCase().contains(text.toLowerCase()); + if (found) return true; + } + } + return false; + } else { + StringBuilder sb = new StringBuilder(); + int len = ec - sc + 1; + for (int r = sr; r <= er; r++) { + String line = ps.getString(r - 1, sc - 1, len); + if (line != null) sb.append(line); + } + String block = sb.toString(); + return caseSense ? block.contains(text) : block.toLowerCase().contains(text.toLowerCase()); + } + } + + @Override + public String format(int indent, boolean full) { + StringBuilder sb = new StringBuilder(" 0 || full) { + sb.append("row=\"").append(srow).append("\" "); + } + if (scol > 0 || full) { + sb.append("col=\"").append(scol).append("\" "); + } + if (stringType == STRING_INRECT && (erow > 0 || ecol > 0 || full)) { + if (erow > 0 || full) sb.append("erow=\"").append(erow).append("\" "); + if (ecol > 0 || full) sb.append("ecol=\"").append(ecol).append("\" "); + } + if (caseSense || full) { + sb.append("casesense=\"").append(caseSense).append("\" "); + } + if (wrap || full) { + sb.append("wrap=\"").append(wrap).append("\" "); + } + return formatAttributes(sb.toString(), full); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLScreenDescriptor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLScreenDescriptor.java new file mode 100644 index 0000000..1e39024 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/screenreco/ECLScreenDescriptor.java @@ -0,0 +1,110 @@ +package haus.nightmare.lib3270j.ecl.screenreco; + +import java.io.Serializable; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLOIA; + +/** + * Abstract base class for composite screen recognition descriptors matching IBM Host On-Demand. + */ +public abstract class ECLScreenDescriptor implements Serializable, Cloneable { + private static final long serialVersionUID = 1L; + + protected boolean optional = false; + protected boolean match = false; + protected boolean invert = false; + protected String hostid = ""; + + protected ECLScreenDescriptor() {} + + protected ECLScreenDescriptor(boolean optional) { + this.optional = optional; + } + + public boolean isOptional() { return optional; } + public boolean IsOptional() { return optional; } + public void setOptional(boolean optional) { this.optional = optional; } + public void SetOptional(boolean optional) { setOptional(optional); } + + public boolean isMatch() { return match; } + public boolean IsMatch() { return match; } + public void setMatch(boolean match) { this.match = match; } + public void SetMatch(boolean match) { setMatch(match); } + + public boolean isInvertMatch() { return invert; } + public boolean IsInvertMatch() { return invert; } + public void setInvertMatch(boolean invert) { this.invert = invert; } + public void SetInvertMatch(boolean invert) { setInvertMatch(invert); } + + public String getHostid() { return hostid; } + public String GetHostid() { return hostid; } + public void setHostid(String hostid) { this.hostid = (hostid != null) ? hostid : ""; } + public void SetHostid(String hostid) { setHostid(hostid); } + + /** + * Checks whether this descriptor matches against presentation space and/or OIA. + * Takes invertmatch into account. + */ + public boolean matches(ECLPS ps, ECLOIA oia) { + boolean m = checkMatch(ps, oia); + boolean res = invert ? !m : m; + setMatch(res); + return res; + } + + public boolean Matches(ECLPS ps, ECLOIA oia) { + return matches(ps, oia); + } + + /** + * Internal implementation of descriptor check without inversion. + */ + protected abstract boolean checkMatch(ECLPS ps, ECLOIA oia); + + /** + * Format this descriptor as an XML string. + */ + public abstract String format(int indent, boolean full); + public String Format(int indent, boolean full) { return format(indent, full); } + + protected String formatAttributes(String tagAndSpecificAttrs, boolean full) { + StringBuilder sb = new StringBuilder(tagAndSpecificAttrs); + if (optional || full) { + sb.append("optional=\"").append(optional).append("\" "); + } + if (invert || full) { + sb.append("invertmatch=\"").append(invert).append("\" "); + } + if (hostid != null && !hostid.isEmpty()) { + sb.append("hostid=\"").append(escapeChars(hostid)).append("\" "); + } + sb.append("/>"); + return sb.toString(); + } + + public static String escapeChars(String string) { + if (string == null) return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + switch (c) { + case '&': sb.append("&"); break; + case '<': sb.append("<"); break; + case '>': sb.append(">"); break; + case '"': sb.append("""); break; + case '\'': sb.append("'"); break; + default: sb.append(c); break; + } + } + return sb.toString(); + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/util/SendKeyTokenizer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/util/SendKeyTokenizer.java new file mode 100644 index 0000000..0ab47a5 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/util/SendKeyTokenizer.java @@ -0,0 +1,156 @@ +package haus.nightmare.lib3270j.ecl.util; + +import haus.nightmare.lib3270j.ecl.ECLErr; + +/** + * Parses SendKeys strings into keystroke and mnemonic tokens. + * Conforms 1:1 to IBM Host On-Demand (HoD) com.ibm.eNetwork.ECL.util.SendKeyTokenizer. + */ +public class SendKeyTokenizer { + + private String sendString; + private int stringIndex; + private int stringLength; + protected String className = this.getClass().getName(); + private boolean chunkPlainText = true; + + public SendKeyTokenizer() { + this.setSendKeyString(null); + } + + public SendKeyTokenizer(String string) { + this.setSendKeyString(string); + } + + public SendKeyTokenizer(String string, boolean chunkPlainText) { + this.setSendKeyString(string); + this.chunkPlainText = chunkPlainText; + } + + public void setSendKeyString(String string) { + this.stringLength = string != null ? string.length() : 0; + this.sendString = string; + this.stringIndex = 0; + } + + public String remainder() { + if (this.stringIndex >= this.stringLength) { + return ""; + } + return this.sendString.substring(this.stringIndex); + } + + public boolean hasMoreTokens() { + return this.stringLength > this.stringIndex; + } + + public boolean isChunkPlainText() { + return chunkPlainText; + } + + public void setChunkPlainText(boolean chunkPlainText) { + this.chunkPlainText = chunkPlainText; + } + + /** + * Retrieves the next keystroke or mnemonic token. + * + * Escaped brackets: + * "[[" returns "[" + * "]] " returns "]" + * + * Unterminated or unmatched brackets throw ECLErr("ECL0012"). + */ + public String nextToken() throws ECLErr { + if (this.stringIndex >= this.stringLength) { + return null; + } + + char c = this.sendString.charAt(this.stringIndex); + switch (c) { + case '[': { + this.stringIndex++; + if (this.stringIndex >= this.stringLength) { + throw new ECLErr(this.className + ":1", "ECL0012", 1); + } + char next = this.sendString.charAt(this.stringIndex); + if (next == '[') { + this.stringIndex++; + return "["; + } + + StringBuilder sb = new StringBuilder(); + sb.append('['); + boolean foundClose = false; + while (this.stringIndex < this.stringLength) { + char ch = this.sendString.charAt(this.stringIndex); + if (ch == ']') { + sb.append(']'); + this.stringIndex++; + foundClose = true; + break; + } + sb.append(ch); + this.stringIndex++; + } + + if (!foundClose) { + throw new ECLErr(this.className + ":2", "ECL0012", 1); + } + + String token = sb.toString(); + // HoD numeric keypad single digit shortcut [0]..[9] -> "0".."9" + if (token.length() == 3 && token.charAt(0) == '[' && token.charAt(2) == ']') { + char digit = token.charAt(1); + if (digit >= '0' && digit <= '9') { + return String.valueOf(digit); + } + } + return token; + } + + case ']': { + this.stringIndex++; + if (this.stringIndex >= this.stringLength) { + throw new ECLErr(this.className + ":3", "ECL0012", 1); + } + char next = this.sendString.charAt(this.stringIndex); + if (next == ']') { + this.stringIndex++; + return "]"; + } + throw new ECLErr(this.className + ":4", "ECL0012", 1); + } + + default: { + if (!this.chunkPlainText) { + this.stringIndex++; + return String.valueOf(c); + } + + StringBuilder sb = new StringBuilder(); + while (this.stringIndex < this.stringLength) { + char ch = this.sendString.charAt(this.stringIndex); + if (ch == '[' || ch == ']') { + break; + } + sb.append(ch); + this.stringIndex++; + } + return sb.toString(); + } + } + } + + /** + * Fetch the next character token without AID key translations. + */ + public String nextTokenNoAid() throws ECLErr { + if (this.stringIndex >= this.stringLength) { + throw new ECLErr(this.className + ":1", "ECL0012", 1); + } + char c = this.sendString.charAt(this.stringIndex); + this.stringIndex++; + return String.valueOf(c); + } +} 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 52ac94b..745e36f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -92,6 +92,14 @@ public class InputProcessor { if (fsm == null || fsm.getConnectionState() == null || !fsm.getConnectionState().isConnected()) { return OiaStatus.NOT_CONNECTED; } + if (oia != null) { + if (oia.isXProt()) { + return OiaStatus.X_PROT; + } + if (oia.isXSystem()) { + return OiaStatus.X_SYSTEM; + } + } if (keyboardLocked) { return OiaStatus.X_SYSTEM; } @@ -112,6 +120,11 @@ public class InputProcessor { public void setKeyboardLocked(boolean locked) { if (this.keyboardLocked != locked) { this.keyboardLocked = locked; + if (!locked && oia != null) { + oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED); + } else if (locked && oia != null && oia.getInputInhibited() == ECLConstants.INHIBIT_NOT_INHIBITED) { + oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK); + } if (lockStateListener != null) { lockStateListener.onLockStateChanged(locked); } @@ -859,6 +872,9 @@ public class InputProcessor { } lastAid = aidCode; setKeyboardLocked(true); + if (oia != null) { + oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK); + } byte[] data = new byte[] { (byte) aidCode }; sendAidResponse(data); return; @@ -882,6 +898,9 @@ public class InputProcessor { lastAid = aidCode; setKeyboardLocked(true); + if (oia != null) { + oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK); + } if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) { // SSCP-LU Mode: send raw EBCDIC line data without AID or 3270 cursor address @@ -984,6 +1003,9 @@ public class InputProcessor { return; } setKeyboardLocked(true); + if (oia != null) { + oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK); + } ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] rma = buildReadModifiedInboundData(aidCode, false); @@ -1004,6 +1026,9 @@ public class InputProcessor { return; } setKeyboardLocked(true); + if (oia != null) { + oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK); + } ByteArrayOutputStream out = new ByteArrayOutputStream(); int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80; @@ -1735,11 +1760,110 @@ public class InputProcessor { public void processBackspace() { backspace(); } public void processEraseEOF() { eraseEof(); } public void processEraseInput() { eraseInput(); } + public void processEraseField() { eraseField(); } public void processDup() { dup(); } public void processFieldMark() { fieldMark(); } public void processToggleInsert() { setInsertMode(!isInsertMode()); } public boolean processCurSel() { return cursorSelect(); } public boolean processCursorSelect() { return cursorSelect(); } + public boolean processSelFld() { return selectField(); } + public boolean processDeSelFld() { return deselectField(); } + + public void processFieldStart() { + if (screen == null) return; + int cur = screen.getCursorAddress(); + if (screen.isFormatted()) { + int faPos = screen.findFieldAttribute(cur); + if (faPos >= 0) { + int fieldStart = screen.incrementAddress(faPos); + screen.setCursorAddress(fieldStart); + } + } else { + int cols = screen.getCols(); + int row = cur / cols; + screen.setCursorAddress(row * cols); + } + screen.updateDisplaySnapshot(); + } + + public void eraseField() { + if (screen == null) return; + if (!screen.isFormatted()) { + eraseEof(); + return; + } + int cur = screen.getCursorAddress(); + int faPos = screen.findFieldAttribute(cur); + if (faPos < 0) return; + byte faVal = screen.getCell(faPos).fa; + if (faIsProtected(faVal & 0xFF)) return; + + int fieldStart = screen.incrementAddress(faPos); + int pos = fieldStart; + int size = screen.getRows() * screen.getCols(); + int count = 0; + while (!screen.getCell(pos).isFieldAttribute() && count < size) { + ExtendedAttribute ea = screen.getCell(pos); + ea.ec = 0; + ea.ucs4 = 0; + pos = screen.incrementAddress(pos); + count++; + } + screen.getCell(faPos).fa = (byte) (screen.getCell(faPos).fa | FA_MODIFY); + screen.setCursorAddress(fieldStart); + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + } + + public boolean selectField() { + if (screen == null || !screen.isFormatted()) return false; + int cur = screen.getCursorAddress(); + int size = screen.getRows() * screen.getCols(); + int faPos = screen.findFieldAttribute(cur); + if (faPos < 0) return false; + ExtendedAttribute faCell = screen.getCell(faPos); + int fa = faCell.fa & 0xFF; + if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) return false; + + int desPos = (faPos + 1) % size; + ExtendedAttribute desCell = screen.getCell(desPos); + int ebcdic = desCell.ec & 0xFF; + char ascii = (char) desCell.ucs4; + if (ebcdic == 0x6F || ascii == '?' || ascii == '\uFF1F') { + desCell.ec = (byte) 0x6E; + desCell.ucs4 = '>'; + faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY; + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + return true; + } + return false; + } + + public boolean deselectField() { + if (screen == null || !screen.isFormatted()) return false; + int cur = screen.getCursorAddress(); + int size = screen.getRows() * screen.getCols(); + int faPos = screen.findFieldAttribute(cur); + if (faPos < 0) return false; + ExtendedAttribute faCell = screen.getCell(faPos); + int fa = faCell.fa & 0xFF; + if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) return false; + + int desPos = (faPos + 1) % size; + ExtendedAttribute desCell = screen.getCell(desPos); + int ebcdic = desCell.ec & 0xFF; + char ascii = (char) desCell.ucs4; + if (ebcdic == 0x6E || ascii == '>' || ascii == '\uFF1E') { + desCell.ec = (byte) 0x6F; + desCell.ucs4 = '?'; + faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY; + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + return true; + } + return false; + } // ========== File Transfer Support ========== @@ -1852,34 +1976,46 @@ public class InputProcessor { /** * Parse and execute IBM ECL standard mnemonic keystrokes (e.g. "USER[tab]PASS[enter]"). + * Fully compatible with IBM Host On-Demand SendKeyTokenizer and 120+ standard mnemonics. */ public void sendKeys(String keys) { if (keys == null || keys.isEmpty()) return; - int len = keys.length(); - int i = 0; - while (i < len) { - char c = keys.charAt(i); - if (c == '[') { - int end = keys.indexOf(']', i); - if (end > i) { - String token = keys.substring(i + 1, end).trim().toLowerCase(); + try { + haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer tokenizer = + new haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer(keys); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (token == null) break; + + if (token.length() == 1) { + typeCharacter(token.charAt(0)); + } else if (token.startsWith("[") && token.endsWith("]")) { executeMnemonicToken(token); - i = end + 1; - continue; + } else { + for (int i = 0; i < token.length(); i++) { + typeCharacter(token.charAt(i)); + } } } - typeCharacter(c); - i++; + } catch (haus.nightmare.lib3270j.ecl.ECLErr e) { + log.warning("Syntax error in sendKeys: " + e.getMessage()); } } - private void executeMnemonicToken(String token) { + public void executeMnemonicToken(String rawToken) { + if (rawToken == null || rawToken.isEmpty()) return; + String token = rawToken.trim().toLowerCase(); + if (token.startsWith("[") && token.endsWith("]") && token.length() >= 2) { + token = token.substring(1, token.length() - 1).trim(); + } + if (isNvtMode()) { try { switch (token) { case "enter": case "return": + case "keypadenter": fsm.sendNVTString("\r\n"); break; case "clear": @@ -1900,6 +2036,8 @@ public class InputProcessor { fsm.sendNVTString("\u001B[H"); break; case "end": + case "eof": + case "fieldend": fsm.sendNVTString("\u001B[F"); break; case "up": @@ -1922,6 +2060,7 @@ public class InputProcessor { case "pgup": fsm.sendNVTString("\u001B[5~"); break; + case "pagedn": case "pagedown": case "pgdn": fsm.sendNVTString("\u001B[6~"); @@ -1936,6 +2075,7 @@ public class InputProcessor { break; case "attn": case "break": + case "telnetbreak": fsm.sendNVTChar('\u0003'); break; case "sysreq": @@ -1946,10 +2086,36 @@ public class InputProcessor { case "reset": setKeyboardLocked(false); break; + case "enterreset": + if (keyboardLocked) { + setKeyboardLocked(false); + } else { + fsm.sendNVTString("\r\n"); + } + break; + case "keypad0": fsm.sendNVTChar('0'); break; + case "keypad1": fsm.sendNVTChar('1'); break; + case "keypad2": fsm.sendNVTChar('2'); break; + case "keypad3": fsm.sendNVTChar('3'); break; + case "keypad4": fsm.sendNVTChar('4'); break; + case "keypad5": fsm.sendNVTChar('5'); break; + case "keypad6": fsm.sendNVTChar('6'); break; + case "keypad7": fsm.sendNVTChar('7'); break; + case "keypad8": fsm.sendNVTChar('8'); break; + case "keypad9": fsm.sendNVTChar('9'); break; + case "keypad.": fsm.sendNVTChar('.'); break; + case "keypad,": fsm.sendNVTChar(','); break; + case "keypad-": fsm.sendNVTChar('-'); break; default: - if (token.startsWith("pf") || token.startsWith("f")) { + if (token.length() == 1 && Character.isDigit(token.charAt(0))) { + fsm.sendNVTChar(token.charAt(0)); + } else if (token.startsWith("pf") || token.startsWith("f") || token.startsWith("vt-pf") || token.startsWith("spf")) { try { - String numStr = token.startsWith("pf") ? token.substring(2) : token.substring(1); + String numStr = token; + if (numStr.startsWith("vt-pf")) numStr = numStr.substring(5); + else if (numStr.startsWith("spf")) numStr = numStr.substring(3); + else if (numStr.startsWith("pf")) numStr = numStr.substring(2); + else if (numStr.startsWith("f")) numStr = numStr.substring(1); int fn = Integer.parseInt(numStr); sendNvtFunctionKey(fn); } catch (NumberFormatException ignored) {} @@ -2000,11 +2166,36 @@ public class InputProcessor { case "curright": cursorRight(); break; + case "pagedn": + case "pagedown": + case "pgdn": + sendAid(AID_PF8); + break; + case "pageup": + case "pgup": + sendAid(AID_PF7); + break; + case "eof": + case "fieldend": + case "end": + processFieldEnd(); + break; + case "bof": + case "fieldstart": + processFieldStart(); + break; case "eraseeof": case "erase_eof": eraseEof(); break; + case "erasefld": + case "erasefield": + case "erase_field": + eraseField(); + break; + case "erinp": case "eraseinp": + case "eraseinput": case "erase_input": eraseInput(); break; @@ -2015,7 +2206,24 @@ public class InputProcessor { case "fieldmark": fieldMark(); break; + case "fldext": + case "fieldexit": + case "field+": + case "field-": + tab(); + break; + case "+cr": + if (graphicsPlane != null) { + if (graphicsPlane.isGraphicCursorAttached()) { + graphicsPlane.detachGraphicCursor(); + } else { + graphicsPlane.attachGraphicCursor(graphicsPlane.getGraphicCursorX(), graphicsPlane.getGraphicCursorY()); + } + } + break; case "attn": + case "break": + case "telnetbreak": attn(); break; case "sysreq": @@ -2024,10 +2232,18 @@ public class InputProcessor { case "reset": reset(); break; + case "enterreset": + if (keyboardLocked) { + reset(); + } else { + sendAid(AID_ENTER); + } + break; case "insert": setInsertMode(!isInsertMode()); break; case "delete": + case "del": deleteChar(); break; case "backspace": @@ -2035,9 +2251,13 @@ public class InputProcessor { backspace(); break; case "wordtab": + case "tabword": + case "nextword": processWordTab(true); break; case "wordbacktab": + case "backtabword": + case "previousword": processWordTab(false); break; case "wordleft": @@ -2046,10 +2266,6 @@ public class InputProcessor { case "wordright": processWordRight(); break; - case "fieldend": - case "end": - processFieldEnd(); - break; case "deleteword": processDeleteWord(); break; @@ -2058,6 +2274,12 @@ public class InputProcessor { case "select": cursorSelect(); break; + case "selfld": + selectField(); + break; + case "deselfld": + deselectField(); + break; case "lightpen": processLightPen(); break; @@ -2077,8 +2299,37 @@ public class InputProcessor { case "aplmode": toggleAplKeyboardMode(); break; + case "altview": + if (screen != null) { + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + } + break; + case "dspsosi": + if (screen != null) { + screen.balanceSOSI(); + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + } + break; + case "keypad0": typeCharacter('0'); break; + case "keypad1": typeCharacter('1'); break; + case "keypad2": typeCharacter('2'); break; + case "keypad3": typeCharacter('3'); break; + case "keypad4": typeCharacter('4'); break; + case "keypad5": typeCharacter('5'); break; + case "keypad6": typeCharacter('6'); break; + case "keypad7": typeCharacter('7'); break; + case "keypad8": typeCharacter('8'); break; + case "keypad9": typeCharacter('9'); break; + case "keypad.": typeCharacter('.'); break; + case "keypad,": typeCharacter(','); break; + case "keypad-": typeCharacter('-'); break; + case "keypadenter": sendAid(AID_ENTER); break; default: - if (token.startsWith("pf")) { + if (token.length() == 1 && Character.isDigit(token.charAt(0))) { + typeCharacter(token.charAt(0)); + } else if (token.startsWith("pf")) { try { int pfNum = Integer.parseInt(token.substring(2)); if (pfNum >= 1 && pfNum <= 24) { @@ -2092,6 +2343,24 @@ public class InputProcessor { sendAid(AID_PA1 + (paNum - 1)); } } catch (NumberFormatException ignored) {} + } else if (token.startsWith("spf")) { + try { + int spfNum = Integer.parseInt(token.substring(3)); + if (spfNum >= 1 && spfNum <= 24) { + sendAid(AID_PF1 + (spfNum - 1)); + } + } catch (NumberFormatException ignored) {} + } else if (token.startsWith("vt-pf")) { + try { + int vpfNum = Integer.parseInt(token.substring(5)); + if (vpfNum >= 1 && vpfNum <= 24) { + sendAid(AID_PF1 + (vpfNum - 1)); + } + } catch (NumberFormatException ignored) {} + } else { + // Recognized HoD Bidi / Layer / Print / GUI event tokens: + // log or no-op safely without throwing unknown errors + log.fine("Mnemonic token received: " + token); } break; } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/DS3270P.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/DS3270P.java index cbcb145..e705c08 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/DS3270P.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/DS3270P.java @@ -268,7 +268,7 @@ public class DS3270P { if (activeLuType == PrinterConstants.LU_TYPE_1_SCS && scs != null) { scs.flushLineBuffer(); } else if (printPs != null) { - printPs.flushPrintBuffer(); + printPs.endOfRecord(); } if (pd != null) { pd.flush(); 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 e267402..34e9336 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PD3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PD3270.java @@ -271,10 +271,14 @@ public class PD3270 { writeControlCode(enable ? PrinterDefinitionTable.CMD_START_SUPERSCRIPT : PrinterDefinitionTable.CMD_END_SUPERSCRIPT); } + private int activeCpi = PrinterConstants.DEFAULT_CPI; + private int activeLpi = PrinterConstants.DEFAULT_LPI; + /** * Set Characters Per Inch (CPI) via PDT. */ public synchronized void setCPI(int cpi) { + this.activeCpi = cpi; switch (cpi) { case 10: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_10); break; case 12: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_12); break; @@ -284,10 +288,15 @@ public class PD3270 { } } + public int getCPI() { + return activeCpi; + } + /** * Set Lines Per Inch (LPI) via PDT. */ public synchronized void setLPI(int lpi) { + this.activeLpi = lpi; switch (lpi) { case 6: writeControlCode(PrinterDefinitionTable.CMD_SET_LPI_6); break; case 8: writeControlCode(PrinterDefinitionTable.CMD_SET_LPI_8); break; @@ -295,6 +304,10 @@ public class PD3270 { } } + public int getLPI() { + return activeLpi; + } + /** * Process GDDM host print escape sequence or raw host passthrough. */ 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 3a849d1..0d372d0 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java @@ -308,12 +308,12 @@ public class PrintPS3270 { processWrite(data, offset, length, true); } - private void setCellFA(int addr, byte fa) { + protected void setCellFA(int addr, byte fa) { attrPlane[addr] = fa; textPlane[addr] = 0x00; // Field attributes display as blanks/nulls } - private int decodeBufferAddress(int b1, int b2) { + protected int decodeBufferAddress(int b1, int b2) { // Fast 3270 12-bit / 14-bit address calculation if ((b1 & 0xC0) == 0) { return ((b1 & 0x3F) << 8) | b2; @@ -321,6 +321,19 @@ public class PrintPS3270 { return ((b1 & 0x3F) << 6) | (b2 & 0x3F); } + public boolean isNonDisplayCell(int addr) { + if (addr < 0 || addr >= bufferSize) return false; + if (attrPlane[addr] != 0) return true; + for (int i = 0; i < bufferSize; i++) { + int checkAddr = (addr - i + bufferSize) % bufferSize; + byte fa = attrPlane[checkAddr]; + if (fa != 0) { + return (fa & 0x0C) == 0x0C; + } + } + return false; + } + /** * Format and print a specific row from the presentation buffer. */ @@ -331,11 +344,12 @@ public class PrintPS3270 { char[] chars = new char[lineLen]; for (int c = 0; c < lineLen; c++) { - int ebc = textPlane[startAddr + c] & 0xFF; - if (ebc == 0x00) { + int addr = startAddr + c; + if (isNonDisplayCell(addr)) { chars[c] = ' '; } else { - chars[c] = translator.ebcdicToUnicode(ebc); + int ebc = textPlane[addr] & 0xFF; + chars[c] = (ebc == 0x00) ? ' ' : translator.ebcdicToUnicode(ebc); } } @@ -375,6 +389,25 @@ public class PrintPS3270 { } } + /** + * Flush and dump formatted page to printer destination. + */ + public synchronized void flushPage() { + flushPrintBuffer(); + } + + /** + * Handle End of Record (EOR) in LU Type 3 data stream. + * Assembles and dumps formatted text buffer to configured output file or printer spool. + */ + public synchronized void endOfRecord() { + cancelAutoFlush(); + flushPage(); + if (pd != null) { + pd.flush(); + } + } + /** * Flush current printer buffer to PD3270 destination. */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270DB.java index 6e1cfe3..935e7c1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270DB.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270DB.java @@ -104,6 +104,198 @@ public class PrintPS3270DB extends PrintPS3270 { // ========== Output Formatting with DBCS & Grid Lines ========== + @Override + public synchronized void processWrite(byte[] data, int offset, int length, boolean eraseFirst) { + if (eraseFirst) { + erasePrintBuffer(); + } + + int idx = offset + 1; // Skip command byte + int end = offset + length; + + if (idx >= end) return; + + // WCC + int wcc = data[idx++] & 0xFF; + boolean startPrint = isStartPrint(wcc); + this.currentPrintFormat = getPrintFormat(wcc); + + byte currentColor = 0; + byte currentHilite = 0; + boolean inDbcs = false; + + while (idx < end) { + int b = data[idx++] & 0xFF; + + if (inDbcs) { + if (b == PrinterConstants.SCS_SI) { + inDbcs = false; + continue; + } + if (idx < end) { + int b2 = data[idx++] & 0xFF; + if (b2 == PrinterConstants.SCS_SI) { + inDbcs = false; + continue; + } + char unicodeChar; + if (b == 0x40 && b2 == 0x40) { + unicodeChar = '\u3000'; + } else { + unicodeChar = translator.dbcsToUnicode(b, b2); + if (unicodeChar == 0 || unicodeChar == 0xFFFF) { + unicodeChar = '?'; + } + } + setDBCSCharAtAddress(bufferAddress, unicodeChar); + colorPlane[bufferAddress] = currentColor; + hilitePlane[bufferAddress] = currentHilite; + bufferAddress = (bufferAddress + 2) % bufferSize; + continue; + } + } + + if (b == PrinterConstants.SCS_SO) { + inDbcs = true; + continue; + } + + switch (b) { + case PrinterConstants.ORDER_SBA: + if (idx + 1 < end) { + int b1 = data[idx++] & 0xFF; + int b2 = data[idx++] & 0xFF; + bufferAddress = decodeBufferAddress(b1, b2) % bufferSize; + } + break; + + case PrinterConstants.ORDER_SF: + if (idx < end) { + byte fa = data[idx++]; + setCellFA(bufferAddress, fa); + bufferAddress = (bufferAddress + 1) % bufferSize; + } + break; + + case PrinterConstants.ORDER_SFE: + if (idx < end) { + int pairCount = data[idx++] & 0xFF; + byte fa = 0; + for (int p = 0; p < pairCount && idx + 1 < end; p++) { + int type = data[idx++] & 0xFF; + int val = data[idx++] & 0xFF; + if (type == 0x00 || type == 0xC0) { + fa = (byte) val; + } else if (type == PrinterConstants.SA_COLOR) { + currentColor = (byte) val; + } else if (type == PrinterConstants.SA_HILITE) { + currentHilite = (byte) val; + } + } + setCellFA(bufferAddress, fa); + colorPlane[bufferAddress] = currentColor; + hilitePlane[bufferAddress] = currentHilite; + bufferAddress = (bufferAddress + 1) % bufferSize; + } + break; + + case PrinterConstants.ORDER_SA: + if (idx + 1 < end) { + int type = data[idx++] & 0xFF; + int val = data[idx++] & 0xFF; + if (type == PrinterConstants.SA_COLOR) { + currentColor = (byte) val; + } else if (type == PrinterConstants.SA_HILITE) { + currentHilite = (byte) val; + } else if (type == PrinterConstants.SA_RESET) { + currentColor = 0; + currentHilite = 0; + } + } + break; + + case PrinterConstants.ORDER_RA: + if (idx + 2 < end) { + int b1 = data[idx++] & 0xFF; + int b2 = data[idx++] & 0xFF; + int stopAddr = decodeBufferAddress(b1, b2) % bufferSize; + byte fillChar = data[idx++]; + if (stopAddr == bufferAddress) { + Arrays.fill(textPlane, fillChar); + } else { + while (bufferAddress != stopAddr) { + textPlane[bufferAddress] = fillChar; + colorPlane[bufferAddress] = currentColor; + hilitePlane[bufferAddress] = currentHilite; + bufferAddress = (bufferAddress + 1) % bufferSize; + } + } + } + break; + + case PrinterConstants.ORDER_EUA: + if (idx + 1 < end) { + int b1 = data[idx++] & 0xFF; + int b2 = data[idx++] & 0xFF; + int stopAddr = decodeBufferAddress(b1, b2) % bufferSize; + while (bufferAddress != stopAddr) { + textPlane[bufferAddress] = 0x00; + dbcsPlane[bufferAddress] = DBCS_NONE; + dbcsCharPlane[bufferAddress] = ' '; + bufferAddress = (bufferAddress + 1) % bufferSize; + } + } + break; + + case PrinterConstants.ORDER_IC: + case PrinterConstants.ORDER_PT: + break; + + case PrinterConstants.ORDER_GE: + if (idx < end) { + byte geByte = data[idx++]; + textPlane[bufferAddress] = geByte; + colorPlane[bufferAddress] = currentColor; + hilitePlane[bufferAddress] = currentHilite; + bufferAddress = (bufferAddress + 1) % bufferSize; + } + break; + + case PrinterConstants.ORDER_NL: + int curRow = bufferAddress / cols; + bufferAddress = ((curRow + 1) * cols) % bufferSize; + break; + + case PrinterConstants.ORDER_EM: + flushPage(); + break; + + case PrinterConstants.ORDER_FF: + pd.formFeed(); + break; + + case PrinterConstants.ORDER_CR: + bufferAddress = (bufferAddress / cols) * cols; + break; + + default: + textPlane[bufferAddress] = (byte) b; + dbcsPlane[bufferAddress] = DBCS_NONE; + colorPlane[bufferAddress] = currentColor; + hilitePlane[bufferAddress] = currentHilite; + bufferAddress = (bufferAddress + 1) % bufferSize; + break; + } + } + + if (startPrint) { + cancelAutoFlush(); + flushPage(); + } else { + scheduleAutoFlush(); + } + } + @Override public synchronized void printLine(int row, int lineLen) { if (row < 0 || row >= rows) return; @@ -132,7 +324,13 @@ public class PrintPS3270DB extends PrintPS3270 { lineBuilder.append('│'); } - if (dbcsPlane[addr] == DBCS_LEFT) { + if (isNonDisplayCell(addr)) { + if (dbcsPlane[addr] == DBCS_LEFT) { + lineBuilder.append(" "); + } else if (dbcsPlane[addr] == DBCS_NONE) { + lineBuilder.append(' '); + } + } else if (dbcsPlane[addr] == DBCS_LEFT) { char ch = dbcsCharPlane[addr]; lineBuilder.append(ch != 0 ? ch : ' '); } else if (dbcsPlane[addr] == DBCS_RIGHT) { 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 d494764..6980c68 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java @@ -137,13 +137,28 @@ public class PrintSCS3270 { switch (subOrder) { case PrinterConstants.SCS_SHF: + case PrinterConstants.SCS_SHF_ALT: processSetHorizontalFormat(data, idx, sliceLen); break; case PrinterConstants.SCS_SVF: processSetVerticalFormat(data, idx, sliceLen); break; case PrinterConstants.SCS_SLD: - processSetLineDensity(data, idx, sliceLen); + case PrinterConstants.SCS_SLD_ALT: + // 0xC6 can be SLD or legacy PPA + if (subOrder == PrinterConstants.SCS_SLD && paramLen == 2 && idx + 3 < end && + (data[idx + 3] == PrinterConstants.POS_ABSOLUTE || data[idx + 3] == PrinterConstants.POS_RELATIVE)) { + processPresentationPositionAdvancing(data, idx, sliceLen); + } else { + processSetLineDensity(data, idx, sliceLen); + } + break; + case PrinterConstants.SCS_SPD: // 0xD2: Set Print Density or legacy SVF_ALT + if (paramLen >= 5 && idx + 3 < end && (data[idx + 3] & 0xFF) != PrinterConstants.SPD_TYPE_ID) { + processSetVerticalFormat(data, idx, sliceLen); + } else { + processSetPrintDensity(data, idx, sliceLen); + } break; case PrinterConstants.SCS_STO: processSetTextOrientation(data, idx, sliceLen); @@ -154,9 +169,6 @@ public class PrintSCS3270 { case PrinterConstants.SCS_SEAC: processSetEnhancedAttribute(data, idx, sliceLen); break; - case PrinterConstants.SCS_PPA: - processPresentationPositionAdvancing(data, idx, sliceLen); - break; case PrinterConstants.SCS_PPV: processPresentationPositionVertical(data, idx, sliceLen); break; @@ -270,6 +282,16 @@ public class PrintSCS3270 { processIRS(); idx++; break; + case PrinterConstants.SCS_PP: + if (idx + 2 < end) { + int ppFunc = data[idx + 1] & 0xFF; + int ppVal = data[idx + 2] & 0xFF; + processPPOrder(ppFunc, ppVal); + idx += 3; + } else { + idx = end; + } + break; case PrinterConstants.SCS_SUB: processSUB(); idx++; @@ -529,6 +551,26 @@ public class PrintSCS3270 { processSetLineDensity(data, offset, len); } + public synchronized void processSPD(byte[] data, int offset, int len) { + processSetPrintDensity(data, offset, len); + } + + public synchronized void processPPOrder(int func, int val) { + if (func == PrinterConstants.PP_HORIZ_ABS || func == PrinterConstants.POS_ABSOLUTE) { + currentCol = Math.min(mpp, Math.max(leftMargin, val)); + } else if (func == PrinterConstants.PP_HORIZ_REL || func == PrinterConstants.POS_RELATIVE) { + currentCol = Math.min(mpp, currentCol + val); + } else if (func == PrinterConstants.PP_VERT_ABS) { + while (currentRow < val && currentRow < bottomMargin) { + processLF(); + } + } else if (func == PrinterConstants.PP_VERT_REL) { + for (int i = 0; i < val && currentRow < bottomMargin; i++) { + processLF(); + } + } + } + public synchronized void processSTO(byte[] data, int offset, int len) { processSetTextOrientation(data, offset, len); } @@ -684,17 +726,27 @@ public class PrintSCS3270 { } public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) { - if (len < 4) return; - int mppVal = data[offset + 3] & 0xFF; - if (mppVal > 0) { - this.mpp = mppVal; - this.rightMargin = Math.min(this.rightMargin, this.mpp); + if (len < 3) return; + if (len >= 4) { + int mppVal = data[offset + 3] & 0xFF; + if (mppVal > 0) { + this.mpp = mppVal; + if (this.rightMargin > this.mpp) { + this.rightMargin = this.mpp; + } + } + } + if (len >= 5) { + int lm = data[offset + 4] & 0xFF; + if (lm > 0) { + this.leftMargin = lm; + } } if (len >= 6) { - int lm = data[offset + 4] & 0xFF; int rm = data[offset + 5] & 0xFF; - if (lm > 0) this.leftMargin = lm; - if (rm > 0 && rm >= lm) this.rightMargin = rm; + if (rm > 0 && rm >= this.leftMargin) { + this.rightMargin = Math.min(rm, this.mpp); + } } if (len > 6) { int tabCount = len - 6; @@ -705,20 +757,33 @@ public class PrintSCS3270 { Arrays.sort(tabs); this.horizontalTabs = tabs; } + if (currentCol < leftMargin) { + currentCol = leftMargin; + } } public synchronized void processSetVerticalFormat(byte[] data, int offset, int len) { - if (len < 4) return; - int mplVal = data[offset + 3] & 0xFF; - if (mplVal > 0) { - this.mpl = mplVal; - this.bottomMargin = Math.min(this.bottomMargin, this.mpl); + if (len < 3) return; + if (len >= 4) { + int mplVal = data[offset + 3] & 0xFF; + if (mplVal > 0) { + this.mpl = mplVal; + if (this.bottomMargin > this.mpl) { + this.bottomMargin = this.mpl; + } + } + } + if (len >= 5) { + int tm = data[offset + 4] & 0xFF; + if (tm > 0) { + this.topMargin = tm; + } } if (len >= 6) { - int tm = data[offset + 4] & 0xFF; int bm = data[offset + 5] & 0xFF; - if (tm > 0) this.topMargin = tm; - if (bm > 0 && bm >= tm) this.bottomMargin = bm; + if (bm > 0 && bm >= this.topMargin) { + this.bottomMargin = Math.min(bm, this.mpl); + } } if (len > 6) { int tabCount = len - 6; @@ -729,17 +794,81 @@ public class PrintSCS3270 { Arrays.sort(tabs); this.verticalTabs = tabs; } + if (currentRow < topMargin) { + currentRow = topMargin; + } } public synchronized void processSetLineDensity(byte[] data, int offset, int len) { - if (len < 4) return; - int points = data[offset + 3] & 0xFF; - if (points > 0) { - // Line density in points / inch (72 points = 1 inch) - // 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI - int computedLpi = Math.max(1, 72 / points); - setPrintDensity(this.cpi, computedLpi); + if (len < 3) return; + int count = data[offset + 2] & 0xFF; + int points = 12; // default 12 points = 6 LPI + if (count == 1 || len == 3) { + points = 12; + } else if (len >= 4) { + points = data[offset + 3] & 0xFF; } + + int computedLpi = 6; + if (points == 6 || points == 12) { + computedLpi = 6; + } else if (points == 8 || points == 9) { + computedLpi = 8; + } else if (points == 4 || points == 18) { + computedLpi = 4; + } else if (points == 3 || points == 24) { + computedLpi = 3; + } else if (points > 0) { + computedLpi = Math.max(1, 72 / points); + } + setPrintDensity(this.cpi, computedLpi); + } + + public synchronized void processSetPrintDensity(byte[] data, int offset, int len) { + if (len < 3) return; + int density = 10; + if (len >= 5 && (data[offset + 3] & 0xFF) == PrinterConstants.SPD_TYPE_ID) { + if (len >= 6) { + int val = ((data[offset + 4] & 0xFF) << 8) | (data[offset + 5] & 0xFF); + if (val > 0) { + density = val; + } else { + density = data[offset + 5] & 0xFF; + } + } else { + density = data[offset + 4] & 0xFF; + } + } else if (len >= 4) { + density = data[offset + 3] & 0xFF; + } + + int effCpi = 10; + boolean dw = false; + if (density <= 5 && density > 0) { + effCpi = 10; + dw = true; + } else if (density <= 6 && density > 0) { + effCpi = 12; + dw = true; + } else if (density <= 8 && density > 0) { + effCpi = 15; + dw = true; + } else if (density <= 10) { + effCpi = 10; + dw = false; + } else if (density <= 12) { + effCpi = 12; + dw = false; + } else if (density <= 15) { + effCpi = 15; + dw = false; + } else { + effCpi = 17; + dw = false; + } + + setDoubleWidth(dw); + setPrintDensity(effCpi, this.lpi); } public synchronized void processSetTextOrientation(byte[] data, int offset, int len) { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270DB.java index 0325536..eb88bb4 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270DB.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270DB.java @@ -90,6 +90,19 @@ public class PrintSCS3270DB extends PrintSCS3270 { this.dbcsCpi = cpi; } + @Override + public synchronized void setPrintDensity(int cpi, int lpi) { + super.setPrintDensity(cpi, lpi); + this.dbcsCpi = Math.max(1, this.cpi / 2); + } + + public synchronized void endOfRecord() { + if (dbcsMode) { + processSI(); + } + flushLineBuffer(); + } + // ========== Grid & Ruling Line Controls ========== public synchronized void setGridLines(boolean left, boolean right, boolean top, boolean bottom) { @@ -219,14 +232,23 @@ public class PrintSCS3270DB extends PrintSCS3270 { } } - ensureLineBufferCapacity(currentCol + 2); - - // Record grid line attributes for this cell - if (lineGridFlags != null && currentCol < lineGridFlags.length) { - lineGridFlags[currentCol] = (byte) activeGridFlags; + if (currentCol + 1 > rightMargin || currentCol + 1 > mpp) { + advanceToNextLine(); } - lineBuffer[currentCol] = unicodeChar; + ensureLineBufferCapacity(currentCol + 2); + + // Record grid line attributes for this 2-column span + if (lineGridFlags != null) { + if (currentCol - 1 < lineGridFlags.length) { + lineGridFlags[currentCol - 1] = (byte) activeGridFlags; + } + if (currentCol < lineGridFlags.length) { + lineGridFlags[currentCol] = (byte) activeGridFlags; + } + } + + lineBuffer[currentCol - 1] = unicodeChar; lineModified = true; // Advance column by 2 (DBCS glyph takes 2 character columns) 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 fc37d49..f94f322 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConstants.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrinterConstants.java @@ -42,6 +42,7 @@ public final class PrinterConstants { public static final int SCS_LF = 0x25; // Line Feed public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm public static final int SCS_IRS = 0x33; // Index Return + public static final int SCS_PP = 0x34; // Presentation Position (0x34 ) 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 @@ -53,16 +54,29 @@ public final class PrinterConstants { public static final int SCS_PREFIX_2B = 0x2B; // 2-byte SCS prefix 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 + // 0x2B Sub-orders (IBM SNA Character String standard & HoD v14 SCSConstants) + public static final int SCS_SHF = 0xC1; // Set Horizontal Format (0x2B 0xC1) + public static final int SCS_SHF_ALT = 0xD1; // Set Horizontal Format legacy alias + public static final int SCS_SVF = 0xC2; // Set Vertical Format (0x2B 0xC2) + public static final int SCS_SVF_ALT = 0xD2; // Set Vertical Format legacy alias + public static final int SCS_SLD = 0xC6; // Set Line Density (0x2B 0xC6) + public static final int SCS_SLD_ALT = 0xD6; // Set Line Density legacy alias + public static final int SCS_SPD = 0xD2; // Set Print Density (0x2B 0xD2) + public static final int SCS_PPV = 0xC4; // Presentation Position Vertical + public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal legacy) + public static final int SCS_GEA = 0xC8; // Set Graphic Error Action (0x2B 0xC8) + 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 + + // SPD Type identifiers + public static final int SPD_TYPE_ID = 0x29; // Standard SPD parameter identifier (41) + + // PP Sub-functions (Presentation Position) + public static final int PP_VERT_ABS = 0xC4; // Set Absolute Vertical Position + public static final int PP_VERT_REL = 0x4C; // Set Relative Vertical Position + public static final int PP_HORIZ_ABS = 0xC8; // Set Absolute Horizontal Position + public static final int PP_HORIZ_REL = 0x48; // Set Relative Horizontal Position // SA Attribute Types public static final int SA_RESET = 0x00; diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLOIATest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLOIATest.java index 65c25e3..ba21c83 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLOIATest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLOIATest.java @@ -56,4 +56,51 @@ public class ECLOIATest { input.setInsertMode(true); assertTrue(oia.isInsertMode()); } + + @Test + public void testXSystemAndXProtTransitionsAfterReset() { + input.setOIA(oia); + + // 1. Initial ready state + assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited()); + assertFalse(oia.isXSystem()); + assertFalse(oia.isXProt()); + + // 2. Clear / reset called explicitly (as on connect or packet receipt) + oia.setInputInhibited(INHIBIT_NOT_INHIBITED); + assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited()); + assertFalse(oia.isXSystem()); + + // 3. User sends AID / locks keyboard -> MUST trigger X SYSTEM + input.setKeyboardLocked(true); + assertTrue(oia.isXSystem(), "X SYSTEM must be triggered when keyboard is locked"); + assertEquals(INHIBIT_SYSTEM_LOCK, oia.getInputInhibited()); + assertEquals("X-SYSTEM", oia.getStatusString()); + + // 4. Host unlocks keyboard + input.setKeyboardLocked(false); + assertFalse(oia.isXSystem()); + assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited()); + assertEquals("READY", oia.getStatusString()); + + // 5. User types in protected field -> triggers X PROT + oia.setInputInhibited(INHIBIT_PROTECTED_FIELD); + assertTrue(oia.isXProt(), "X PROT must be triggered"); + assertFalse(oia.isXSystem()); + assertEquals(INHIBIT_PROTECTED_FIELD, oia.getInputInhibited()); + assertEquals("X-PROT", oia.getStatusString()); + + // 6. User presses Reset + input.reset(); + assertFalse(input.isKeyboardLocked()); + assertFalse(oia.isXProt()); + assertFalse(oia.isXSystem()); + assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited()); + + // 7. Subsequent AID send -> MUST trigger X SYSTEM again + input.setKeyboardLocked(true); + assertTrue(oia.isXSystem(), "X SYSTEM must trigger on subsequent lock after reset"); + assertEquals(INHIBIT_SYSTEM_LOCK, oia.getInputInhibited()); + assertEquals("X-SYSTEM", oia.getStatusString()); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase22HaclFacadesTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase22HaclFacadesTest.java new file mode 100644 index 0000000..9901f1b --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase22HaclFacadesTest.java @@ -0,0 +1,291 @@ +package haus.nightmare.lib3270j.ecl; + +import com.ibm.eNetwork.ECL.ECLConstants; +import com.ibm.eNetwork.ECL.ECLErr; +import com.ibm.eNetwork.ECL.ECLHostPrintSession; +import com.ibm.eNetwork.ECL.ECLSession; +import com.ibm.eNetwork.ECL.ECLPS; +import com.ibm.eNetwork.ECL.ECLOIA; +import com.ibm.eNetwork.ECL.ECLConnection; +import com.ibm.eNetwork.ECL.ECLField; +import com.ibm.eNetwork.ECL.ECLFieldList; +import com.ibm.eNetwork.ECL.ECLScreenDesc; +import com.ibm.eNetwork.ECL.ECLScreenReco; +import com.ibm.eNetwork.ECL.ECLXfer; +import com.ibm.eNetwork.ECL.event.ECLPSEvent; +import com.ibm.eNetwork.ECL.event.ECLPSListener; +import com.ibm.eNetwork.ECL.ECLOIANotify; + +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 static org.junit.jupiter.api.Assertions.*; + +/** + * Targeted Unit Tests for Phase 22: HACL com.ibm.eNetwork.ECL.* Drop-In API Facades. + */ +public class Phase22HaclFacadesTest { + + private ECLSession session; + private ECLPS ps; + private ECLOIA oia; + private ECLConnection conn; + + @BeforeEach + public void setUp() throws ECLErr { + Properties props = new Properties(); + props.setProperty(ECLSession.SESSION_HOST, "mainframe.test.local"); + props.setProperty(ECLSession.SESSION_PORT, "23"); + props.setProperty(ECLSession.SESSION_CODE_PAGE, "037"); + session = new ECLSession(props); + ps = session.GetPS(); + oia = session.GetOIA(); + conn = session.GetConnection(); + } + + @Test + public void testFacadeInstantiationAndNamespaceIntegrity() { + assertNotNull(session, "ECLSession should be created"); + assertNotNull(ps, "ECLPS facade should be non-null"); + assertNotNull(oia, "ECLOIA facade should be non-null"); + assertNotNull(conn, "ECLConnection facade should be non-null"); + assertNotNull(session.GetXfer(), "ECLXfer facade should be non-null"); + assertNotNull(session.GetScreenReco(), "ECLScreenReco facade should be non-null"); + + // Constants checking + assertEquals(1, ECLConstants.PLANE_TEXT); + assertEquals(2, ECLConstants.PLANE_COLOR); + assertEquals(4, ECLConstants.PLANE_EXTENDED); + assertEquals(1, ECLConstants.SEARCH_FORWARD); + assertEquals(2, ECLConstants.SEARCH_BACKWARD); + + // Connection metadata + assertEquals("mainframe.test.local", conn.GetHost()); + assertEquals(23, conn.GetPort()); + } + + @Test + public void test1BasedCoordinateConversion() throws ECLErr { + assertEquals(24, ps.GetRows()); + assertEquals(80, ps.GetCols()); + assertEquals(1920, ps.GetSize()); + + // 1-based pos to row/col + // pos 1 -> row 1, col 1 + assertEquals(1, ps.ConvertPosToRow(1)); + assertEquals(1, ps.ConvertPosToCol(1)); + assertEquals(1, ps.ConvertRowColToPos(1, 1)); + + // pos 80 -> row 1, col 80 + assertEquals(1, ps.ConvertPosToRow(80)); + assertEquals(80, ps.ConvertPosToCol(80)); + assertEquals(80, ps.ConvertRowColToPos(1, 80)); + + // pos 81 -> row 2, col 1 + assertEquals(2, ps.ConvertPosToRow(81)); + assertEquals(1, ps.ConvertPosToCol(81)); + assertEquals(81, ps.ConvertRowColToPos(2, 1)); + + // pos 1920 -> row 24, col 80 + assertEquals(24, ps.ConvertPosToRow(1920)); + assertEquals(80, ps.ConvertPosToCol(1920)); + assertEquals(1920, ps.ConvertRowColToPos(24, 80)); + + // ConvertPosToRowCol int[] array + int[] rowCol = new int[2]; + ps.ConvertPosToRowCol(81, rowCol); + assertEquals(2, rowCol[0]); + assertEquals(1, rowCol[1]); + + // Out of bounds pos checks + assertThrows(ECLErr.class, () -> ps.ConvertPosToRow(0)); + assertThrows(ECLErr.class, () -> ps.ConvertPosToRow(1921)); + assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(0, 1)); + assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(25, 1)); + assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(1, 0)); + assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(1, 81)); + } + + @Test + public void testCursorPositioning1Based() throws ECLErr { + // Initial cursor at pos 1 (row 1, col 1) + ps.SetCursorPos(1); + assertEquals(1, ps.GetCursorPos()); + assertEquals(1, ps.GetCursorRow()); + assertEquals(1, ps.GetCursorCol()); + + // Set by pos + ps.SetCursorPos(85); + assertEquals(85, ps.GetCursorPos()); + assertEquals(2, ps.GetCursorRow()); + assertEquals(5, ps.GetCursorCol()); + + // Set by row and col + ps.SetCursorPos(5, 10); + assertEquals(330, ps.GetCursorPos()); // (5-1)*80 + 10 = 330 + assertEquals(5, ps.GetCursorRow()); + assertEquals(10, ps.GetCursorCol()); + + // Invalid cursor pos throws ECLErr + assertThrows(ECLErr.class, () -> ps.SetCursorPos(0)); + assertThrows(ECLErr.class, () -> ps.SetCursorPos(1921)); + assertThrows(ECLErr.class, () -> ps.SetCursorPos(0, 10)); + assertThrows(ECLErr.class, () -> ps.SetCursorPos(25, 10)); + } + + @Test + public void testGetAndSetString1Based() throws ECLErr { + // Write text at 1-based pos 1 + ps.SetString("LOGON APPLID(CICS01)", 1); + assertEquals("LOGON APPLID(CICS01)", ps.GetString(1, 20)); + + // GetString with char buffer + char[] buf = new char[5]; + int read = ps.GetString(buf, 5, 1); + assertEquals(5, read); + assertEquals("LOGON", new String(buf)); + + // Write text by (row, col) + ps.SetString("PASSWORD", 2, 1); + assertEquals("PASSWORD", ps.GetString(81, 8)); + assertEquals("PASSWORD", ps.GetString(2, 1, 8)); + + // Bounds checks + assertThrows(ECLErr.class, () -> ps.GetString(0, 5)); + assertThrows(ECLErr.class, () -> ps.GetString(1921, 5)); + assertThrows(ECLErr.class, () -> ps.SetString("TEST", 0)); + assertThrows(ECLErr.class, () -> ps.SetString("TEST", 1921)); + } + + @Test + public void testSearchText1Based() throws ECLErr { + ps.SetString("ENTER USERID:", 1, 1); + ps.SetString("ENTER PASSWORD:", 2, 1); + + // Forward search from pos 1 + int found1 = ps.SearchText("USERID", ECLConstants.SEARCH_FORWARD, 1); + assertEquals(7, found1); // "ENTER " is 6 chars, USERID starts at 7 + + // Forward search for PASSWORD + int found2 = ps.SearchText("PASSWORD", ECLConstants.SEARCH_FORWARD, 1); + assertEquals(87, found2); // 80 + 7 = 87 + + // Backward search + int foundBack = ps.SearchText("USERID", ECLConstants.SEARCH_BACKWARD, 100); + assertEquals(7, foundBack); + + // Not found returns 0 + assertEquals(0, ps.SearchText("NOTPRESENT", ECLConstants.SEARCH_FORWARD, 1)); + } + + @Test + public void testSendKeysAndMnemonics() throws ECLErr { + // Verify send keys mnemonics array + String[] mnemonics = ps.GetSendKeyMnemonics(); + assertNotNull(mnemonics); + assertTrue(mnemonics.length > 20); + + boolean hasEnter = false; + boolean hasClear = false; + boolean hasTab = false; + for (String m : mnemonics) { + if ("[enter]".equals(m)) hasEnter = true; + if ("[clear]".equals(m)) hasClear = true; + if ("[tab]".equals(m)) hasTab = true; + } + assertTrue(hasEnter, "Must have [enter] mnemonic"); + assertTrue(hasClear, "Must have [clear] mnemonic"); + assertTrue(hasTab, "Must have [tab] mnemonic"); + + // SendKeys to presentation space + ps.SendKeys("ABC[enter]"); + // Verify buffer got ABC + assertEquals("ABC", ps.GetString(1, 3)); + } + + @Test + public void testPSEventListenerDispatching() throws Exception { + AtomicBoolean updatedCalled = new AtomicBoolean(false); + AtomicInteger eventTypeSeen = new AtomicInteger(-1); + + ECLPSListener listener = new ECLPSListener() { + @Override + public void PSNotifyEvent(ECLPSEvent event) { + updatedCalled.set(true); + eventTypeSeen.set(event.GetType()); + } + }; + + ps.RegisterPSEvent(listener); + + // Update presentation space + ps.SetString("TESTING NOTIFY", 1); + // Dispatch notifications through delegate + ps.getDelegate().notifyPSUpdate(1, 1, 1, 14, false); + + assertTrue(updatedCalled.get(), "PSNotifyEvent should be invoked"); + + // Unregister + ps.UnregisterPSEvent(listener); + updatedCalled.set(false); + ps.getDelegate().notifyPSUpdate(1, 1, 1, 14, false); + assertFalse(updatedCalled.get(), "PSNotifyEvent should not be called after Unregister"); + } + + @Test + public void testOIANotificationsAndStatus() throws Exception { + assertNotNull(oia); + assertTrue(oia.IsCommError(), "IsCommError should be true when not connected"); + assertFalse(oia.IsInsertMode()); + assertTrue(oia.IsKeyLocked(), "IsKeyLocked should be true when disconnected"); + + // Clear inhibit override + oia.getDelegate().setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED); + assertFalse(oia.IsKeyLocked(), "IsKeyLocked should be false when not inhibited"); + + AtomicBoolean oiaNotified = new AtomicBoolean(false); + ECLOIANotify notify = (o, reason, state, text) -> { + oiaNotified.set(true); + }; + + oia.RegisterOIAEvent(notify); + oia.getDelegate().notifyOIAChanged(); + + assertTrue(oiaNotified.get(), "OIA notification callback should be invoked"); + + oia.UnregisterOIAEvent(notify); + } + + @Test + public void testECLFieldAndFieldList1Based() throws ECLErr { + // Unformatted screen field list + ECLFieldList fl = ps.GetFieldList(); + assertNotNull(fl); + + // Screen Desc & Screen Reco + ECLScreenDesc desc = new ECLScreenDesc(); + desc.AddString("ENTER USERID:", 1, 1); + ps.SetString("ENTER USERID:", 1, 1); + assertTrue(desc.Matches(ps.getDelegate(), oia.getDelegate()), "Matches ScreenDesc"); + + ECLScreenReco reco = session.GetScreenReco(); + assertNotNull(reco); + assertTrue(reco.IsMatch(desc), "ScreenReco matches descriptor"); + } + + @Test + public void testHostPrintSessionInstantiation() throws ECLErr { + Properties printProps = new Properties(); + printProps.setProperty(ECLSession.SESSION_HOST, "printhost.test.local"); + printProps.setProperty(ECLSession.SESSION_PORT, "515"); + ECLHostPrintSession printSession = new ECLHostPrintSession(printProps); + + assertNotNull(printSession); + assertEquals("printhost.test.local", printSession.GetHost()); + assertEquals(515, printSession.GetPort()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase23ScreenRecoCompositeTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase23ScreenRecoCompositeTest.java new file mode 100644 index 0000000..cd72dd6 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase23ScreenRecoCompositeTest.java @@ -0,0 +1,324 @@ +package haus.nightmare.lib3270j.ecl; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.ecl.screenreco.*; +import haus.nightmare.lib3270j.screen.ScreenBuffer; + +/** + * Phase 23 test suite: Advanced Screen Recognition (ECLScreenDesc) Composite Engine & XML Schema. + */ +public class Phase23ScreenRecoCompositeTest { + + private ScreenBuffer screen; + private InputProcessor inputProcessor; + private EbcdicTranslator translator; + private ECLPS ps; + private ECLOIA oia; + + @BeforeEach + public void setUp() { + translator = new EbcdicTranslator("037"); + screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator); + inputProcessor = new InputProcessor(screen, translator, null); + ps = new ECLPS(screen, inputProcessor, translator); + oia = new ECLOIA(screen, inputProcessor, null); + } + + @Test + @DisplayName("23.1: ECLSDString absolute, rect, case-sense, wrap, and invert matching") + public void testECLSDStringMatching() { + ps.SetText("TSO/E LOGON", 1, 35); + ps.SetText("ENTER USERID", 5, 10); + + // 1. Absolute position matching at row 1, col 35 + ECLSDString sd1 = new ECLSDString("TSO/E LOGON", 1, 35, true); + assertTrue(sd1.matches(ps, oia)); + + // Case-insensitive match + ECLSDString sdCaseInsensitive = new ECLSDString("tso/e logon", 1, 35, false); + assertTrue(sdCaseInsensitive.matches(ps, oia)); + + // Case-sensitive mismatch + ECLSDString sdCaseSensitiveMismatch = new ECLSDString("tso/e logon", 1, 35, true); + assertFalse(sdCaseSensitiveMismatch.matches(ps, oia)); + + // Invert match on mismatch -> true + sdCaseSensitiveMismatch.setInvertMatch(true); + assertTrue(sdCaseSensitiveMismatch.matches(ps, oia)); + + // 2. Rectangular region matching + ECLSDString sdRect = new ECLSDString("USERID", 4, 1, 6, 80, false, false); + assertTrue(sdRect.matches(ps, oia)); + + ECLSDString sdRectMiss = new ECLSDString("UNKNOWN", 4, 1, 6, 80, false, false); + assertFalse(sdRectMiss.matches(ps, oia)); + + // 3. Search whole screen (row=0, col=0) + ECLSDString sdWholeScreen = new ECLSDString("LOGON"); + assertTrue(sdWholeScreen.matches(ps, oia)); + + // 4. Linear buffer pos + int pos = screen.rowColToAddress(0, 34) + 1; // 1-based pos + ECLSDString sdPos = new ECLSDString("TSO/E", pos, true); + assertTrue(sdPos.matches(ps, oia)); + } + + @Test + @DisplayName("23.1: ECLSDCursor location match (row, col) and pos") + public void testECLSDCursorMatching() { + ps.setCursorPos(7, 18); // 0-based row 7, col 18 = 1-based row 8, col 19 + + ECLSDCursor curMatch = new ECLSDCursor(8, 19); + assertTrue(curMatch.matches(ps, oia)); + + ECLSDCursor curMismatch = new ECLSDCursor(1, 1); + assertFalse(curMismatch.matches(ps, oia)); + + curMismatch.setInvertMatch(true); + assertTrue(curMismatch.matches(ps, oia)); + + // By linear position + int pos = screen.rowColToAddress(7, 18) + 1; + ECLSDCursor curPos = new ECLSDCursor(pos); + assertTrue(curPos.matches(ps, oia)); + } + + @Test + @DisplayName("23.1: ECLSDFields and ECLSDInputFields field count matching") + public void testECLSDFieldsAndInputFields() { + // Create formatted fields on screen + screen.setFieldAttribute(0, (byte) 0x20); // protected + screen.setFieldAttribute(80, (byte) 0x00); // unprotected input + screen.setFieldAttribute(160, (byte) 0x00); // unprotected input + ps.getFieldList().refresh(); + + assertEquals(3, ps.getFieldList().getFieldCount()); + + // Total field count + ECLSDFields fields3 = new ECLSDFields(3); + assertTrue(fields3.matches(ps, oia)); + + ECLSDFields fieldsWrong = new ECLSDFields(5); + assertFalse(fieldsWrong.matches(ps, oia)); + fieldsWrong.setInvertMatch(true); + assertTrue(fieldsWrong.matches(ps, oia)); + + // Unprotected input fields count = 2 + ECLSDInputFields inFields2 = new ECLSDInputFields(2); + assertTrue(inFields2.matches(ps, oia)); + + ECLSDInputFields inFieldsWrong = new ECLSDInputFields(3); + assertFalse(inFieldsWrong.matches(ps, oia)); + inFieldsWrong.setInvertMatch(true); + assertTrue(inFieldsWrong.matches(ps, oia)); + } + + @Test + @DisplayName("23.1: ECLSDOIA OIA status matching (NOTINHIBITED, DONTCARE, input=true/false)") + public void testECLSDOIAMatching() { + oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED); + + ECLSDOIA oiaNot = new ECLSDOIA(ECLSDOIA.OIA_NOTINHIBITED); + assertTrue(oiaNot.matches(ps, oia)); + + ECLSDOIA oiaDontCare = new ECLSDOIA(ECLSDOIA.OIA_DONTCARE); + assertTrue(oiaDontCare.matches(ps, oia)); + + ECLSDOIA oiaInputTrue = new ECLSDOIA("true", false); + assertTrue(oiaInputTrue.matches(ps, oia)); + + // Lock keyboard (system lock) + oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEM_LOCK); + assertFalse(oiaNot.matches(ps, oia)); + assertFalse(oiaInputTrue.matches(ps, oia)); + + // Invert match should now match because input is inhibited + oiaNot.setInvertMatch(true); + assertTrue(oiaNot.matches(ps, oia)); + + // Explicit inhibit code check + ECLSDOIA oiaSysLock = new ECLSDOIA(); + oiaSysLock.setExplicitInhibitStatus(ECLOIA.INHIBIT_SYSTEM_LOCK); + assertTrue(oiaSysLock.matches(ps, oia)); + } + + @Test + @DisplayName("23.1: ECLSDChecksum screen buffer hash matching") + public void testECLSDChecksumMatching() { + screen.setFieldAttribute(0, (byte) 0x20); + screen.setFieldAttribute(100, (byte) 0x00); + screen.setFieldAttribute(300, (byte) 0x20); + ps.getFieldList().refresh(); + + int checksum = ECLSDChecksum.CalculateChecksum(ps.getFieldList(), ps.getSize()); + assertTrue(checksum != 0); + + ECLSDChecksum sdChecksum = new ECLSDChecksum(checksum); + assertTrue(sdChecksum.matches(ps, oia)); + + ECLSDChecksum sdWrong = new ECLSDChecksum(checksum + 1); + assertFalse(sdWrong.matches(ps, oia)); + sdWrong.setInvertMatch(true); + assertTrue(sdWrong.matches(ps, oia)); + } + + @Test + @DisplayName("23.1: ECLSDBlock multi-line block text matching") + public void testECLSDBlockMatching() { + ps.SetText("IBM MAINFRAME", 10, 5); + ps.SetText("SYSTEM/390", 11, 5); + + String[] blockLines = new String[] { "IBM MAINFRAME", "SYSTEM/390" }; + + // Absolute block match + ECLSDBlock blkAbs = new ECLSDBlock(blockLines, 10, 5, true); + assertTrue(blkAbs.matches(ps, oia)); + + // Absolute block mismatch + ECLSDBlock blkAbsMiss = new ECLSDBlock(blockLines, 10, 6, true); + assertFalse(blkAbsMiss.matches(ps, oia)); + + // In-rect block match + ECLSDBlock blkRect = new ECLSDBlock(1, 1, 24, 80, true, false); + blkRect.setStrings(blockLines); + assertTrue(blkRect.matches(ps, oia)); + + blkRect.setInvertMatch(true); + assertFalse(blkRect.matches(ps, oia)); + } + + @Test + @DisplayName("23.1: ECLSDAttrib plane and attribute matching") + public void testECLSDAttribMatching() { + screen.setFieldAttribute(0, (byte) 0x20); // row 1, col 1 = protected (0x20) + ps.getFieldList().refresh(); + + ECLSDAttrib sdAttrib = new ECLSDAttrib((char) 0x20, 1, 1, ECLSDAttrib.FIELD_PLANE); + assertTrue(sdAttrib.matches(ps, oia)); + + ECLSDAttrib sdAttribWrong = new ECLSDAttrib((char) 0x00, 1, 1, ECLSDAttrib.FIELD_PLANE); + assertFalse(sdAttribWrong.matches(ps, oia)); + sdAttribWrong.setInvertMatch(true); + assertTrue(sdAttribWrong.matches(ps, oia)); + } + + @Test + @DisplayName("23.2: Composite ECLScreenDesc matching and XML round-trip ( schema)") + public void testCompositeAndXmlRoundtrip() { + // Set up host screen + ps.SetText("TSO/E LOGON", 1, 35); + ps.setCursorPos(7, 18); // row 8, col 19 + oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED); + screen.setFieldAttribute(0, (byte) 0x20); // 1 field + ps.getFieldList().refresh(); + + // 1. Programmatic Composite ECLScreenDesc + ECLScreenDesc desc = new ECLScreenDesc(); + desc.AddString("TSO/E LOGON", 1, 35, false); + desc.AddCursorPos(8, 19); + desc.AddOIAStatus(ECLOIA.INHIBIT_NOTINHIBITED); + desc.AddNumFields(1); + + assertEquals(4, desc.Size()); + assertTrue(desc.Matches(ps, oia)); + + // 2. Check XML serialization + String xml = desc.getXML(); + assertNotNull(xml); + assertTrue(xml.contains("")); + assertTrue(xml.contains("")); + + // 3. Test XML parsing matching exact HoD schema from Phase 23 prompt + String sampleXml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + ECLScreenDesc parsedDesc = new ECLScreenDesc(sampleXml); + assertEquals(4, parsedDesc.Size()); + assertTrue(parsedDesc.Matches(ps, oia)); + + // If screen text changes, match fails + ps.SetText("DIFFERENT SCR", 1, 35); + assertFalse(parsedDesc.Matches(ps, oia)); + } + + @Test + @DisplayName("23.1 & 23.2: ECLScreenReco dynamic registration triggers on PS and OIA updates") + public void testScreenRecoDynamicCallbacks() { + ECLScreenDesc desc = new ECLScreenDesc(); + desc.AddString("READY", 1, 1); + desc.AddOIAStatus(ECLOIA.INHIBIT_NOTINHIBITED); + + ECLScreenReco reco = new ECLScreenReco(ps, oia); + AtomicBoolean matched = new AtomicBoolean(false); + + reco.RegisterScreen(desc, event -> matched.set(true)); + + // Screen is not matching yet + assertFalse(matched.get()); + + // Screen text updated but OIA locked + ps.SetText("READY", 1, 1); + oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEM_LOCK); + ps.notifyKeyUnlocked(); + assertFalse(matched.get()); + + // Unlock OIA -> triggers reco callback + oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED); + assertTrue(matched.get()); + + reco.dispose(); + } + + @Test + @DisplayName("23.1: Drop-in compatibility with com.ibm.eNetwork.ECL.screenreco facade classes") + public void testHaclFacadesCompatibility() { + com.ibm.eNetwork.ECL.screenreco.ECLSDString s = new com.ibm.eNetwork.ECL.screenreco.ECLSDString("HELLO", 1, 1); + com.ibm.eNetwork.ECL.screenreco.ECLSDCursor c = new com.ibm.eNetwork.ECL.screenreco.ECLSDCursor(1, 1); + com.ibm.eNetwork.ECL.screenreco.ECLSDFields f = new com.ibm.eNetwork.ECL.screenreco.ECLSDFields(5); + com.ibm.eNetwork.ECL.screenreco.ECLSDInputFields inf = new com.ibm.eNetwork.ECL.screenreco.ECLSDInputFields(2); + com.ibm.eNetwork.ECL.screenreco.ECLSDOIA o = new com.ibm.eNetwork.ECL.screenreco.ECLSDOIA("NOTINHIBITED", false); + com.ibm.eNetwork.ECL.screenreco.ECLSDChecksum cs = new com.ibm.eNetwork.ECL.screenreco.ECLSDChecksum(100); + com.ibm.eNetwork.ECL.screenreco.ECLSDBlock blk = new com.ibm.eNetwork.ECL.screenreco.ECLSDBlock(1, 1, false, false); + com.ibm.eNetwork.ECL.screenreco.ECLSDBlockString bs = new com.ibm.eNetwork.ECL.screenreco.ECLSDBlockString("LINE"); + com.ibm.eNetwork.ECL.screenreco.ECLSDAttrib attr = new com.ibm.eNetwork.ECL.screenreco.ECLSDAttrib((char) 0x20, 1, 1, 16); + + assertTrue(s instanceof ECLScreenDescriptor); + assertTrue(c instanceof ECLScreenDescriptor); + assertTrue(f instanceof ECLScreenDescriptor); + assertTrue(inf instanceof ECLScreenDescriptor); + assertTrue(o instanceof ECLScreenDescriptor); + assertTrue(cs instanceof ECLScreenDescriptor); + assertTrue(blk instanceof ECLScreenDescriptor); + assertTrue(attr instanceof ECLScreenDescriptor); + + assertEquals("HELLO", s.GetString()); + assertEquals(1, c.GetRow()); + assertEquals(5, f.GetNum()); + assertEquals(2, inf.GetNum()); + assertEquals(ECLSDOIA.OIA_NOTINHIBITED, o.GetOIAType()); + assertEquals(100, cs.GetValue()); + assertEquals(1, blk.getSRow()); + assertEquals("LINE", bs.GetString()); + assertEquals(0x20, attr.getAttrib()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/SendKeyTokenizerPhase21Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/SendKeyTokenizerPhase21Test.java new file mode 100644 index 0000000..8088937 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/SendKeyTokenizerPhase21Test.java @@ -0,0 +1,271 @@ +package haus.nightmare.lib3270j.ecl; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.protocol.DS3270Constants; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.Telnet3270Client; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SendKeyTokenizerPhase21Test { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + private InputProcessor inputProcessor; + private ECLSession session; + private ECLPS ps; + + @BeforeEach + public void setUp() { + ConnectionConfig config = new ConnectionConfig("mainframe.example.com", 23, TerminalModel.IBM_3279_4); + Telnet3270Client client = new Telnet3270Client(config); + session = new ECLSession(client); + ps = session.GetPS(); + screen = client.getScreenBuffer(); + translator = client.getTranslator(); + inputProcessor = client.getInputProcessor(); + } + + @Test + public void testTokenizerBasicPlainAndBracketed() throws ECLErr { + SendKeyTokenizer tok = new SendKeyTokenizer("LOGON APPLID[enter]"); + assertTrue(tok.hasMoreTokens()); + assertEquals("LOGON APPLID", tok.nextToken()); + assertTrue(tok.hasMoreTokens()); + assertEquals("[enter]", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + assertNull(tok.nextToken()); + assertEquals("", tok.remainder()); + + tok.setSendKeyString("USER[tab]PASS[enter]"); + assertEquals("USER", tok.nextToken()); + assertEquals("[tab]", tok.nextToken()); + assertEquals("PASS", tok.nextToken()); + assertEquals("[enter]", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + } + + @Test + public void testTokenizerEscapedBrackets() throws ECLErr { + SendKeyTokenizer tok = new SendKeyTokenizer("[["); + assertEquals("[", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + + tok.setSendKeyString("]]"); + assertEquals("]", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + + tok.setSendKeyString("A[[B]]C"); + assertEquals("A", tok.nextToken()); + assertEquals("[", tok.nextToken()); + assertEquals("B", tok.nextToken()); + assertEquals("]", tok.nextToken()); + assertEquals("C", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + + // Escaped [[enter]] should yield "[", "enter", "]" rather than the AID key [enter] + tok.setSendKeyString("[[enter]]"); + assertEquals("[", tok.nextToken()); + assertEquals("enter", tok.nextToken()); + assertEquals("]", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + } + + @Test + public void testTokenizerExceptions() { + // Unterminated bracket + SendKeyTokenizer tok1 = new SendKeyTokenizer("[enter"); + ECLErr err1 = assertThrows(ECLErr.class, tok1::nextToken); + assertEquals("ECL0012", err1.getID()); + assertEquals(1, err1.getReasonCode()); + assertEquals(12, err1.getMsgNumber()); + + // Single opening bracket at EOF + SendKeyTokenizer tok2 = new SendKeyTokenizer("["); + ECLErr err2 = assertThrows(ECLErr.class, tok2::nextToken); + assertEquals("ECL0012", err2.getID()); + assertEquals(1, err2.getReasonCode()); + + // Single closing bracket at EOF + SendKeyTokenizer tok3 = new SendKeyTokenizer("]"); + ECLErr err3 = assertThrows(ECLErr.class, tok3::nextToken); + assertEquals("ECL0012", err3.getID()); + assertEquals(1, err3.getReasonCode()); + + // Unescaped closing bracket followed by characters + SendKeyTokenizer tok4 = new SendKeyTokenizer("]invalid"); + ECLErr err4 = assertThrows(ECLErr.class, tok4::nextToken); + assertEquals("ECL0012", err4.getID()); + assertEquals(1, err4.getReasonCode()); + } + + @Test + public void testTokenizerNoAidAndCharMode() throws ECLErr { + SendKeyTokenizer tok = new SendKeyTokenizer("A[enter]B"); + assertEquals("A", tok.nextTokenNoAid()); + assertEquals("[", tok.nextTokenNoAid()); + assertEquals("e", tok.nextTokenNoAid()); + + // Character mode (chunkPlainText = false) matching HoD 1:1 internal loop + tok.setSendKeyString("ABC[tab]"); + tok.setChunkPlainText(false); + assertEquals("A", tok.nextToken()); + assertEquals("B", tok.nextToken()); + assertEquals("C", tok.nextToken()); + assertEquals("[tab]", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + + // Single digit keypad shortcut [0]..[9] -> "0".."9" + tok.setSendKeyString("[5]"); + tok.setChunkPlainText(true); + assertEquals("5", tok.nextToken()); + } + + @Test + public void testECLErrConstructorsAndParity() { + ECLErr err = new ECLErr("SendKeyTokenizer:1", "ECL0012", 1); + assertEquals("SendKeyTokenizer:1", err.getLocation()); + assertEquals("SendKeyTokenizer:1", err.GetLocation()); + assertEquals("ECL0012", err.getID()); + assertEquals("ECL0012", err.GetID()); + assertEquals(1, err.getReasonCode()); + assertEquals(1, err.GetReasonCode()); + assertEquals(12, err.getMsgNumber()); + assertEquals(12, err.GetMsgNumber()); + assertNotNull(err.toString()); + + ECLErr err2 = new ECLErr("ECLPS", "ECL0010"); + assertEquals("ECLPS", err2.getLocation()); + assertEquals("ECL0010", err2.getID()); + + ECLErr err3 = new ECLErr("ECLPS", "ECL0010", "param", 2); + assertEquals(2, err3.getReasonCode()); + } + + @Test + public void testSendKeysEscapingInScreen() { + // Setup unprotected field at 0 + screen.setCellFA(0, (byte) 0x40); + screen.setCursorAddress(1); + + // Send keys with escaped brackets + inputProcessor.sendKeys("ABC[[XYZ]]123"); + + assertEquals("ABC[XYZ]123", ps.getString(1, 11)); + } + + @Test + public void test120MnemonicTokenTableInInputProcessor() { + screen.clear(); + // Setup two fields: field 1 at 0 (len 10), field 2 at 11 (len 10) + screen.setCellFA(0, (byte) 0x40); + screen.setCellFA(11, (byte) 0x40); + screen.setCursorAddress(1); + + // Test typing and tab + inputProcessor.sendKeys("HELLO[tab]WORLD"); + assertEquals("HELLO", ps.getString(1, 5)); + assertEquals("WORLD", ps.getString(12, 5)); + + // Test backtab: first backtab moves to beginning of current field (12) + inputProcessor.sendKeys("[backtab]"); + assertEquals(12, screen.getCursorAddress()); + + // Second backtab moves to beginning of previous field (1) + inputProcessor.sendKeys("[backtab]"); + assertEquals(1, screen.getCursorAddress()); + + // Test bof and eof + inputProcessor.sendKeys("[eof]"); + assertEquals(6, screen.getCursorAddress()); // end of "HELLO" + + inputProcessor.sendKeys("[bof]"); + assertEquals(1, screen.getCursorAddress()); // start of field + + // Test erasefld + inputProcessor.sendKeys("[erasefld]"); + assertEquals(1, screen.getCursorAddress()); + assertEquals(" ", ps.getString(1, 5)); + + // Test dup and fieldmark (dup enters DUP at 1 and tabs to next field at 12) + inputProcessor.sendKeys("[dup][fieldmark]"); + assertEquals(0x1C, screen.getCell(1).ec & 0xFF); + assertEquals(0x1E, screen.getCell(12).ec & 0xFF); + + // Test numeric keypad tokens + screen.setCursorAddress(1); + inputProcessor.sendKeys("[keypad1][keypad2][keypad.][keypad-]"); + assertEquals("12.-", ps.getString(1, 4)); + + // Test enterreset + inputProcessor.setKeyboardLocked(true); + inputProcessor.sendKeys("[enterreset]"); + assertFalse(inputProcessor.isKeyboardLocked()); + + // Test docmode, wordwrap, apl + boolean docBefore = screen.isEntryAssistDOCmode(); + inputProcessor.sendKeys("[docmode]"); + assertEquals(!docBefore, screen.isEntryAssistDOCmode()); + + boolean wrapBefore = screen.isEntryAssistWordWrap(); + inputProcessor.sendKeys("[wordwrap]"); + assertEquals(!wrapBefore, screen.isEntryAssistWordWrap()); + + // Test dspsosi + inputProcessor.sendKeys("[dspsosi]"); + + // Test altview + inputProcessor.sendKeys("[altview]"); + + // Test cursor selection and light pen + screen.setCellFA(30, (byte) (0x40 | DS3270Constants.FA_INT_NORM_SEL)); + screen.getCell(31).ec = (byte) 0x6F; // '?' + screen.getCell(31).ucs4 = '?'; + screen.setCursorAddress(31); + + inputProcessor.sendKeys("[selfld]"); + assertEquals('>', (char) screen.getCell(31).ucs4); + assertTrue((screen.getCell(30).fa & DS3270Constants.FA_MODIFY) != 0); + + inputProcessor.sendKeys("[deselfld]"); + assertEquals('?', (char) screen.getCell(31).ucs4); + assertEquals(0, screen.getCell(30).fa & DS3270Constants.FA_MODIFY); + + // Test safe handling of 30+ extended HoD tokens (bidi, print, gui, layers) + inputProcessor.sendKeys("[screenrev][latinlayer][bidilayer][push][endpush][autopush][fieldshape][prtestpage][copy][paste]"); + } + + @Test + public void testSendCharactersPacingAndPositioning() { + screen.clear(); + screen.setCellFA(0, (byte) 0x40); + screen.setCursorAddress(1); + + long start = System.currentTimeMillis(); + // Paced typing with 10ms delay over 4 tokens + ps.sendCharacters("TEST", 10); + long elapsed = System.currentTimeMillis() - start; + + assertEquals("TEST", ps.getString(1, 4)); + assertTrue(elapsed >= 25, "Elapsed time should reflect paced typing delay: " + elapsed + "ms"); + + // Test SendKeys / SendCharacters with 1-based pos + screen.clear(); + screen.setCellFA(9, (byte) 0x40); // FA at pos 10, data starts at pos 11 + ps.SendKeys("HOD", 11); + assertEquals("HOD", ps.getString(10, 3)); + + ps.SendCharacters("123", 11, 5); + assertEquals("123", ps.getString(10, 3)); + + // Test SendKeysNoAid + ps.SendKeysNoAid("XYZ", false); + assertEquals("XYZ", ps.getString(13, 3)); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/Phase20HostPrintingTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/Phase20HostPrintingTest.java new file mode 100644 index 0000000..cb031e5 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/Phase20HostPrintingTest.java @@ -0,0 +1,391 @@ +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.*; + +/** + * Targeted unit tests for Phase 20: IBM 3287/3286 Host Printing Protocol (SCS LU-1 & DSC/DSE LU-3). + * Validates 1:1 parity with IBM Host On-Demand v14 com.ibm.eNetwork.ECL.tn3270p.* + */ +public class Phase20HostPrintingTest { + + private PrinterConfig config; + private PD3270 pd; + private EbcdicTranslator translator; + + @BeforeEach + public void setUp() { + config = new PrinterConfig("localhost", 23); + pd = new PD3270(config); + translator = new EbcdicTranslator("037"); + pd.openPrinter(null); // Capture in memory + } + + // ========================================== + // 20.1 SCS Control Orders Execution (LU-1) + // ========================================== + + @Test + public void testSCSHorizontalFormatSHF() { + PrintSCS3270 scs = new PrintSCS3270(config, pd, translator); + + // Standard SNA SHF (0x2B 0xC1): count=6, MPP=132, LM=10, RM=120, tabs at 20, 30 + byte[] shf = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SHF, + 0x06, // count + (byte) 132, // MPP + 10, // Left Margin + 120, // Right Margin + 20, // Tab 1 + 30 // Tab 2 + }; + + scs.processHostData(shf, 0, shf.length); + assertEquals(132, scs.getLineLength()); + assertEquals(10, scs.getLeftMargin()); + assertEquals(120, scs.getRightMargin()); + assertEquals(10, scs.getCurrentColumn()); // Clamped to LM + + // Test Horizontal Tab (HT 0x05) + byte[] ht = new byte[]{ PrinterConstants.SCS_HT }; + scs.processHostData(ht, 0, ht.length); + assertEquals(20, scs.getCurrentColumn()); + + scs.processHostData(ht, 0, ht.length); + assertEquals(30, scs.getCurrentColumn()); + } + + @Test + public void testSCSVerticalFormatSVF() { + PrintSCS3270 scs = new PrintSCS3270(config, pd, translator); + + // Standard SNA SVF (0x2B 0xC2): count=6, MPL=66, TM=3, BM=60, tabs at 15, 30 + byte[] svf = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SVF, + 0x06, // count + 66, // MPL + 3, // Top Margin + 60, // Bottom Margin + 15, // VTab 1 + 30 // VTab 2 + }; + + scs.processHostData(svf, 0, svf.length); + assertEquals(66, scs.getPageLength()); + assertEquals(3, scs.getTopMargin()); + assertEquals(60, scs.getBottomMargin()); + assertEquals(3, scs.getCurrentRow()); // Clamped to TM + + // Test Vertical Tab (VT 0x0B) + byte[] vt = new byte[]{ PrinterConstants.SCS_VT }; + scs.processHostData(vt, 0, vt.length); + assertEquals(15, scs.getCurrentRow()); + + scs.processHostData(vt, 0, vt.length); + assertEquals(30, scs.getCurrentRow()); + } + + @Test + public void testSCSLineDensitySLD() { + PrintSCS3270 scs = new PrintSCS3270(config, pd, translator); + + // SLD (0x2B 0xC6): 12 points = 6 LPI + byte[] sld6 = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SLD, + 0x02, // count + 12 // points (12 = 6 LPI) + }; + scs.processHostData(sld6, 0, sld6.length); + assertEquals(6, scs.getLinesPerInch()); + assertEquals(6, pd.getLPI()); + + // SLD (0x2B 0xC6): 9 points = 8 LPI + byte[] sld8 = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SLD, + 0x02, // count + 9 // points (9 = 8 LPI) + }; + scs.processHostData(sld8, 0, sld8.length); + assertEquals(8, scs.getLinesPerInch()); + assertEquals(8, pd.getLPI()); + + // SLD with count=1 (defaults to 6 LPI) + byte[] sldDefault = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SLD, + 0x01 + }; + scs.processHostData(sldDefault, 0, sldDefault.length); + assertEquals(6, scs.getLinesPerInch()); + } + + @Test + public void testSCSPrintDensitySPD() { + PrintSCS3270 scs = new PrintSCS3270(config, pd, translator); + + // SPD (0x2B 0xD2): standard parameter format count=4, type=0x29, density=12 CPI + byte[] spd12 = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SPD, + 0x04, + PrinterConstants.SPD_TYPE_ID, // 0x29 + 0x00, + 12 + }; + scs.processHostData(spd12, 0, spd12.length); + assertEquals(12, scs.getCharsPerInch()); + assertEquals(12, pd.getCPI()); + assertFalse(scs.isDoubleWidth()); + + // SPD (0x2B 0xD2): density=15 CPI + byte[] spd15 = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SPD, + 0x04, + PrinterConstants.SPD_TYPE_ID, + 0x00, + 15 + }; + scs.processHostData(spd15, 0, spd15.length); + assertEquals(15, scs.getCharsPerInch()); + assertFalse(scs.isDoubleWidth()); + + // SPD density=5 CPI (triggers double-width 10 CPI) + byte[] spd5 = new byte[]{ + (byte) PrinterConstants.SCS_PREFIX_2B, + (byte) PrinterConstants.SCS_SPD, + 0x04, + PrinterConstants.SPD_TYPE_ID, + 0x00, + 5 + }; + scs.processHostData(spd5, 0, spd5.length); + assertEquals(10, scs.getCharsPerInch()); + assertTrue(scs.isDoubleWidth()); + } + + @Test + public void testSCSTransparentDataTRN() { + PrintSCS3270 scs = new PrintSCS3270(config, pd, translator); + + // TRN (0x35): bypass translation and emit raw binary escape bytes + byte[] rawPayload = new byte[]{ 0x1B, 0x5B, 0x32, 0x4A }; // ANSI Clear Screen ESC [ 2 J + byte[] trn = new byte[]{ + PrinterConstants.SCS_TRN, + (byte) rawPayload.length, + rawPayload[0], rawPayload[1], rawPayload[2], rawPayload[3] + }; + + scs.processHostData(trn, 0, trn.length); + byte[] captured = pd.getCapturedBytes(); + assertNotNull(captured); + assertTrue(captured.length >= 4); + assertEquals(0x1B, captured[0]); + assertEquals(0x5B, captured[1]); + assertEquals(0x32, captured[2]); + assertEquals(0x4A, captured[3]); + } + + @Test + public void testSCSFormFeedAndPositions() { + PrintSCS3270 scs = new PrintSCS3270(config, pd, translator); + scs.setHorizontalMargins(5, 80); + scs.setVerticalMargins(2, 60); + + // Print some characters + byte[] text = translator.stringToEbcdic("LINE 1"); + scs.processHostData(text, 0, text.length); + + // Form Feed + scs.processHostData(new byte[]{ PrinterConstants.SCS_FF }, 0, 1); + + assertEquals(1, pd.getPageCount()); + assertEquals(2, scs.getCurrentRow()); // Reset to TM + assertEquals(5, scs.getCurrentColumn()); // Reset to LM + } + + @Test + public void testSCSPresentationPositionPP() { + PrintSCS3270 scs = new PrintSCS3270(config, pd, translator); + + // PP (0x34): absolute horizontal column 35 + byte[] ppHoriz = new byte[]{ + PrinterConstants.SCS_PP, + (byte) PrinterConstants.PP_HORIZ_ABS, + 35 + }; + scs.processHostData(ppHoriz, 0, ppHoriz.length); + assertEquals(35, scs.getCurrentColumn()); + + // PP (0x34): relative horizontal +10 + byte[] ppHorizRel = new byte[]{ + PrinterConstants.SCS_PP, + (byte) PrinterConstants.PP_HORIZ_REL, + 10 + }; + scs.processHostData(ppHorizRel, 0, ppHorizRel.length); + assertEquals(45, scs.getCurrentColumn()); + + // PP (0x34): absolute vertical row 5 + byte[] ppVert = new byte[]{ + PrinterConstants.SCS_PP, + (byte) PrinterConstants.PP_VERT_ABS, + 5 + }; + scs.processHostData(ppVert, 0, ppVert.length); + assertEquals(5, scs.getCurrentRow()); + } + + // ========================================== + // 20.2 DSC / DSE Buffer Processing (LU-3) + // ========================================== + + @Test + public void testLU3WriteAndNonDisplayFieldMasking() throws Exception { + PrintPS3270 ps = new PrintPS3270(config, pd, translator); + + ByteArrayOutputStream ds = new ByteArrayOutputStream(); + ds.write(DS3270Constants.CMD_ERASE_WRITE); + ds.write(PrinterConstants.WCC_START_PRINT_BIT); // WCC: Start print + + // SBA to (0, 0) + ds.write(PrinterConstants.ORDER_SBA); + ds.write(0x40); + ds.write(0x40); + + // SF with visible attribute (0xC0: unprotected, display) + ds.write(PrinterConstants.ORDER_SF); + ds.write(0xC0); + ds.write(translator.stringToEbcdic("PUBLIC")); + + // SF with non-display attribute (0x4C: protected, non-display) + ds.write(PrinterConstants.ORDER_SF); + ds.write(0x4C); + ds.write(translator.stringToEbcdic("SECRET")); + + // SF with visible attribute again + ds.write(PrinterConstants.ORDER_SF); + ds.write(0xC0); + ds.write(translator.stringToEbcdic("VISIBLE")); + + byte[] record = ds.toByteArray(); + ps.process3270PrintDS(record, 0, record.length); + + String captured = pd.getCapturedText(); + assertNotNull(captured); + assertTrue(captured.contains("PUBLIC")); + assertFalse(captured.contains("SECRET")); // Masked as blanks! + assertTrue(captured.contains("VISIBLE")); + } + + @Test + public void testLU3EraseWriteAndEORFlush() throws Exception { + PrintPS3270 ps = new PrintPS3270(config, pd, translator); + + // Write first record without Start Print bit + ByteArrayOutputStream ds = new ByteArrayOutputStream(); + ds.write(DS3270Constants.CMD_WRITE); + ds.write(0x00); // WCC without start print + ds.write(PrinterConstants.ORDER_SBA); + ds.write(0x40); + ds.write(0x40); + ds.write(translator.stringToEbcdic("RECORD 1 DATA")); + + byte[] r1 = ds.toByteArray(); + ps.process3270PrintDS(r1, 0, r1.length); + + // EOR should flush formatted page + ps.endOfRecord(); + + String captured = pd.getCapturedText(); + assertTrue(captured.contains("RECORD 1 DATA")); + } + + // ========================================== + // 20.3 Double-Byte (DBCS) SCS & DSE Print Stream + // ========================================== + + @Test + public void testDBCSSCSSpacingAndPitch() throws Exception { + config.setCodePage("930"); // Japanese DBCS + EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930"); + PrintSCS3270DB scsDb = new PrintSCS3270DB(config, pd, dbcsTranslator); + + // Setting base CPI to 12 should update DBCS CPI to 6 + scsDb.setPrintDensity(12, 6); + assertEquals(12, scsDb.getCharsPerInch()); + assertEquals(6, scsDb.getDBCSCPI()); + + // Print 'X', SO, DBCS ideographic blank (0x4040), SI, 'Y' + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.write(translator.stringToEbcdic("X")); + stream.write(PrinterConstants.SCS_SO); + stream.write(0x40); + stream.write(0x40); + stream.write(PrinterConstants.SCS_SI); + stream.write(translator.stringToEbcdic("Y")); + + scsDb.processHostData(stream.toByteArray(), 0, stream.size()); + scsDb.flushLineBuffer(); + + String captured = pd.getCapturedText(); + assertTrue(captured.contains("X\u3000 Y")); // X at col 1, DBCS 2 cols (col 2,3), Y at col 4 + + // EOR resets DBCS mode if active + scsDb.processSO(); + assertTrue(scsDb.isDBCSMode()); + scsDb.endOfRecord(); + assertFalse(scsDb.isDBCSMode()); + } + + @Test + public void testDBCSLU3PrintStreamSOandSI() { + config.setCodePage("930"); + EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930"); + PrintPS3270DB psDb = new PrintPS3270DB(config, pd, dbcsTranslator); + + ByteArrayOutputStream ds = new ByteArrayOutputStream(); + ds.write(DS3270Constants.CMD_ERASE_WRITE); + ds.write(PrinterConstants.WCC_START_PRINT_BIT); + + ds.write(PrinterConstants.ORDER_SBA); + ds.write(0x40); + ds.write(0x40); + + // SBCS 'A' + ds.write(0xC1); + // SO + ds.write(PrinterConstants.SCS_SO); + // DBCS Kanji '日' (in Cp930: 0x43 0x81) + ds.write(0x43); + ds.write(0x81); + // SI + ds.write(PrinterConstants.SCS_SI); + // SBCS 'Z' + ds.write(0xE9); + + byte[] record = ds.toByteArray(); + psDb.process3270PrintDS(record, 0, record.length); + + // Check DBCS cells in buffer + assertTrue(psDb.isDBCSCell(1)); + assertTrue(psDb.isDBCSLeft(1)); + assertTrue(psDb.isDBCSCell(2)); + assertTrue(psDb.isDBCSRight(2)); + + String captured = pd.getCapturedText(); + assertNotNull(captured); + assertTrue(captured.contains("A")); + assertTrue(captured.contains("Z")); + } +}