diff --git a/build_all.sh b/build_all.sh index 522b0de..061e89a 100755 --- a/build_all.sh +++ b/build_all.sh @@ -131,9 +131,7 @@ public class TestRunner { summary.printTo(new PrintWriter(System.out)); summary.printFailuresTo(new PrintWriter(System.err)); - if (summary.getTotalFailureCount() > 0) { - System.exit(1); - } + System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0); } } EOF diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index 4332463..0171608 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -178,6 +178,38 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate viewMenu.add(themeMenu); viewMenu.addSeparator(); + JCheckBoxMenuItem rulerItem = new JCheckBoxMenuItem("Crosshair Ruler", terminalPanel.isCrosshairRulerEnabled()); + ThemeManager.styleMenuItem(rulerItem); + rulerItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + rulerItem.addActionListener(e -> { + boolean en = rulerItem.isSelected(); + terminalPanel.setCrosshairRulerEnabled(en); + haus.nightmare.j3270.config.Settings.setCrosshairRuler(en); + }); + viewMenu.add(rulerItem); + + JMenu cursorMenu = createMenu("Cursor Style"); + ButtonGroup cursorGroup = new ButtonGroup(); + JRadioButtonMenuItem blockCursorItem = new JRadioButtonMenuItem("Block Cursor", terminalPanel.getCursorStyle() == TerminalPanel.CursorStyle.BLOCK); + ThemeManager.styleMenuItem(blockCursorItem); + blockCursorItem.addActionListener(e -> { + terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.BLOCK); + haus.nightmare.j3270.config.Settings.setCursorStyle("BLOCK"); + }); + cursorGroup.add(blockCursorItem); + cursorMenu.add(blockCursorItem); + + JRadioButtonMenuItem underlineCursorItem = new JRadioButtonMenuItem("Underline Cursor", terminalPanel.getCursorStyle() == TerminalPanel.CursorStyle.UNDERLINE); + ThemeManager.styleMenuItem(underlineCursorItem); + underlineCursorItem.addActionListener(e -> { + terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE); + haus.nightmare.j3270.config.Settings.setCursorStyle("UNDERLINE"); + }); + cursorGroup.add(underlineCursorItem); + cursorMenu.add(underlineCursorItem); + viewMenu.add(cursorMenu); + viewMenu.addSeparator(); + // CodePage Submenu JMenu cpMenu = createMenu("Code Page"); String[] codePages = { @@ -704,7 +736,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate "Delete — Delete character\n" + "Backspace — Backspace\n" + "Insert — Toggle insert mode\n" + - "Escape — Reset\n" + + "Escape / Alt+R — Reset\n" + "PageUp/Down — PF7/PF8\n" + "Alt+C / Alt+K — Clear\n" + "Alt+E — Erase Input\n" + @@ -713,6 +745,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate "Alt+Q — Cursor Select\n" + "Alt+L — Toggle Light Pen\n" + "Alt+T — File Transfer\n" + + "Alt+Shift+R — Crosshair Ruler\n" + "Alt+=/-/0 — Font size +/-/reset\n" + "Cmd/Ctrl+F — Find on Screen\n" + "Cmd/Ctrl+G / F3— Find Next\n" + @@ -893,10 +926,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } TerminalModel model = TerminalModel.IBM_3279_4; if (remainingArgs.size() >= 3) { - try { - int modelNum = Integer.parseInt(remainingArgs.get(2)); - model = TerminalModel.forModel(modelNum, true); - } catch (Exception ignored) {} + String mArg = remainingArgs.get(2).trim(); + if (mArg.equalsIgnoreCase("dynamic") || mArg.equals("0")) { + model = TerminalModel.IBM_DYNAMIC; + } else { + try { + int modelNum = Integer.parseInt(mArg); + model = TerminalModel.forModel(modelNum, true); + } catch (Exception ignored) {} + } } ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model); if (finalTls) { diff --git a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java index e751893..9f6fc56 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java +++ b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java @@ -113,6 +113,22 @@ public class Settings { prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037"); } + public static int getDynamicRows() { + return prefs.getInt("dynamicRows", 62); + } + + public static void setDynamicRows(int rows) { + prefs.putInt("dynamicRows", Math.max(1, rows)); + } + + public static int getDynamicCols() { + return prefs.getInt("dynamicCols", 160); + } + + public static void setDynamicCols(int cols) { + prefs.putInt("dynamicCols", Math.max(1, cols)); + } + public static Color getColorOverride(int index, Color defaultColor) { String hex = prefs.get("color_" + index, null); try { @@ -233,6 +249,26 @@ public class Settings { prefs.putBoolean("blockSelectMode", block); } + // ========== Crosshair Ruler ========== + + public static boolean getCrosshairRuler() { + return prefs.getBoolean("crosshairRuler", false); + } + + public static void setCrosshairRuler(boolean enabled) { + prefs.putBoolean("crosshairRuler", enabled); + } + + // ========== Cursor Style ========== + + public static String getCursorStyle() { + return prefs.get("cursorStyle", "BLOCK"); + } + + public static void setCursorStyle(String style) { + prefs.put("cursorStyle", style != null ? style.toUpperCase() : "BLOCK"); + } + private static void applyConfigEntry(String section, String key, String value) { switch (section) { case "appearance": @@ -244,6 +280,13 @@ public class Settings { case "uiTheme": setJavaUiTheme(UITheme.fromString(value)); break; + case "crosshairRuler": + case "ruler": + setCrosshairRuler(Boolean.parseBoolean(value)); + break; + case "cursorStyle": + setCursorStyle(value); + break; default: log.warning("Unknown appearance key: " + key); } @@ -284,6 +327,14 @@ public class Settings { case "charset": setCodePage(value); break; + case "dynamicRows": + case "dynamic_rows": + setDynamicRows(Integer.parseInt(value)); + break; + case "dynamicCols": + case "dynamic_cols": + setDynamicCols(Integer.parseInt(value)); + break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; default: log.warning("Unknown behavior/connection key: " + key); @@ -340,6 +391,8 @@ public class Settings { w.println("javaUiTheme = " + getJavaUiTheme().name()); w.println("fontFamily = " + getFontFamily()); w.println("fontSize = " + getFontSize()); + w.println("crosshairRuler = " + getCrosshairRuler()); + w.println("cursorStyle = " + getCursorStyle()); w.println(); // [behavior] @@ -354,6 +407,8 @@ public class Settings { w.println("autoConnectTn3270e = " + getAutoConnectTn3270e()); } w.println("codePage = " + getCodePage()); + w.println("dynamicRows = " + getDynamicRows()); + w.println("dynamicCols = " + getDynamicCols()); w.println("blockSelectMode = " + getBlockSelectMode()); w.println(); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java index bbd25dd..33302a4 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java @@ -14,6 +14,10 @@ public class ConnectDialog extends JDialog { private JTextField hostField; private JTextField portField; private JComboBox modelCombo; + private JLabel dynamicDimLabel; + private JPanel dynamicDimPanel; + private JSpinner dynamicRowsSpinner; + private JSpinner dynamicColsSpinner; private JComboBox graphicsCombo; private JComboBox codePageCombo; private JTextField luField; @@ -79,10 +83,49 @@ public class ConnectDialog extends JDialog { ThemeManager.styleComboBox(modelCombo); mainPanel.add(modelCombo, gbc); - // LU Name + // Dynamic Dimensions (shown when IBM-DYNAMIC is selected) gbc.gridx = 0; gbc.gridy = 3; gbc.weightx = 0; + dynamicDimLabel = new JLabel("Screen Size:"); + dynamicDimLabel.setFont(labelFont); + mainPanel.add(dynamicDimLabel, gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + dynamicDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + dynamicDimPanel.setOpaque(false); + + JLabel rowsLabel = new JLabel("Rows:"); + rowsLabel.setFont(labelFont); + dynamicRowsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicRows(), 24, 255, 1)); + ThemeManager.styleSpinner(dynamicRowsSpinner); + + JLabel colsLabel = new JLabel("Cols:"); + colsLabel.setFont(labelFont); + dynamicColsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicCols(), 80, 255, 1)); + ThemeManager.styleSpinner(dynamicColsSpinner); + + dynamicDimPanel.add(rowsLabel); + dynamicDimPanel.add(dynamicRowsSpinner); + dynamicDimPanel.add(colsLabel); + dynamicDimPanel.add(dynamicColsSpinner); + mainPanel.add(dynamicDimPanel, gbc); + + Runnable updateDynamicVisibility = () -> { + TerminalModel m = (TerminalModel) modelCombo.getSelectedItem(); + boolean isDyn = (m != null && m.isDynamic()); + dynamicDimLabel.setVisible(isDyn); + dynamicDimPanel.setVisible(isDyn); + pack(); + }; + modelCombo.addActionListener(e -> updateDynamicVisibility.run()); + updateDynamicVisibility.run(); + + // LU Name + gbc.gridx = 0; + gbc.gridy = 4; + gbc.weightx = 0; JLabel luLabel = new JLabel("LU Name:"); luLabel.setFont(labelFont); mainPanel.add(luLabel, gbc); @@ -93,7 +136,7 @@ public class ConnectDialog extends JDialog { // Graphics Mode gbc.gridx = 0; - gbc.gridy = 4; + gbc.gridy = 5; gbc.weightx = 0; JLabel graphicsLabel = new JLabel("Graphics:"); graphicsLabel.setFont(labelFont); @@ -108,7 +151,7 @@ public class ConnectDialog extends JDialog { // Code Page gbc.gridx = 0; - gbc.gridy = 5; + gbc.gridy = 6; gbc.weightx = 0; JLabel cpLabel = new JLabel("Code Page:"); cpLabel.setFont(labelFont); @@ -167,7 +210,7 @@ public class ConnectDialog extends JDialog { // TLS / SSL Checkbox gbc.gridx = 1; - gbc.gridy = 6; + gbc.gridy = 7; gbc.weightx = 1.0; tlsCheckBox = new JCheckBox("Enable TLS/SSL"); ThemeManager.styleCheckBox(tlsCheckBox); @@ -186,7 +229,7 @@ public class ConnectDialog extends JDialog { // Verify Certificate Checkbox gbc.gridx = 1; - gbc.gridy = 7; + gbc.gridy = 8; verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); ThemeManager.styleCheckBox(verifyCertCheckBox); verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); @@ -196,7 +239,7 @@ public class ConnectDialog extends JDialog { // TN3270E Checkbox gbc.gridx = 1; - gbc.gridy = 8; + gbc.gridy = 9; tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); ThemeManager.styleCheckBox(tn3270eCheckBox); tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); @@ -223,7 +266,7 @@ public class ConnectDialog extends JDialog { buttonPanel.add(connectBtn); gbc.gridx = 0; - gbc.gridy = 9; + gbc.gridy = 10; gbc.gridwidth = 2; mainPanel.add(buttonPanel, gbc); @@ -256,7 +299,15 @@ public class ConnectDialog extends JDialog { return; } - result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem()); + TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem(); + result = new ConnectionConfig(host, port, selectedModel); + if (selectedModel != null && selectedModel.isDynamic()) { + int dRows = (Integer) dynamicRowsSpinner.getValue(); + int dCols = (Integer) dynamicColsSpinner.getValue(); + result.setDynamicDimensions(dRows, dCols); + haus.nightmare.j3270.config.Settings.setDynamicRows(dRows); + haus.nightmare.j3270.config.Settings.setDynamicCols(dCols); + } String lu = luField.getText().trim(); if (!lu.isEmpty()) { result.setLuName(lu); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/ScreenExporter.java b/j3270/src/main/java/haus/nightmare/j3270/ui/ScreenExporter.java index 5e8f4a1..b1b5196 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ScreenExporter.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ScreenExporter.java @@ -98,19 +98,21 @@ public class ScreenExporter { writer.write("\n\n\n
");
@@ -209,17 +211,17 @@ public class ScreenExporter {
                 case 6: return "c-yellow";
                 case 7: return "c-white";
                 case 8: return "c-black";
-                case 9: return "c-blue";
+                case 9: return "c-deepblue";
                 case 10: return "c-orange";
                 case 11: return "c-purple";
                 case 12: return "c-palegreen";
                 case 13: return "c-paleturq";
-                case 14: return "c-grey";
-                case 15: return "c-white";
+                case 14: return "c-mustard";
+                case 15: return "c-grey";
             }
         }
         if (faIsProtected(currentFA & 0xFF)) {
-            return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue";
+            return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-turq";
         }
         return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
     }
diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java
index e3f02c0..8625350 100644
--- a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java
+++ b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java
@@ -31,6 +31,8 @@ public class SettingsDialog extends JDialog {
     private JTextField hostField;
     private JTextField portField;
     private JCheckBox blockSelectCheck;
+    private JSpinner dynamicRowsSpinner;
+    private JSpinner dynamicColsSpinner;
 
     // Advanced tab state tracking
     private final Color[] tempHostColors = new Color[16];
@@ -235,8 +237,30 @@ public class SettingsDialog extends JDialog {
         blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
         panel.add(blockSelectCheck, gbc);
 
-        // Placeholder for potentially more behavior options below
+        // Default Dynamic Screen Size
         gbc.gridy = 3;
+        gbc.gridwidth = 1;
+        gbc.gridx = 0;
+        panel.add(new JLabel("Default Dynamic Screen:"), gbc);
+
+        JPanel dynDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
+        dynDimPanel.setOpaque(false);
+        dynDimPanel.add(new JLabel("Rows:"));
+        dynamicRowsSpinner = new JSpinner(new SpinnerNumberModel(Settings.getDynamicRows(), 24, 255, 1));
+        ThemeManager.styleSpinner(dynamicRowsSpinner);
+        dynDimPanel.add(dynamicRowsSpinner);
+        dynDimPanel.add(new JLabel("Cols:"));
+        dynamicColsSpinner = new JSpinner(new SpinnerNumberModel(Settings.getDynamicCols(), 80, 255, 1));
+        ThemeManager.styleSpinner(dynamicColsSpinner);
+        dynDimPanel.add(dynamicColsSpinner);
+
+        gbc.gridx = 1;
+        panel.add(dynDimPanel, gbc);
+
+        // Placeholder for potentially more behavior options below
+        gbc.gridx = 0;
+        gbc.gridy = 4;
+        gbc.gridwidth = 2;
         gbc.weighty = 1.0;
         panel.add(Box.createGlue(), gbc);
 
@@ -555,6 +579,12 @@ public class SettingsDialog extends JDialog {
             // Block select mode
             Settings.setBlockSelectMode(blockSelectCheck.isSelected());
 
+            // Default Dynamic screen dimensions
+            if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
+                Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
+                Settings.setDynamicCols((Integer) dynamicColsSpinner.getValue());
+            }
+
             // Propagate visual changes to the app
             // Save Colors
             for (int i=0; i<16; i++) {
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 dba72ca..12b1ea1 100644
--- a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java
+++ b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java
@@ -110,25 +110,25 @@ public class StatusBar extends JPanel {
                 break;
             case CONNECTED_3270:
                 connectionStatus.setText("TN3270");
-                connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
+                connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
                 break;
             case CONNECTED_TN3270E:
                 connectionStatus.setText("TN3270E");
-                connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
+                connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
                 break;
             case CONNECTED_SSCP:
                 connectionStatus.setText("SSCP-LU");
-                connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
+                connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
                 break;
             case CONNECTED_NVT:
             case CONNECTED_NVT_CHAR:
             case CONNECTED_E_NVT:
                 connectionStatus.setText("NVT");
-                connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
+                connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
                 break;
             case CONNECTED_UNBOUND:
                 connectionStatus.setText("Unbound");
-                connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
+                connectionStatus.setForeground(ThemeManager.getOiaAttention());
                 break;
             default:
                 connectionStatus.setText(state.name());
@@ -144,11 +144,11 @@ public class StatusBar extends JPanel {
             String protocol = session != null ? session.getProtocol() : "TLS";
             if (verified) {
                 tlsStatus.setText("🔒 TLS");
-                tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
+                tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
                 tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
             } else {
                 tlsStatus.setText("🔓 TLS (Unverified)");
-                tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
+                tlsStatus.setForeground(ThemeManager.getOiaAttention());
                 tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
             }
         } else {
@@ -164,38 +164,46 @@ public class StatusBar extends JPanel {
             lu = "LU:" + client.getConfig().getLuName();
         }
         luName.setText(lu);
-        luName.setForeground(ThemeManager.getOiaFgNormal(theme));
+        luName.setForeground(ThemeManager.getOiaStatusSysAvail());
 
         // Lock / Inhibit status
         int inhibit = client.getOIA().getInputInhibited();
         if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
+            Color lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
             switch (inhibit) {
                 case ECLConstants.INHIBIT_SYSTEM_LOCK:
                     lockStatus.setText("X SYSTEM");
+                    lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
                     break;
                 case ECLConstants.INHIBIT_COMM_CHECK:
                     lockStatus.setText("X COMM");
+                    lockFg = ThemeManager.getOiaCommCheck(); // Red (oEI)
                     break;
                 case ECLConstants.INHIBIT_NUMERIC_ONLY:
                     lockStatus.setText("X NUM");
+                    lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
                     break;
                 case ECLConstants.INHIBIT_PROTECTED_FIELD:
                     lockStatus.setText("X PROT");
+                    lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
                     break;
                 case ECLConstants.INHIBIT_OVERFLOW:
                     lockStatus.setText("X >");
+                    lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
                     break;
                 case ECLConstants.INHIBIT_OPERATOR_DUE:
                     lockStatus.setText("X OP");
+                    lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
                     break;
                 default:
                     lockStatus.setText("X LOCKED");
+                    lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
                     break;
             }
-            lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
+            lockStatus.setForeground(lockFg);
         } else if (client.getInputProcessor().isInsertMode()) {
             lockStatus.setText("INSERT");
-            lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
+            lockStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
         } else {
             lockStatus.setText("");
         }
@@ -223,7 +231,13 @@ public class StatusBar extends JPanel {
         ScreenBuffer sb = client.getScreenBuffer();
         int rows = sb.getDisplayRows();
         int cols = sb.getDisplayCols();
-        modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
+        String modelName;
+        if (client.getConfig().isDynamicModel() || (client.getConfig().getModel() != null && client.getConfig().getModel().isDynamic())) {
+            modelName = "IBM-DYNAMIC [" + rows + "x" + cols + "]";
+        } else {
+            modelName = client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]";
+        }
+        modelInfo.setText(modelName);
         modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
 
         // Cursor position and buffer address
diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java
index 5645edc..8567b83 100644
--- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java
+++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java
@@ -57,11 +57,22 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
             CHAR_STRINGS[i] = String.valueOf((char) i);
         }
     }
+    public enum CursorStyle {
+        BLOCK,
+        UNDERLINE
+    }
+    private CursorStyle cursorStyle = CursorStyle.BLOCK;
     private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
+    private boolean crosshairRulerEnabled = false;
+    private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
+    private boolean textBlinkVisible = true;
+    private Image wallpaperImage = null;
+    private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
+
     private int selectionStartRow = -1, selectionStartCol = -1;
     private int selectionEndRow = -1, selectionEndCol = -1;
     private boolean isDragging = false;
-    private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
+    private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
 
     // ========== Search Highlight state ==========
     private int searchHighlightAddr = -1;
@@ -96,31 +107,31 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
         return Math.max(padding, extra / 2);
     }
 
-    // Default Host color mapping
+    // Default Host color mapping aligned 1:1 with IBM Host On-Demand ColorRemapModel3270
     public static final Color[] DEFAULT_HOST_COLORS = {
             new Color(0, 0, 0),       // 0: Neutral Black
-            new Color(80, 120, 255),  // 1: Blue
-            new Color(255, 50, 50),   // 2: Red
-            new Color(255, 130, 180), // 3: Pink
-            new Color(50, 205, 50),   // 4: Green
-            new Color(64, 224, 208),  // 5: Turquoise
-            new Color(255, 255, 80),  // 6: Yellow
+            new Color(120, 144, 240), // 1: Blue (0x7890F0 CUSTOMBLUE)
+            new Color(255, 0, 0),     // 2: Red
+            new Color(255, 0, 255),   // 3: Pink
+            new Color(0, 255, 0),     // 4: Green
+            new Color(0, 255, 255),   // 5: Turquoise / Cyan
+            new Color(255, 255, 0),   // 6: Yellow
             new Color(255, 255, 255), // 7: Neutral White
             new Color(0, 0, 0),       // 8: Black
-            new Color(30, 60, 180),   // 9: Deep Blue
-            new Color(255, 165, 0),   // 10: Orange
-            new Color(180, 130, 255), // 11: Purple
-            new Color(144, 238, 144), // 12: Pale Green
-            new Color(175, 238, 238), // 13: Pale Turquoise
-            new Color(170, 170, 170), // 14: Grey
-            new Color(255, 255, 255), // 15: White
+            new Color(0, 0, 128),     // 9: Deep Blue
+            new Color(255, 162, 0),   // 10: Orange (0xFFFFA200)
+            new Color(128, 0, 128),   // 11: Purple
+            new Color(0, 128, 0),     // 12: Pale Green
+            new Color(0, 128, 128),   // 13: Pale Turquoise
+            new Color(160, 160, 0),   // 14: Mustard (0xFFA0A000)
+            new Color(192, 192, 192), // 15: Grey (0xFFC0C0C0)
     };
 
-    // Default 3278 monochrome colors
-    public static final Color DEFAULT_MONO_NORMAL = new Color(50, 205, 50);
-    public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 255, 255);
-    public static final Color DEFAULT_MONO_PROTECTED = new Color(80, 120, 255);
-    public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
+    // Default 3278 / base monochrome colors aligned with HoD
+    public static final Color DEFAULT_MONO_NORMAL = new Color(0, 255, 0);          // Green
+    public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 0, 0);       // Red
+    public static final Color DEFAULT_MONO_PROTECTED = new Color(0, 255, 255);     // Cyan / Turquoise
+    public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255); // White
 
     // Default Background
     public static final Color DEFAULT_BG_COLOR = Color.BLACK;
@@ -138,6 +149,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
         setupCursorBlink();
 
         blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
+        crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
+        String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
+        cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
 
         // Handle mouse clicks to position cursor, selection, and grab focus
         MouseAdapter mouseHandler = new MouseAdapter() {
@@ -1075,6 +1089,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
         setupKeyBindings();
         setupFont();
         blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
+        crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
+        String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
+        cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
         revalidate();
         repaint();
     }
@@ -1103,13 +1120,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
     }
 
     private void setupCursorBlink() {
-        blinkTimer = new Timer(530, e -> {
+        blinkTimer = new Timer(500, e -> {
             cursorVisible = !cursorVisible;
+            textBlinkVisible = !textBlinkVisible;
             repaint();
         });
         blinkTimer.start();
     }
 
+    public Telnet3270Client getClient() {
+        return client;
+    }
+
     public void setClient(Telnet3270Client client) {
         this.client = client;
         if (client != null) {
@@ -1247,7 +1269,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
         int cols = sb.getDisplayCols();
         boolean isColorModel = client.getConfig().getModel().isColor();
 
-        // Draw Vector Graphics Plane under text if present
+        // Layer 0: Optional Wallpaper / Background Image
+        if (hodWallpaper != null) {
+            int gridW = cols * cellWidth;
+            int gridH = rows * cellHeight;
+            hodWallpaper.paint(this, g2, ox, oy, gridW, gridH);
+        } else if (wallpaperImage != null) {
+            int gridW = cols * cellWidth;
+            int gridH = rows * cellHeight;
+            g2.drawImage(wallpaperImage, ox, oy, gridW, gridH, null);
+        }
+
+        // Layer 1: Draw Vector Graphics Plane under text if present
         if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) {
             int gridW = cols * cellWidth;
             int gridH = rows * cellHeight;
@@ -1269,6 +1302,22 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
             }
         }
 
+        // Layer 2: Optional Crosshair Ruler under character glyphs
+        if (crosshairRulerEnabled && client.getConnectionState().isFullSession()) {
+            int curAddr = sb.getDisplayCursorAddress();
+            int curRow = curAddr / cols;
+            int curCol = curAddr % cols;
+            int cx = ox + curCol * cellWidth;
+            int cy = oy + curRow * cellHeight;
+            int gridW = cols * cellWidth;
+            int gridH = rows * cellHeight;
+
+            g2.setColor(CROSSHAIR_RULER_COLOR);
+            g2.fillRect(ox, cy, gridW, cellHeight);
+            g2.fillRect(cx, oy, cellWidth, gridH);
+        }
+
+        // Layer 3: Character / Text Plane
         byte currentFA = 0;
         ExtendedAttribute currentFieldEa = null;
 
@@ -1296,7 +1345,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
                     continue;
                 }
 
-                // Determine foreground color
+                // Determine foreground and background colors
                 if (isColorModel) {
                     fgColor = getColorForAttribute(ea, currentFieldEa, currentFA);
                     bgColor = getBackgroundForAttribute(ea, currentFieldEa);
@@ -1305,6 +1354,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
                     bgColor = this.bgColor;
                 }
 
+                int rawBg = ea.bg != 0 ? (ea.bg & 0xFF) : (currentFieldEa != null ? (currentFieldEa.bg & 0xFF) : 0);
+                boolean bgIsExplicit = (rawBg >= 0xF0 && rawBg <= 0xFF && rawBg != HOST_COLOR_NEUTRAL_BLACK && rawBg != HOST_COLOR_BLACK);
+
                 // Graphics rendition
                 byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
                 if (gr != 0) {
@@ -1334,22 +1386,26 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
 
                 if (reverse) {
                     Color tmp = fgColor;
-                    fgColor = bgColor;
+                    fgColor = (rawBg == 0) ? this.bgColor : bgColor;
                     bgColor = tmp;
-                }
-
-                if (!bgColor.equals(this.bgColor) || reverse) {
+                    g2.setColor(bgColor);
+                    g2.fillRect(x, y, cellWidth, cellHeight);
+                } else if (bgIsExplicit || !bgColor.equals(this.bgColor)) {
                     g2.setColor(bgColor);
                     g2.fillRect(x, y, cellWidth, cellHeight);
                 }
 
+                // Check text blink (500ms cycle)
+                boolean blink = (gr & GR_BLINK) != 0;
+                boolean suppressGlyph = blink && !textBlinkVisible;
+
                 // Draw character or Programmed Symbol
                 int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
                 boolean drawnAsPs = false;
-                if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
+                if (cs >= 0x40 && client.getProgramSymbolManager() != null && !suppressGlyph) {
                     haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
                     if (slot != null) {
-                        int symBg = (!bgColor.equals(this.bgColor) || reverse) ? bgColor.getRGB() : 0;
+                        int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
                         java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg);
                         if (img != null) {
                             g2.drawImage(img, x, y, null);
@@ -1358,7 +1414,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
                     }
                 }
 
-                if (!drawnAsPs) {
+                if (!drawnAsPs && !suppressGlyph) {
                     char ch = ea.ucs4;
                     if (ch > 0x20 && ch != 0xFF) {
                         Font f = bold ? boldTerminalFont : terminalFont;
@@ -1385,7 +1441,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
                     g2.fillRect(x, y, cellWidth, cellHeight);
                 }
 
-                // Draw selection highlight
+                // Draw selection highlight (40% alpha blend over cell)
                 if (isCellSelected(row, col)) {
                     g2.setColor(SELECTION_COLOR);
                     g2.fillRect(x, y, cellWidth, cellHeight);
@@ -1394,9 +1450,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
         }
 
         // Draw Graphic Cursor if active
-        if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
-            int gocaX = client.getGocaDecoder().getGraphicCursorX();
-            int gocaY = client.getGocaDecoder().getGraphicCursorY();
+        boolean isGraphicCursor = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
+                || (client.getGraphicsPlane() != null && client.getGraphicsPlane().isGraphicCursorAttached());
+        if (isGraphicCursor && client.getGraphicsPlane() != null) {
+            int gocaX = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
+                    ? client.getGocaDecoder().getGraphicCursorX()
+                    : client.getGraphicsPlane().getGraphicCursorX();
+            int gocaY = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
+                    ? client.getGocaDecoder().getGraphicCursorY()
+                    : client.getGraphicsPlane().getGraphicCursorY();
             int canvasPx = client.getGraphicsPlane().mapX(gocaX);
             int canvasPy = client.getGraphicsPlane().mapY(gocaY);
             int gridW = cols * cellWidth;
@@ -1408,12 +1470,19 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
 
             g2.setColor(Color.WHITE);
             g2.setXORMode(Color.BLACK);
-            g2.drawLine(px - 6, py, px + 6, py);
-            g2.drawLine(px, py - 6, px, py + 6);
+            int shape = client.getGraphicsPlane().getHodCursorShape();
+            if (shape == 2) {
+                // Shape 2: Box cursor per HoD
+                g2.drawRect(px - 3, py - 3, 6, 6);
+            } else {
+                // Shape 1 (default): Crosshair per HoD
+                g2.drawLine(px - 8, py, px + 8, py);
+                g2.drawLine(px, py - 8, px, py + 8);
+            }
             g2.setPaintMode();
         }
 
-        // Draw 3270 text cursor
+        // Draw 3270 text cursor with alpha blending (Block / Underline)
         if (cursorVisible && client.getConnectionState().isFullSession()) {
             int curAddr = sb.getDisplayCursorAddress();
             int curRow = curAddr / cols;
@@ -1421,13 +1490,79 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
             int cx = ox + curCol * cellWidth;
             int cy = oy + curRow * cellHeight;
 
+            boolean isUnderline = (cursorStyle == CursorStyle.UNDERLINE) ||
+                                  (client.getInputProcessor() != null && client.getInputProcessor().isInsertMode());
             g2.setColor(CURSOR_COLOR);
-            g2.setXORMode(bgColor);
-            g2.fillRect(cx, cy, cellWidth, cellHeight);
-            g2.setPaintMode();
+            if (isUnderline) {
+                int ulH = Math.max(2, cellHeight / 6);
+                g2.fillRect(cx, cy + cellHeight - ulH, cellWidth, ulH);
+            } else {
+                g2.fillRect(cx, cy, cellWidth, cellHeight);
+            }
         }
     }
 
+    public boolean isCrosshairRulerEnabled() {
+        return crosshairRulerEnabled;
+    }
+
+    public void setCrosshairRulerEnabled(boolean enabled) {
+        this.crosshairRulerEnabled = enabled;
+        repaint();
+    }
+
+    public void toggleCrosshairRuler() {
+        setCrosshairRulerEnabled(!crosshairRulerEnabled);
+    }
+
+    public CursorStyle getCursorStyle() {
+        return cursorStyle;
+    }
+
+    public void setCursorStyle(CursorStyle style) {
+        this.cursorStyle = (style != null) ? style : CursorStyle.BLOCK;
+        repaint();
+    }
+
+    public Image getWallpaperImage() {
+        return wallpaperImage;
+    }
+
+    public void setWallpaperImage(Image wallpaperImage) {
+        this.wallpaperImage = wallpaperImage;
+        if (wallpaperImage != null) {
+            if (this.hodWallpaper == null) {
+                this.hodWallpaper = new haus.nightmare.lib3270j.graphics.HODWallpaper(wallpaperImage, haus.nightmare.lib3270j.graphics.HODWallpaper.HOD_STRETCH);
+            } else {
+                this.hodWallpaper.setImage(wallpaperImage);
+            }
+        } else {
+            this.hodWallpaper = null;
+        }
+        repaint();
+    }
+
+    public haus.nightmare.lib3270j.graphics.HODWallpaper getHodWallpaper() {
+        return hodWallpaper;
+    }
+
+    public void setHodWallpaper(haus.nightmare.lib3270j.graphics.HODWallpaper wallpaper) {
+        this.hodWallpaper = wallpaper;
+        if (wallpaper != null) {
+            this.wallpaperImage = wallpaper.getHODImage();
+        }
+        repaint();
+    }
+
+    public void setWallpaperMode(int displayMode) {
+        if (this.hodWallpaper == null && this.wallpaperImage != null) {
+            this.hodWallpaper = new haus.nightmare.lib3270j.graphics.HODWallpaper(this.wallpaperImage, displayMode);
+        } else if (this.hodWallpaper != null) {
+            this.hodWallpaper.setDisplay(displayMode);
+        }
+        repaint();
+    }
+
     private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
         int fg = ea.fg != 0 ? (ea.fg & 0xFF)
                 : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
@@ -1435,7 +1570,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
             return hostColors[fg - 0xf0];
         }
         if (faIsProtected(currentFA & 0xFF)) {
-            return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_BLUE];
+            return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_TURQUOISE];
         }
         return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_RED] : hostColors[HOST_COLOR_GREEN];
     }
diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java b/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java
index 9bcad98..3ec4a75 100644
--- a/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java
+++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java
@@ -244,6 +244,19 @@ public final class ThemeManager {
         return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
     }
 
+    // Host On-Demand OIA Category Colors (oSI, oII, oAI, oEI, oOB)
+    public static final Color HOD_OIA_STATUS_SYS_AVAIL  = new Color(120, 144, 240); // oSI: CUSTOMBLUE
+    public static final Color HOD_OIA_INPUT_INHIBITED   = new Color(255, 255, 255); // oII: White
+    public static final Color HOD_OIA_ATTENTION_WARN    = new Color(255, 255, 0);   // oAI: Yellow
+    public static final Color HOD_OIA_COMM_CHECK_ERROR  = new Color(255, 0, 0);     // oEI: Red
+    public static final Color HOD_OIA_BG_BLACK          = new Color(0, 0, 0);       // oOB: Black
+
+    public static Color getOiaStatusSysAvail() { return HOD_OIA_STATUS_SYS_AVAIL; }
+    public static Color getOiaInputInhibited() { return HOD_OIA_INPUT_INHIBITED; }
+    public static Color getOiaAttention()      { return HOD_OIA_ATTENTION_WARN; }
+    public static Color getOiaCommCheck()      { return HOD_OIA_COMM_CHECK_ERROR; }
+    public static Color getOiaBackground()     { return HOD_OIA_BG_BLACK; }
+
     // =========================================================================
     // Button Variants & Color helpers
     // =========================================================================
diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java
index 3a296c2..2d2e41b 100644
--- a/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java
+++ b/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java
@@ -58,12 +58,26 @@ public class MenuBarShortcutsTest {
             assertNotNull(fileMenu, "File menu should exist");
             assertNotNull(editMenu, "Edit menu should exist");
 
-            // Verify View Menu items use ALT_DOWN_MASK
+            // Verify View Menu items
             Map viewItems = collectMenuItems(viewMenu);
             assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
             assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
             assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
 
+            // Crosshair Ruler must use Alt+Shift+R
+            JMenuItem rulerItem = viewItems.get("Crosshair Ruler");
+            assertNotNull(rulerItem, "Crosshair Ruler menu item must exist");
+            KeyStroke rulerKs = rulerItem.getAccelerator();
+            assertNotNull(rulerKs, "Crosshair Ruler should have an accelerator");
+            assertEquals(KeyEvent.VK_R, rulerKs.getKeyCode(), "Key code mismatch for Crosshair Ruler");
+            int rulerMods = rulerKs.getModifiers();
+            assertTrue((rulerMods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
+                    "Crosshair Ruler accelerator must have ALT modifier");
+            assertTrue((rulerMods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK)) != 0,
+                    "Crosshair Ruler accelerator must have SHIFT modifier");
+            assertEquals(0, rulerMods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
+                    "Crosshair Ruler accelerator must NOT have CTRL or META modifiers");
+
             // Verify Actions Menu items use ALT_DOWN_MASK
             Map actionItems = collectMenuItems(actionsMenu);
             assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER);
@@ -75,6 +89,10 @@ public class MenuBarShortcutsTest {
             assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
             assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
             assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
+
+            // Ensure Crosshair Ruler and Reset do not collide
+            assertNotEquals(rulerKs, actionItems.get("Reset").getAccelerator(),
+                    "Crosshair Ruler and Reset accelerators must not collide");
         } finally {
             // Dispose frame
             app.dispose();
@@ -89,6 +107,8 @@ public class MenuBarShortcutsTest {
         int mods = ks.getModifiers();
         assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
                 "Menu item " + item.getText() + " accelerator must have ALT modifier");
+        assertEquals(0, mods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK),
+                "Menu item " + item.getText() + " accelerator must NOT have SHIFT modifier");
         assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
                 "Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
     }
diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/Phase1UiOverlayTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/Phase1UiOverlayTest.java
new file mode 100644
index 0000000..624979d
--- /dev/null
+++ b/j3270/src/test/java/haus/nightmare/j3270/ui/Phase1UiOverlayTest.java
@@ -0,0 +1,104 @@
+package haus.nightmare.j3270.ui;
+
+import haus.nightmare.j3270.config.Settings;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.awt.Color;
+import java.awt.HeadlessException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Verification test suite for Phase 1 UI Overlays and Color Alignment.
+ * Covers items 1.1, 1.2, and 1.5 of the PhasedUpdates specification.
+ */
+public class Phase1UiOverlayTest {
+
+    @Test
+    @DisplayName("Item 1.1 & 1.2: TerminalPanel host and mono colors match IBM Host On-Demand specs")
+    public void testTerminalPanelColorAlignment() {
+        // Index 10: Orange (255, 162, 0)
+        assertEquals(new Color(255, 162, 0), TerminalPanel.DEFAULT_HOST_COLORS[10]);
+
+        // Index 14: Mustard (160, 160, 0)
+        assertEquals(new Color(160, 160, 0), TerminalPanel.DEFAULT_HOST_COLORS[14]);
+
+        // Index 1: Blue (120, 144, 240)
+        assertEquals(new Color(120, 144, 240), TerminalPanel.DEFAULT_HOST_COLORS[1]);
+
+        // Base 4-Color mono defaults
+        assertEquals(new Color(0, 255, 0), TerminalPanel.DEFAULT_MONO_NORMAL);
+        assertEquals(new Color(255, 0, 0), TerminalPanel.DEFAULT_MONO_INTENSIFY);
+        assertEquals(new Color(0, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED);
+        assertEquals(new Color(255, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH);
+    }
+
+    @Test
+    @DisplayName("Item 1.5: ThemeManager HoD OIA category colors match specification")
+    public void testThemeManagerOiaColors() {
+        // oSI: Status / System Available -> CUSTOMBLUE (120, 144, 240)
+        assertEquals(new Color(120, 144, 240), ThemeManager.getOiaStatusSysAvail());
+
+        // oII: Input Inhibited / X SYSTEM -> White (255, 255, 255)
+        assertEquals(new Color(255, 255, 255), ThemeManager.getOiaInputInhibited());
+
+        // oAI: Attention / Reminders / Message Waiting -> Yellow (255, 255, 0)
+        assertEquals(new Color(255, 255, 0), ThemeManager.getOiaAttention());
+
+        // oEI: Error Checks / Comm Check -> Red (255, 0, 0)
+        assertEquals(new Color(255, 0, 0), ThemeManager.getOiaCommCheck());
+
+        // oOB: OIA Separator / Background -> Black (0, 0, 0)
+        assertEquals(new Color(0, 0, 0), ThemeManager.getOiaBackground());
+    }
+
+    @Test
+    @DisplayName("Item 1.5: TerminalPanel Crosshair Ruler and Cursor Style controls")
+    public void testTerminalPanelRulerAndCursorStyle() {
+        try {
+            TerminalPanel panel = new TerminalPanel();
+
+            // Default cursor style is BLOCK
+            assertEquals(TerminalPanel.CursorStyle.BLOCK, panel.getCursorStyle());
+            panel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE);
+            assertEquals(TerminalPanel.CursorStyle.UNDERLINE, panel.getCursorStyle());
+
+            // Crosshair ruler toggling
+            assertFalse(panel.isCrosshairRulerEnabled());
+            panel.setCrosshairRulerEnabled(true);
+            assertTrue(panel.isCrosshairRulerEnabled());
+            panel.toggleCrosshairRuler();
+            assertFalse(panel.isCrosshairRulerEnabled());
+
+            // Wallpaper image setter
+            assertNull(panel.getWallpaperImage());
+        } catch (HeadlessException e) {
+            // Handled gracefully in headless CI environments
+        }
+    }
+
+    @Test
+    @DisplayName("Settings persistence for Crosshair Ruler and Cursor Style")
+    public void testSettingsPersistence() {
+        boolean originalRuler = Settings.getCrosshairRuler();
+        String originalCursor = Settings.getCursorStyle();
+
+        try {
+            Settings.setCrosshairRuler(true);
+            assertTrue(Settings.getCrosshairRuler());
+
+            Settings.setCrosshairRuler(false);
+            assertFalse(Settings.getCrosshairRuler());
+
+            Settings.setCursorStyle("UNDERLINE");
+            assertEquals("UNDERLINE", Settings.getCursorStyle());
+
+            Settings.setCursorStyle("BLOCK");
+            assertEquals("BLOCK", Settings.getCursorStyle());
+        } finally {
+            Settings.setCrosshairRuler(originalRuler);
+            Settings.setCursorStyle(originalCursor);
+        }
+    }
+}
diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/Phase2UiGraphicsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/Phase2UiGraphicsTest.java
new file mode 100644
index 0000000..491d167
--- /dev/null
+++ b/j3270/src/test/java/haus/nightmare/j3270/ui/Phase2UiGraphicsTest.java
@@ -0,0 +1,95 @@
+package haus.nightmare.j3270.ui;
+
+import haus.nightmare.lib3270j.graphics.GocaConstants;
+import haus.nightmare.lib3270j.graphics.GraphicsPlane;
+import haus.nightmare.lib3270j.graphics.HODWallpaper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.HeadlessException;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Verification test suite for Phase 2 UI Vector Graphics, Wallpaper, and Cursor Overlays.
+ */
+public class Phase2UiGraphicsTest {
+
+    @Test
+    @DisplayName("Test TerminalPanel Wallpaper Integration (Tile, Center, Stretch)")
+    public void testTerminalPanelWallpaperModes() {
+        try {
+            TerminalPanel panel = new TerminalPanel();
+            assertNull(panel.getHodWallpaper());
+            assertNull(panel.getWallpaperImage());
+
+            BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
+            panel.setWallpaperImage(img);
+            assertNotNull(panel.getWallpaperImage());
+            assertNotNull(panel.getHodWallpaper());
+            assertEquals(HODWallpaper.HOD_STRETCH, panel.getHodWallpaper().getDisplay());
+
+            panel.setWallpaperMode(HODWallpaper.HOD_TILE);
+            assertEquals(HODWallpaper.HOD_TILE, panel.getHodWallpaper().getDisplay());
+
+            panel.setWallpaperMode(HODWallpaper.HOD_CENTER);
+            assertEquals(HODWallpaper.HOD_CENTER, panel.getHodWallpaper().getDisplay());
+
+            HODWallpaper customWp = new HODWallpaper(img, HODWallpaper.HOD_TILE);
+            panel.setHodWallpaper(customWp);
+            assertSame(customWp, panel.getHodWallpaper());
+
+            // Clear wallpaper
+            panel.setWallpaperImage(null);
+            assertNull(panel.getHodWallpaper());
+            assertNull(panel.getWallpaperImage());
+        } catch (HeadlessException e) {
+            // Handled gracefully in headless environments
+        }
+    }
+
+    @Test
+    @DisplayName("Test Graphics Plane Cursor Synchronization and Rendering")
+    public void testGraphicsCursorOverlay() {
+        try {
+            TerminalPanel panel = new TerminalPanel();
+            panel.setSize(new Dimension(800, 600));
+            panel.setClient(new haus.nightmare.lib3270j.Telnet3270Client(new haus.nightmare.lib3270j.ConnectionConfig("localhost", 23)));
+
+            GraphicsPlane plane = panel.getClient() != null ? panel.getClient().getGraphicsPlane() : null;
+            if (plane != null) {
+                // Attach graphics cursor
+                plane.attachGraphicCursor(100, 100);
+                plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_CROSSHAIR);
+                assertTrue(plane.isGraphicCursorAttached());
+                assertEquals(100, plane.getGraphicCursorX());
+                assertEquals(100, plane.getGraphicCursorY());
+                assertEquals(1, plane.getHodCursorShape());
+
+                // Paint component to verify no exceptions during rendering
+                BufferedImage offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
+                Graphics2D g2 = offscreen.createGraphics();
+                panel.paint(g2);
+                g2.dispose();
+
+                // Change cursor shape to box
+                plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_BOX);
+                assertEquals(2, plane.getHodCursorShape());
+                offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
+                g2 = offscreen.createGraphics();
+                panel.paint(g2);
+                g2.dispose();
+
+                // Detach graphics cursor
+                plane.detachGraphicCursor();
+                assertFalse(plane.isGraphicCursorAttached());
+            }
+        } catch (HeadlessException e) {
+            // Handled gracefully in headless environments
+        }
+    }
+}
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java
index 237f1eb..3bc65a6 100644
--- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java
@@ -18,6 +18,16 @@ public class ConnectionConfig {
     private boolean tlsVerifyCert = true;
     private haus.nightmare.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
     private String sslProtocol = "TLS";
+    private String keyStorePath = null;
+    private String keyStorePassword = null;
+    private String keyStoreType = null;
+    private String keyStoreAlias = null;
+    private String trustStorePath = null;
+    private String trustStorePassword = null;
+    private String trustStoreType = null;
+    private ClassLoader customizedCAsClassLoader = null;
+    private java.util.List enabledProtocols = new java.util.ArrayList<>();
+    private java.util.List enabledCipherSuites = new java.util.ArrayList<>();
     private int connectTimeoutMs = 15000;
     private int nopIntervalSeconds = 0;
     private String terminalName = null; // override terminal type string
@@ -27,8 +37,8 @@ public class ConnectionConfig {
     private int soTimeoutMs = 0;
     private java.util.List luNames = new java.util.ArrayList<>();
     private boolean dynamicModel = false;
-    private int dynamicRows = 24;
-    private int dynamicCols = 80;
+    private int dynamicRows = 62;
+    private int dynamicCols = 160;
     private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
     private String codePage = "037";
     private String associatedPrinterLu = null;
@@ -81,7 +91,12 @@ public class ConnectionConfig {
     public void setPort(int port)             { this.port = port; }
 
     public TerminalModel getModel()           { return model; }
-    public void setModel(TerminalModel model) { this.model = model; }
+    public void setModel(TerminalModel model) {
+        this.model = model;
+        if (model != null && model.isDynamic()) {
+            this.dynamicModel = true;
+        }
+    }
 
     public String getLuName()                 { return luName; }
     public void setLuName(String luName)      { this.luName = luName; }
@@ -104,6 +119,52 @@ public class ConnectionConfig {
     public String getSslProtocol()            { return sslProtocol; }
     public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
 
+    public String getKeyStorePath() { return keyStorePath; }
+    public void setKeyStorePath(String path) { this.keyStorePath = path; }
+
+    public String getKeyStorePassword() { return keyStorePassword; }
+    public void setKeyStorePassword(String password) { this.keyStorePassword = password; }
+
+    public String getKeyStoreType() { return keyStoreType; }
+    public void setKeyStoreType(String type) { this.keyStoreType = type; }
+
+    public String getKeyStoreAlias() { return keyStoreAlias; }
+    public void setKeyStoreAlias(String alias) { this.keyStoreAlias = alias; }
+
+    public String getTrustStorePath() { return trustStorePath; }
+    public void setTrustStorePath(String path) { this.trustStorePath = path; }
+
+    public String getTrustStorePassword() { return trustStorePassword; }
+    public void setTrustStorePassword(String password) { this.trustStorePassword = password; }
+
+    public String getTrustStoreType() { return trustStoreType; }
+    public void setTrustStoreType(String type) { this.trustStoreType = type; }
+
+    public ClassLoader getCustomizedCAsClassLoader() { return customizedCAsClassLoader; }
+    public void setCustomizedCAsClassLoader(ClassLoader cl) { this.customizedCAsClassLoader = cl; }
+
+    public java.util.List getEnabledProtocols() { return enabledProtocols; }
+    public void setEnabledProtocols(java.util.List protocols) {
+        this.enabledProtocols = protocols != null ? new java.util.ArrayList<>(protocols) : new java.util.ArrayList<>();
+    }
+    public void setEnabledProtocols(String... protocols) {
+        this.enabledProtocols = new java.util.ArrayList<>();
+        if (protocols != null) {
+            for (String p : protocols) if (p != null) this.enabledProtocols.add(p);
+        }
+    }
+
+    public java.util.List getEnabledCipherSuites() { return enabledCipherSuites; }
+    public void setEnabledCipherSuites(java.util.List cipherSuites) {
+        this.enabledCipherSuites = cipherSuites != null ? new java.util.ArrayList<>(cipherSuites) : new java.util.ArrayList<>();
+    }
+    public void setEnabledCipherSuites(String... cipherSuites) {
+        this.enabledCipherSuites = new java.util.ArrayList<>();
+        if (cipherSuites != null) {
+            for (String c : cipherSuites) if (c != null) this.enabledCipherSuites.add(c);
+        }
+    }
+
     public int getConnectTimeoutMs()          { return connectTimeoutMs; }
     public void setConnectTimeoutMs(int ms)   { this.connectTimeoutMs = ms; }
 
@@ -145,15 +206,30 @@ public class ConnectionConfig {
         }
     }
 
-    public boolean isDynamicModel()           { return dynamicModel; }
-    public void setDynamicModel(boolean dynamicModel) { this.dynamicModel = dynamicModel; }
+    public boolean isDynamicModel() {
+        return dynamicModel || (model != null && model.isDynamic());
+    }
+
+    public void setDynamicModel(boolean dynamicModel) {
+        this.dynamicModel = dynamicModel;
+        if (dynamicModel && (model == null || !model.isDynamic())) {
+            this.model = TerminalModel.IBM_DYNAMIC;
+        } else if (!dynamicModel && model != null && model.isDynamic()) {
+            this.model = TerminalModel.IBM_3279_4;
+        }
+    }
+
+    public void setDynamic(boolean dynamic) {
+        setDynamicModel(dynamic);
+    }
 
     public int getDynamicRows()               { return dynamicRows; }
     public int getDynamicCols()               { return dynamicCols; }
     public void setDynamicDimensions(int rows, int cols) {
         this.dynamicModel = true;
-        this.dynamicRows = rows;
-        this.dynamicCols = cols;
+        this.dynamicRows = Math.max(1, rows);
+        this.dynamicCols = Math.max(1, cols);
+        this.model = TerminalModel.IBM_DYNAMIC;
     }
 
     public ProxyType getProxyType() { return proxyType; }
@@ -216,6 +292,9 @@ public class ConnectionConfig {
         String s = hostStr.trim();
         boolean tls = false;
         boolean tn3270e = true;
+        boolean dynamic = false;
+        int dynRows = 62;
+        int dynCols = 160;
 
         // Parse --proxy= or -proxy= flags
         ProxyType pType = ProxyType.NONE;
@@ -287,6 +366,29 @@ public class ConnectionConfig {
                 int colon = s.indexOf(':');
                 s = s.substring(colon + 1);
                 prefixFound = true;
+            } else if (s.startsWith("D:") || s.startsWith("d:")) {
+                dynamic = true;
+                s = s.substring(2);
+                prefixFound = true;
+            } else if (s.toLowerCase().startsWith("dyn:") || s.toLowerCase().startsWith("dynamic:") ||
+                       s.toLowerCase().startsWith("dyn[") || s.toLowerCase().startsWith("dynamic[")) {
+                int colon = s.indexOf(':');
+                if (colon > 0) {
+                    String prefix = s.substring(0, colon);
+                    s = s.substring(colon + 1);
+                    prefixFound = true;
+                    dynamic = true;
+                    if (prefix.contains("[") && prefix.contains("]")) {
+                        String dim = prefix.substring(prefix.indexOf('[') + 1, prefix.indexOf(']'));
+                        String[] parts = dim.toLowerCase().split("x");
+                        if (parts.length == 2) {
+                            try {
+                                dynRows = Integer.parseInt(parts[0].trim());
+                                dynCols = Integer.parseInt(parts[1].trim());
+                            } catch (NumberFormatException ignored) {}
+                        }
+                    }
+                }
             }
         }
 
@@ -315,6 +417,9 @@ public class ConnectionConfig {
         ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
         config.setUseTls(tls);
         config.setTn3270eEnabled(tn3270e);
+        if (dynamic) {
+            config.setDynamicDimensions(dynRows, dynCols);
+        }
         if (pType != ProxyType.NONE && pHost != null) {
             config.setProxy(pType, pHost, pPort, pUser, pPass);
         }
@@ -328,7 +433,7 @@ public class ConnectionConfig {
         if (terminalName != null) {
             return terminalName;
         }
-        if (dynamicModel) {
+        if (isDynamicModel()) {
             return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
         }
         return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java
index e235d1f..aefe639 100644
--- a/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java
@@ -47,7 +47,16 @@ public class Telnet3270Client {
     public Telnet3270Client(ConnectionConfig config) {
         this.config = config;
         this.translator = new EbcdicTranslator(config.getCodePage());
-        this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
+        if (config.isDynamicModel() || (config.getModel() != null && config.getModel().isDynamic())) {
+            this.screenBuffer = new ScreenBuffer(
+                    haus.nightmare.lib3270j.protocol.DS3270Constants.MODEL_2_ROWS,
+                    haus.nightmare.lib3270j.protocol.DS3270Constants.MODEL_2_COLS,
+                    config.getDynamicRows(),
+                    config.getDynamicCols(),
+                    translator);
+        } else {
+            this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
+        }
         this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
         this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
         this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
@@ -244,6 +253,9 @@ public class Telnet3270Client {
     /** Get the data stream processor. */
     public DataStreamProcessor getDataStreamProcessor() { return dsProcessor; }
 
+    /** Get the underlying telnet connection. */
+    public TelnetConnection getConnection() { return connection; }
+
     /** Get the connection config. */
     public ConnectionConfig getConfig() { return config; }
 
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java
index 33b76d5..8cb26d1 100644
--- a/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java
@@ -14,7 +14,8 @@ public enum TerminalModel {
     IBM_3279_2(2, true,  MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
     IBM_3279_3(3, true,  MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
     IBM_3279_4(4, true,  MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
-    IBM_3279_5(5, true,  MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS);
+    IBM_3279_5(5, true,  MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS),
+    IBM_DYNAMIC(0, true, MODEL_2_ROWS, MODEL_2_COLS, 62, 160);
 
     private final int modelNumber;
     private final boolean color;
@@ -40,12 +41,17 @@ public enum TerminalModel {
     public int getDefaultCols()    { return defaultCols; }
     public int getAlternateRows()  { return alternateRows; }
     public int getAlternateCols()  { return alternateCols; }
+    public boolean isDynamic()     { return modelNumber == 0; }
 
     /**
      * Returns the terminal type string for TN3270E negotiation.
-     * e.g., "IBM-3279-4-E" for a color model 4 with extended data stream.
+     * e.g., "IBM-3279-4-E" for a color model 4 with extended data stream,
+     * or "IBM-DYNAMIC-E" for dynamic model.
      */
     public String getTerminalType() {
+        if (modelNumber == 0) {
+            return "IBM-DYNAMIC-E";
+        }
         return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
     }
 
@@ -53,6 +59,9 @@ public enum TerminalModel {
      * Returns the base terminal type without "-E" suffix (for non-extended mode).
      */
     public String getBaseTerminalType() {
+        if (modelNumber == 0) {
+            return "IBM-DYNAMIC";
+        }
         return String.format("IBM-327%c-%d", color ? '9' : '8', modelNumber);
     }
 
@@ -60,6 +69,9 @@ public enum TerminalModel {
      * Look up a model by number and color mode.
      */
     public static TerminalModel forModel(int number, boolean isColor) {
+        if (number == 0) {
+            return IBM_DYNAMIC;
+        }
         for (TerminalModel m : values()) {
             if (m.modelNumber == number && m.color == isColor) {
                 return m;
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePage.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePage.java
index 73ad1ee..3184e64 100644
--- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePage.java
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePage.java
@@ -69,4 +69,88 @@ public interface CodePage {
      * Returns -1 if unmappable.
      */
     int unicodeToDbcs(char unicode);
+
+    /**
+     * Convert an EBCDIC byte buffer to a char array matching IBM HoD conversion.
+     */
+    default char[] convBuffByte2Char(byte[] buf, int offset, int length) {
+        if (buf == null || length <= 0) return new char[0];
+        char[] out = new char[length];
+        for (int i = 0; i < length; i++) {
+            out[i] = ebcdicToUnicode(buf[offset + i] & 0xFF);
+        }
+        return out;
+    }
+
+    /**
+     * Convert a char array to an EBCDIC byte array matching IBM HoD conversion.
+     */
+    default byte[] convBuffChar2Byte(char[] buf, int offset, int length) {
+        if (buf == null || length <= 0) return new byte[0];
+        byte[] out = new byte[length];
+        for (int i = 0; i < length; i++) {
+            out[i] = unicodeToEbcdicSafe(buf[offset + i]);
+        }
+        return out;
+    }
+
+    /**
+     * Get a HODByteToCharConverter instance backed by this CodePage.
+     */
+    default haus.nightmare.lib3270j.converters.HODByteToCharConverter getByteToCharConverter() {
+        if (isDBCS()) {
+            return new haus.nightmare.lib3270j.converters.ByteToCharDBCS_EBCDIC(this);
+        } else {
+            return new haus.nightmare.lib3270j.converters.ByteToCharSingleByte(this);
+        }
+    }
+
+    /**
+     * Get a HODCharToByteConverter instance backed by this CodePage.
+     */
+    default haus.nightmare.lib3270j.converters.HODCharToByteConverter getCharToByteConverter() {
+        if (isDBCS()) {
+            return new haus.nightmare.lib3270j.converters.CharToByteDBCS_EBCDIC(this);
+        } else {
+            return new haus.nightmare.lib3270j.converters.CharToByteSingleByte(this);
+        }
+    }
+
+    /**
+     * Helper to test if a pair of characters forms a Unicode surrogate pair.
+     */
+    static boolean isSurrogate(char high, char low) {
+        return Character.isSurrogatePair(high, low);
+    }
+
+    /**
+     * Helper to test if a character is a high surrogate.
+     */
+    static boolean isHighSurrogate(char c) {
+        return Character.isHighSurrogate(c);
+    }
+
+    /**
+     * Helper to test if a character is a low surrogate.
+     */
+    static boolean isLowSurrogate(char c) {
+        return Character.isLowSurrogate(c);
+    }
+
+    /**
+     * Helper matching IBM HoD CodePage.ComposeChar to combine characters.
+     */
+    static boolean ComposeChar(char[] chars) {
+        if (chars == null || chars.length < 2) return false;
+        if (chars[1] >= '\u0300' && chars[1] <= '\u036F') {
+            String decomposed = new String(chars, 0, 2);
+            String normalized = java.text.Normalizer.normalize(decomposed, java.text.Normalizer.Form.NFC);
+            if (normalized.length() == 1) {
+                chars[0] = normalized.charAt(0);
+                return true;
+            }
+        }
+        return false;
+    }
 }
+
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java
index 7582986..6f6c403 100644
--- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java
@@ -157,7 +157,25 @@ public class CodePageRegistry {
 
     public static String normalizeKey(String name) {
         if (name == null) return "";
-        String s = name.trim().toLowerCase();
+        String s = name.trim();
+        // Strip package qualifiers if present (e.g. com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp273)
+        int lastDot = Math.max(s.lastIndexOf('.'), s.lastIndexOf('/'));
+        if (lastDot >= 0 && lastDot < s.length() - 1) {
+            s = s.substring(lastDot + 1);
+        }
+
+        // Strip HoD converter class prefixes
+        for (String pfx : new String[]{
+                "HODByteToChar", "HODCharToByte", "ByteToChar", "CharToByte",
+                "ConverterBIDIPrinter", "ConverterFT", "ConverterJDK", "ConverterVT", "PrtConverter"
+        }) {
+            if (s.startsWith(pfx)) {
+                s = s.substring(pfx.length());
+                break;
+            }
+        }
+
+        s = s.toLowerCase();
         s = s.replace("_", "").replace("-", "");
         if (s.startsWith("ebcdiccp")) {
             s = s.substring(8);
@@ -174,13 +192,12 @@ public class CodePageRegistry {
     }
 
     /**
-     * Look up a code page by ID or alias.
-     * If not found in built-ins, attempts to load via java.nio.charset.Charset.
-     * Falls back to CP037 if completely unresolvable.
+     * Look up a code page by ID, alias, or converter name without fallback to CP037.
+     * Returns null if unresolvable.
      */
-    public static CodePage getCodePage(String name) {
+    public static CodePage resolveCodePage(String name) {
         if (name == null || name.trim().isEmpty()) {
-            return CODE_PAGES.get("037");
+            return null;
         }
 
         String raw = name.trim();
@@ -198,6 +215,9 @@ public class CodePageRegistry {
         if (targetId != null) {
             cp = CODE_PAGES.get(targetId);
             if (cp != null) return cp;
+            String normTarget = normalizeKey(targetId);
+            cp = CODE_PAGES.get(normTarget);
+            if (cp != null) return cp;
         }
 
         // Try standard NIO Charset dynamic adapter
@@ -205,18 +225,55 @@ public class CodePageRegistry {
             if (Charset.isSupported(raw)) {
                 return new NioCodePageAdapter(raw);
             }
+            if (Charset.isSupported(norm)) {
+                return new NioCodePageAdapter(norm);
+            }
             String ibmName = "IBM" + norm;
             if (Charset.isSupported(ibmName)) {
                 return new NioCodePageAdapter(ibmName);
             }
+            String ibmDashName = "IBM-" + norm;
+            if (Charset.isSupported(ibmDashName)) {
+                return new NioCodePageAdapter(ibmDashName);
+            }
             String cpName = "Cp" + norm;
             if (Charset.isSupported(cpName)) {
                 return new NioCodePageAdapter(cpName);
             }
+            String isoName = "ISO-8859-" + norm.replace("8859", "");
+            if (Charset.isSupported(isoName)) {
+                return new NioCodePageAdapter(isoName);
+            }
+            String winName = "windows-" + norm;
+            if (Charset.isSupported(winName)) {
+                return new NioCodePageAdapter(winName);
+            }
         } catch (Exception e) {
             log.fine("Dynamic charset loading failed for " + name + ": " + e.getMessage());
         }
 
+        return null;
+    }
+
+    /**
+     * Look up a code page by ID or alias.
+     * If not found in built-ins, attempts to load via java.nio.charset.Charset.
+     * Falls back to CP037 if completely unresolvable.
+     */
+    public static CodePage get(String name) {
+        return getCodePage(name);
+    }
+
+    public static CodePage getDefault() {
+        return getCodePage("037");
+    }
+
+    public static CodePage getCodePage(String name) {
+        CodePage cp = resolveCodePage(name);
+        if (cp != null) {
+            return cp;
+        }
+
         log.warning("CodePage not recognized: '" + name + "'; falling back to CP037");
         return CODE_PAGES.get("037");
     }
@@ -301,18 +358,82 @@ public class CodePageRegistry {
         addAlias("chinese-ext-traditional", "1371");
         addAlias("zh-traditional-ext", "1371");
         addAlias("zh-tw-ext", "1371");
+
+        // HoD Converter aliases mapping all HoD converter names to CodePage / Charset
+        addAlias("1390", "930");
+        addAlias("1390jis2004", "930");
+        addAlias("1399", "939");
+        addAlias("1399jis2004", "939");
+        addAlias("937macau", "937");
+        addAlias("1364", "933");
+        addAlias("1379", "937");
+        addAlias("274", "500");
+        addAlias("275", "037");
+        addAlias("924", "1047");
+        addAlias("1153", "870");
+        addAlias("1156", "1025");
+        addAlias("1157", "1025");
+        addAlias("1158", "1025");
+        addAlias("1166", "1025");
+        addAlias("1112", "1025");
+        addAlias("1122", "1025");
+        addAlias("1137", "037");
+        addAlias("1008", "420");
+        addAlias("449", "420");
+        addAlias("1089", "420");
+        addAlias("1134", "424");
+        addAlias("1349", "424");
+        addAlias("8585", "875");
+        addAlias("8586", "875");
+        addAlias("220", "284");
+        addAlias("big5550", "937");
+        addAlias("cns", "937");
+        addAlias("tca", "937");
+        addAlias("ks25550", "933");
+        addAlias("jis", "930");
+        addAlias("euc", "937");
+        addAlias("singlebyte", "037");
+        addAlias("dbcsebcdic", "930");
+        addAlias("dbcsebcdicnibm", "930");
+        addAlias("dbcsebcdicibm", "930");
+        addAlias("dbcsascii", "930");
+        addAlias("encodings", "037");
+        addAlias("1011", "037");
+        addAlias("1012", "037");
+        addAlias("1020", "037");
+        addAlias("1021", "037");
+        addAlias("1023", "037");
+        addAlias("1090", "037");
+        addAlias("1101", "037");
+        addAlias("1102", "037");
+        addAlias("1103", "037");
+        addAlias("1104", "037");
+        addAlias("1105", "037");
+        addAlias("1106", "037");
     }
 
     /**
-     * Check if a code page is registered.
+     * Check if a code page or converter is registered or resolvable.
      */
     public static boolean hasCodePage(String name) {
         if (name == null || name.trim().isEmpty()) return false;
-        String raw = name.trim();
-        if (CODE_PAGES.containsKey(raw)) return true;
-        String norm = normalizeKey(raw);
-        if (CODE_PAGES.containsKey(norm)) return true;
-        return ALIASES.containsKey(norm);
+        return resolveCodePage(name) != null;
+    }
+
+    /**
+     * Convenience factory method to get a HODByteToCharConverter by name or alias.
+     */
+    public static haus.nightmare.lib3270j.converters.HODByteToCharConverter getByteToCharConverter(String name)
+            throws haus.nightmare.lib3270j.converters.HODUnsupportedCodepageException {
+        return haus.nightmare.lib3270j.converters.HODByteToCharConverter.getHODConverter(name);
+    }
+
+    /**
+     * Convenience factory method to get a HODCharToByteConverter by name or alias.
+     */
+    public static haus.nightmare.lib3270j.converters.HODCharToByteConverter getCharToByteConverter(String name)
+            throws haus.nightmare.lib3270j.converters.HODUnsupportedCodepageException {
+        return haus.nightmare.lib3270j.converters.HODCharToByteConverter.getHODConverter(name);
     }
 
     /**
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/ByteToCharDBCS_EBCDIC.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/ByteToCharDBCS_EBCDIC.java
new file mode 100644
index 0000000..df7e184
--- /dev/null
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/ByteToCharDBCS_EBCDIC.java
@@ -0,0 +1,178 @@
+package haus.nightmare.lib3270j.converters;
+
+import haus.nightmare.lib3270j.charset.CodePage;
+import haus.nightmare.lib3270j.charset.CodePageRegistry;
+
+/**
+ * Concrete converter implementing mixed SBCS/DBCS byte-to-char conversion with transparent
+ * Shift-Out (0x0E) and Shift-In (0x0F) state management.
+ * Conforms to com.ibm.eNetwork.HOD.converters.ByteToCharDBCS_EBCDIC.
+ */
+public class ByteToCharDBCS_EBCDIC extends HODByteToCharConverter {
+
+    public ByteToCharDBCS_EBCDIC() {
+        this(CodePageRegistry.getCodePage("930"));
+    }
+
+    public ByteToCharDBCS_EBCDIC(CodePage codePage) {
+        super(codePage != null ? codePage : CodePageRegistry.getCodePage("930"));
+    }
+
+    @Override
+    public int convert(byte[] in, int inOff, int inEnd, char[] out, int outOff, int outEnd)
+            throws HODCharConversionException {
+        if (in == null || inEnd <= inOff) {
+            return 0;
+        }
+        if (out == null) {
+            throw new HODCharConversionException("Output buffer cannot be null");
+        }
+
+        this.byteOff = inOff;
+        this.charOff = outOff;
+
+        while (this.byteOff < inEnd || this.savedBytePresent) {
+            int b;
+            boolean isSaved = false;
+            if (this.savedBytePresent) {
+                b = this.savedByte & 0xFF;
+                this.savedBytePresent = false;
+                isSaved = true;
+            } else {
+                b = in[this.byteOff] & 0xFF;
+            }
+
+            // Handle Shift-Out (0x0E) -> DBCS mode
+            if (b == SO) {
+                this.currentState = 1;
+                if (this.preserveSOSI) {
+                    if (this.charOff >= outEnd) {
+                        throw new HODCharConversionException("Output buffer overflow writing SO at " + this.charOff);
+                    }
+                    out[this.charOff++] = (char) SO;
+                }
+                if (!isSaved) this.byteOff++;
+                continue;
+            }
+
+            // Handle Shift-In (0x0F) -> SBCS mode
+            if (b == SI) {
+                this.currentState = 0;
+                if (this.preserveSOSI) {
+                    if (this.charOff >= outEnd) {
+                        throw new HODCharConversionException("Output buffer overflow writing SI at " + this.charOff);
+                    }
+                    out[this.charOff++] = (char) SI;
+                }
+                if (!isSaved) this.byteOff++;
+                continue;
+            }
+
+            // SBCS mode conversion
+            if (this.currentState == 0) {
+                if (this.charOff >= outEnd) {
+                    throw new HODCharConversionException("Output buffer overflow at position " + this.charOff);
+                }
+                char c = this.codePage.ebcdicToUnicode(b);
+                if (c == '\uFFFD') {
+                    if (this.subMode) {
+                        c = this.subChars[0];
+                    } else {
+                        this.badInputLength = 1;
+                        throw new HODCharConversionException("Unmappable byte 0x" + Integer.toHexString(b) + " at index " + this.byteOff);
+                    }
+                }
+                out[this.charOff++] = c;
+                if (!isSaved) this.byteOff++;
+            } else {
+                // DBCS mode conversion (requires 2 bytes)
+                int b2;
+                if (isSaved) {
+                    if (this.byteOff >= inEnd) {
+                        // Incomplete DBCS pair at end of buffer
+                        this.savedByte = (byte) b;
+                        this.savedBytePresent = true;
+                        break;
+                    }
+                    b2 = in[this.byteOff++] & 0xFF;
+                } else {
+                    if (this.byteOff + 1 >= inEnd) {
+                        // Trailing single byte inside DBCS shift
+                        this.savedByte = (byte) b;
+                        this.savedBytePresent = true;
+                        this.byteOff++;
+                        break;
+                    }
+                    b2 = in[this.byteOff + 1] & 0xFF;
+                    this.byteOff += 2;
+                }
+
+                // Check for premature Shift-In
+                if (b2 == SI) {
+                    this.currentState = 0;
+                    if (this.charOff >= outEnd) {
+                        throw new HODCharConversionException("Output buffer overflow at position " + this.charOff);
+                    }
+                    out[this.charOff++] = this.codePage.ebcdicToUnicode(b);
+                    if (this.preserveSOSI) {
+                        if (this.charOff >= outEnd) {
+                            throw new HODCharConversionException("Output buffer overflow at position " + this.charOff);
+                        }
+                        out[this.charOff++] = (char) SI;
+                    }
+                    continue;
+                }
+
+                char c = this.codePage.dbcsToUnicode(b, b2);
+                if (c == '?' || c == '\uFFFD') {
+                    if (this.subMode) {
+                        c = this.subChars[0];
+                    } else {
+                        this.badInputLength = 2;
+                        throw new HODCharConversionException("Unmappable DBCS pair 0x" + Integer.toHexString(b) + ", 0x" + Integer.toHexString(b2));
+                    }
+                }
+
+                if (this.charOff >= outEnd) {
+                    throw new HODCharConversionException("Output buffer overflow at position " + this.charOff);
+                }
+                out[this.charOff++] = c;
+            }
+        }
+
+        return this.charOff - outOff;
+    }
+
+    @Override
+    public int flush(char[] out, int outOff, int outEnd) throws HODCharConversionException {
+        int count = 0;
+        if (this.savedBytePresent) {
+            if (!this.subMode) {
+                reset();
+                this.badInputLength = 1;
+                throw new HODCharConversionException("Unclosed trailing DBCS byte at end of input");
+            }
+            if (outOff < outEnd) {
+                out[outOff] = this.subChars[0];
+                count = 1;
+            }
+        }
+        reset();
+        return count;
+    }
+
+    @Override
+    public void reset() {
+        this.byteOff = 0;
+        this.charOff = 0;
+        this.currentState = 0;
+        this.savedBytePresent = false;
+        this.savedByte = 0;
+        this.badInputLength = 0;
+    }
+
+    @Override
+    public String getCharacterEncoding() {
+        return this.codePage != null ? "Cp" + this.codePage.getCodePageId() : "DBCS_EBCDIC";
+    }
+}
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/ByteToCharSingleByte.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/ByteToCharSingleByte.java
new file mode 100644
index 0000000..fed7695
--- /dev/null
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/ByteToCharSingleByte.java
@@ -0,0 +1,74 @@
+package haus.nightmare.lib3270j.converters;
+
+import haus.nightmare.lib3270j.charset.CodePage;
+import haus.nightmare.lib3270j.charset.CodePageRegistry;
+
+/**
+ * Concrete converter implementing single-byte character set (SBCS) byte-to-char conversion.
+ * Conforms to com.ibm.eNetwork.HOD.converters.ByteToCharSingleByte.
+ */
+public class ByteToCharSingleByte extends HODByteToCharConverter {
+
+    public ByteToCharSingleByte() {
+        this(CodePageRegistry.getDefault());
+    }
+
+    public ByteToCharSingleByte(CodePage codePage) {
+        super(codePage != null ? codePage : CodePageRegistry.getDefault());
+    }
+
+    @Override
+    public int convert(byte[] in, int inOff, int inEnd, char[] out, int outOff, int outEnd)
+            throws HODCharConversionException {
+        if (in == null || inEnd <= inOff) {
+            return 0;
+        }
+        if (out == null) {
+            throw new HODCharConversionException("Output buffer cannot be null");
+        }
+
+        this.byteOff = inOff;
+        this.charOff = outOff;
+
+        while (this.byteOff < inEnd) {
+            if (this.charOff >= outEnd) {
+                throw new HODCharConversionException("Output char buffer overflow at position " + this.charOff);
+            }
+
+            int b = in[this.byteOff] & 0xFF;
+            char c = this.codePage.ebcdicToUnicode(b);
+
+            if (c == '\uFFFD') {
+                if (this.subMode) {
+                    c = this.subChars[0];
+                } else {
+                    this.badInputLength = 1;
+                    throw new HODCharConversionException("Unmappable byte 0x" + Integer.toHexString(b) + " at input index " + this.byteOff);
+                }
+            }
+
+            out[this.charOff++] = c;
+            this.byteOff++;
+        }
+
+        return this.charOff - outOff;
+    }
+
+    @Override
+    public int flush(char[] out, int outOff, int outEnd) {
+        reset();
+        return 0;
+    }
+
+    @Override
+    public void reset() {
+        this.byteOff = 0;
+        this.charOff = 0;
+        this.badInputLength = 0;
+    }
+
+    @Override
+    public String getCharacterEncoding() {
+        return this.codePage != null ? "Cp" + this.codePage.getCodePageId() : "SingleByte";
+    }
+}
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/CharToByteDBCS_EBCDIC.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/CharToByteDBCS_EBCDIC.java
new file mode 100644
index 0000000..ebd69a6
--- /dev/null
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/CharToByteDBCS_EBCDIC.java
@@ -0,0 +1,137 @@
+package haus.nightmare.lib3270j.converters;
+
+import haus.nightmare.lib3270j.charset.CodePage;
+import haus.nightmare.lib3270j.charset.CodePageRegistry;
+
+/**
+ * Concrete converter implementing mixed SBCS/DBCS char-to-byte conversion with transparent
+ * Shift-Out (0x0E) and Shift-In (0x0F) state generation.
+ * Conforms to com.ibm.eNetwork.HOD.converters.CharToByteDBCS_EBCDIC.
+ */
+public class CharToByteDBCS_EBCDIC extends HODCharToByteConverter {
+
+    public CharToByteDBCS_EBCDIC() {
+        this(CodePageRegistry.getCodePage("930"));
+    }
+
+    public CharToByteDBCS_EBCDIC(CodePage codePage) {
+        super(codePage != null ? codePage : CodePageRegistry.getCodePage("930"));
+    }
+
+    @Override
+    public int convert(char[] in, int inOff, int inEnd, byte[] out, int outOff, int outEnd)
+            throws HODCharConversionException {
+        if (in == null || inEnd <= inOff) {
+            return 0;
+        }
+        if (out == null) {
+            throw new HODCharConversionException("Output buffer cannot be null");
+        }
+
+        this.charOff = inOff;
+        this.byteOff = outOff;
+
+        while (this.charOff < inEnd) {
+            char c = in[this.charOff];
+
+            // Explicit Shift-Out control character handling
+            if (c == (char) SO) {
+                if (this.currentState == 0) {
+                    if (this.byteOff >= outEnd) {
+                        throw new HODCharConversionException("Output buffer overflow writing SO at " + this.byteOff);
+                    }
+                    out[this.byteOff++] = SO;
+                    this.currentState = 1;
+                }
+                this.charOff++;
+                continue;
+            }
+
+            // Explicit Shift-In control character handling
+            if (c == (char) SI) {
+                if (this.currentState == 1) {
+                    if (this.byteOff >= outEnd) {
+                        throw new HODCharConversionException("Output buffer overflow writing SI at " + this.byteOff);
+                    }
+                    out[this.byteOff++] = SI;
+                    this.currentState = 0;
+                }
+                this.charOff++;
+                continue;
+            }
+
+            int dbcs = this.codePage.unicodeToDbcs(c);
+
+            if (dbcs >= 0) {
+                // Character is DBCS: ensure in DBCS mode
+                int needed = (this.currentState == 0) ? 3 : 2;
+                if (this.byteOff + needed > outEnd) {
+                    throw new HODCharConversionException("Output buffer overflow writing DBCS character at " + this.byteOff);
+                }
+
+                if (this.currentState == 0) {
+                    out[this.byteOff++] = SO;
+                    this.currentState = 1;
+                }
+
+                out[this.byteOff++] = (byte) ((dbcs >> 8) & 0xFF);
+                out[this.byteOff++] = (byte) (dbcs & 0xFF);
+            } else {
+                // Character is SBCS: ensure in SBCS mode
+                int needed = (this.currentState == 1) ? 2 : 1;
+                if (this.byteOff + needed > outEnd) {
+                    throw new HODCharConversionException("Output buffer overflow writing SBCS character at " + this.byteOff);
+                }
+
+                if (this.currentState == 1) {
+                    out[this.byteOff++] = SI;
+                    this.currentState = 0;
+                }
+
+                int ebc = this.codePage.unicodeToEbcdic(c);
+                if (ebc < 0) {
+                    if (this.subMode) {
+                        out[this.byteOff++] = this.subBytes[0];
+                    } else {
+                        this.badInputLength = 1;
+                        throw new HODCharConversionException("Unmappable character '\\u" + Integer.toHexString(c) + "' at input index " + this.charOff);
+                    }
+                } else {
+                    out[this.byteOff++] = (byte) (ebc & 0xFF);
+                }
+            }
+
+            this.charOff++;
+        }
+
+        return this.byteOff - outOff;
+    }
+
+    @Override
+    public int flush(byte[] out, int outOff, int outEnd) throws HODCharConversionException {
+        int flushed = 0;
+        if (this.currentState == 1) {
+            if (outOff >= outEnd) {
+                throw new HODCharConversionException("Output buffer overflow during flush at " + outOff);
+            }
+            out[outOff] = SI;
+            flushed = 1;
+            this.currentState = 0;
+        }
+        reset();
+        return flushed;
+    }
+
+    @Override
+    public void reset() {
+        this.charOff = 0;
+        this.byteOff = 0;
+        this.currentState = 0;
+        this.badInputLength = 0;
+    }
+
+    @Override
+    public String getCharacterEncoding() {
+        return this.codePage != null ? "Cp" + this.codePage.getCodePageId() : "DBCS_EBCDIC";
+    }
+}
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/CharToByteSingleByte.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/CharToByteSingleByte.java
new file mode 100644
index 0000000..06d6b02
--- /dev/null
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/CharToByteSingleByte.java
@@ -0,0 +1,75 @@
+package haus.nightmare.lib3270j.converters;
+
+import haus.nightmare.lib3270j.charset.CodePage;
+import haus.nightmare.lib3270j.charset.CodePageRegistry;
+
+/**
+ * Concrete converter implementing single-byte character set (SBCS) char-to-byte conversion.
+ * Conforms to com.ibm.eNetwork.HOD.converters.CharToByteSingleByte.
+ */
+public class CharToByteSingleByte extends HODCharToByteConverter {
+
+    public CharToByteSingleByte() {
+        this(CodePageRegistry.getDefault());
+    }
+
+    public CharToByteSingleByte(CodePage codePage) {
+        super(codePage != null ? codePage : CodePageRegistry.getDefault());
+    }
+
+    @Override
+    public int convert(char[] in, int inOff, int inEnd, byte[] out, int outOff, int outEnd)
+            throws HODCharConversionException {
+        if (in == null || inEnd <= inOff) {
+            return 0;
+        }
+        if (out == null) {
+            throw new HODCharConversionException("Output buffer cannot be null");
+        }
+
+        this.charOff = inOff;
+        this.byteOff = outOff;
+
+        while (this.charOff < inEnd) {
+            if (this.byteOff >= outEnd) {
+                throw new HODCharConversionException("Output byte buffer overflow at position " + this.byteOff);
+            }
+
+            char c = in[this.charOff];
+            int ebc = this.codePage.unicodeToEbcdic(c);
+
+            if (ebc < 0) {
+                if (this.subMode) {
+                    out[this.byteOff++] = this.subBytes[0];
+                } else {
+                    this.badInputLength = 1;
+                    throw new HODCharConversionException("Unmappable character '\\u" + Integer.toHexString(c) + "' at input index " + this.charOff);
+                }
+            } else {
+                out[this.byteOff++] = (byte) (ebc & 0xFF);
+            }
+
+            this.charOff++;
+        }
+
+        return this.byteOff - outOff;
+    }
+
+    @Override
+    public int flush(byte[] out, int outOff, int outEnd) {
+        reset();
+        return 0;
+    }
+
+    @Override
+    public void reset() {
+        this.charOff = 0;
+        this.byteOff = 0;
+        this.badInputLength = 0;
+    }
+
+    @Override
+    public String getCharacterEncoding() {
+        return this.codePage != null ? "Cp" + this.codePage.getCodePageId() : "SingleByte";
+    }
+}
diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODByteToCharConverter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODByteToCharConverter.java
new file mode 100644
index 0000000..21b1303
--- /dev/null
+++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODByteToCharConverter.java
@@ -0,0 +1,184 @@
+package haus.nightmare.lib3270j.converters;
+
+import haus.nightmare.lib3270j.charset.CodePage;
+import haus.nightmare.lib3270j.charset.CodePageRegistry;
+
+import java.util.Objects;
+
+/**
+ * High-performance adapter and bridge converting byte streams (EBCDIC, ASCII, ISO, UTF)
+ * to Unicode characters conforming to IBM Host On-Demand (HoD) converter specifications.
+ * 

+ * Supports dynamic factory resolution for all 275 HoD converter class names and transparent + * Shift-In (0x0F) / Shift-Out (0x0E) DBCS transitions. + */ +public abstract class HODByteToCharConverter { + + public static final int SO = 0x0E; + public static final int SI = 0x0F; + + protected int byteOff = 0; + protected int charOff = 0; + protected int badInputLength = 0; + protected boolean subMode = true; + protected char[] subChars = new char[]{'\uFFFD'}; + protected CodePage codePage; + + // DBCS State + protected int currentState = 0; // 0 = SBCS, 1 = DBCS + protected boolean savedBytePresent = false; + protected byte savedByte = 0; + protected boolean preserveSOSI = false; + + public HODByteToCharConverter() { + } + + public HODByteToCharConverter(CodePage codePage) { + this.codePage = Objects.requireNonNull(codePage, "codePage cannot be null"); + } + + /** + * Look up and instantiate a ByteToChar converter matching the given encoding, + * codepage ID, alias, or HoD converter class name (e.g. "Cp037", "037", "ByteToCharCp1047", "ConverterFT1047"). + * + * @param encoding encoding identifier or class name + * @return initialized HODByteToCharConverter instance + * @throws HODUnsupportedCodepageException if the codepage cannot be resolved + */ + public static HODByteToCharConverter getHODConverter(String encoding) throws HODUnsupportedCodepageException { + if (encoding == null || encoding.trim().isEmpty()) { + throw new HODUnsupportedCodepageException("Encoding name cannot be null or empty"); + } + + CodePage cp = CodePageRegistry.resolveCodePage(encoding); + if (cp == null) { + throw new HODUnsupportedCodepageException("Unsupported codepage / converter: " + encoding); + } + + if (cp.isDBCS()) { + return new ByteToCharDBCS_EBCDIC(cp); + } else { + return new ByteToCharSingleByte(cp); + } + } + + /** + * Standard converter lookup alias conforming to Java/HoD converter factory patterns. + */ + public static HODByteToCharConverter getConverter(String encoding) throws HODUnsupportedCodepageException { + return getHODConverter(encoding); + } + + /** + * Convert an array of bytes into an array of characters. + * + * @param in source byte buffer + * @param inOff start offset in input buffer + * @param inEnd end offset in input buffer (exclusive) + * @param out destination char buffer + * @param outOff start offset in output buffer + * @param outEnd end offset in output buffer (exclusive) + * @return number of characters converted and written into out + * @throws HODCharConversionException if an unmappable byte is encountered with substitution disabled, + * or if the output buffer overflows + */ + public abstract int convert(byte[] in, int inOff, int inEnd, char[] out, int outOff, int outEnd) + throws HODCharConversionException; + + /** + * Overload supporting an extra boolean flag matching HoD CFR decompiled signature. + */ + public int convert(byte[] in, int inOff, int inEnd, char[] out, int outOff, int outEnd, boolean bl) + throws HODCharConversionException { + return convert(in, inOff, inEnd, out, outOff, outEnd); + } + + /** + * Flush any buffered / trailing state into the output char buffer. + * + * @param out destination char buffer + * @param outOff start offset + * @param outEnd end offset + * @return number of characters flushed + * @throws HODCharConversionException if flush fails or trailing incomplete sequence cannot be converted + */ + public abstract int flush(char[] out, int outOff, int outEnd) throws HODCharConversionException; + + /** + * Reset converter state and offsets to defaults. + */ + public abstract void reset(); + + /** + * Get the canonical character encoding name (e.g. "Cp037", "Cp930"). + */ + public abstract String getCharacterEncoding(); + + /** + * Convert an entire byte array to a char array matching IBM HoD's hodConvertAll API. + */ + public char[] hodConvertAll(byte[] in) throws HODCharConversionException { + if (in == null) return new char[0]; + reset(); + char[] buf = new char[Math.max(16, in.length * 2 + 16)]; + int converted = convert(in, 0, in.length, buf, 0, buf.length); + int flushed = flush(buf, converted, buf.length); + int total = converted + flushed; + char[] result = new char[total]; + System.arraycopy(buf, 0, result, 0, total); + return result; + } + + /** + * Convenience alias for hodConvertAll. + */ + public char[] convertAll(byte[] in) throws HODCharConversionException { + return hodConvertAll(in); + } + + public void setSubstitutionMode(boolean mode) { + this.subMode = mode; + } + + public boolean getSubstitutionMode() { + return this.subMode; + } + + public void setSubstitutionChars(char[] subChars) { + if (subChars != null && subChars.length > 0) { + this.subChars = subChars; + } + } + + public char[] getSubstitutionChars() { + return this.subChars; + } + + public int getBadInputLength() { + return this.badInputLength; + } + + public int nextByteIndex() { + return this.byteOff; + } + + public int nextCharIndex() { + return this.charOff; + } + + public CodePage getCodePage() { + return this.codePage; + } + + public void setPreserveSOSI(boolean preserve) { + this.preserveSOSI = preserve; + } + + public boolean isPreserveSOSI() { + return this.preserveSOSI; + } + + public int getCurrentState() { + return this.currentState; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODCharConversionException.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODCharConversionException.java new file mode 100644 index 0000000..0551cd3 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODCharConversionException.java @@ -0,0 +1,20 @@ +package haus.nightmare.lib3270j.converters; + +import java.io.CharConversionException; + +/** + * Exception thrown when character conversion fails during Host On-Demand converter processing. + * Conforms to com.ibm.eNetwork.HOD.common.HODCharConversionException. + */ +public class HODCharConversionException extends CharConversionException { + + private static final long serialVersionUID = 1L; + + public HODCharConversionException() { + super(); + } + + public HODCharConversionException(String message) { + super(message); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODCharToByteConverter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODCharToByteConverter.java new file mode 100644 index 0000000..0790350 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODCharToByteConverter.java @@ -0,0 +1,169 @@ +package haus.nightmare.lib3270j.converters; + +import haus.nightmare.lib3270j.charset.CodePage; +import haus.nightmare.lib3270j.charset.CodePageRegistry; + +import java.util.Objects; + +/** + * High-performance adapter and bridge converting Unicode characters to byte streams + * (EBCDIC, ASCII, ISO, UTF) conforming to IBM Host On-Demand (HoD) converter specifications. + *

+ * Supports dynamic factory resolution for all 275 HoD converter class names and transparent + * Shift-In (0x0F) / Shift-Out (0x0E) DBCS transitions. + */ +public abstract class HODCharToByteConverter { + + public static final byte SO = 0x0E; + public static final byte SI = 0x0F; + + protected int byteOff = 0; + protected int charOff = 0; + protected int badInputLength = 0; + protected boolean subMode = true; + protected byte[] subBytes = new byte[]{(byte) 0x6F}; // 0x6F '?' in EBCDIC (or safe fallback) + protected CodePage codePage; + + // DBCS State + protected int currentState = 0; // 0 = SBCS, 1 = DBCS + + public HODCharToByteConverter() { + } + + public HODCharToByteConverter(CodePage codePage) { + this.codePage = Objects.requireNonNull(codePage, "codePage cannot be null"); + } + + /** + * Look up and instantiate a CharToByte converter matching the given encoding, + * codepage ID, alias, or HoD converter class name (e.g. "Cp037", "037", "CharToByteCp1047", "ConverterFT1047"). + * + * @param encoding encoding identifier or class name + * @return initialized HODCharToByteConverter instance + * @throws HODUnsupportedCodepageException if the codepage cannot be resolved + */ + public static HODCharToByteConverter getHODConverter(String encoding) throws HODUnsupportedCodepageException { + if (encoding == null || encoding.trim().isEmpty()) { + throw new HODUnsupportedCodepageException("Encoding name cannot be null or empty"); + } + + CodePage cp = CodePageRegistry.resolveCodePage(encoding); + if (cp == null) { + throw new HODUnsupportedCodepageException("Unsupported codepage / converter: " + encoding); + } + + if (cp.isDBCS()) { + return new CharToByteDBCS_EBCDIC(cp); + } else { + return new CharToByteSingleByte(cp); + } + } + + /** + * Standard converter lookup alias conforming to Java/HoD converter factory patterns. + */ + public static HODCharToByteConverter getConverter(String encoding) throws HODUnsupportedCodepageException { + return getHODConverter(encoding); + } + + /** + * Convert an array of characters into an array of bytes. + * + * @param in source char buffer + * @param inOff start offset in input buffer + * @param inEnd end offset in input buffer (exclusive) + * @param out destination byte buffer + * @param outOff start offset in output buffer + * @param outEnd end offset in output buffer (exclusive) + * @return number of bytes converted and written into out + * @throws HODCharConversionException if an unmappable char is encountered with substitution disabled, + * or if the output buffer overflows + */ + public abstract int convert(char[] in, int inOff, int inEnd, byte[] out, int outOff, int outEnd) + throws HODCharConversionException; + + /** + * Flush any buffered / trailing shift state into the output byte buffer. + * + * @param out destination byte buffer + * @param outOff start offset + * @param outEnd end offset + * @return number of bytes flushed + * @throws HODCharConversionException if output buffer overflows + */ + public abstract int flush(byte[] out, int outOff, int outEnd) throws HODCharConversionException; + + /** + * Reset converter state and offsets to defaults. + */ + public abstract void reset(); + + /** + * Get the canonical character encoding name (e.g. "Cp037", "Cp930"). + */ + public abstract String getCharacterEncoding(); + + /** + * Convert an entire char array to a byte array matching IBM HoD's hodConvertAll API. + */ + public byte[] hodConvertAll(char[] in) throws HODCharConversionException { + if (in == null) return new byte[0]; + reset(); + byte[] buf = new byte[Math.max(16, in.length * 3 + 16)]; + int converted = convert(in, 0, in.length, buf, 0, buf.length); + int flushed = flush(buf, converted, buf.length); + int total = converted + flushed; + byte[] result = new byte[total]; + System.arraycopy(buf, 0, result, 0, total); + return result; + } + + /** + * Convenience alias for hodConvertAll. + */ + public byte[] convertAll(char[] in) throws HODCharConversionException { + return hodConvertAll(in); + } + + public void setSubstitutionMode(boolean mode) { + this.subMode = mode; + } + + public boolean getSubstitutionMode() { + return this.subMode; + } + + public void setSubstitutionBytes(byte[] subBytes) { + if (subBytes != null && subBytes.length > 0) { + this.subBytes = subBytes; + } + } + + public byte[] getSubstitutionBytes() { + return this.subBytes; + } + + public int getBadInputLength() { + return this.badInputLength; + } + + public int nextByteIndex() { + return this.byteOff; + } + + public int nextCharIndex() { + return this.charOff; + } + + public int getMaxBytesPerChar() { + return (codePage != null && codePage.isDBCS()) ? 3 : 1; // At most 3 bytes (SO + 2-byte DBCS) + } + + public CodePage getCodePage() { + return this.codePage; + } + + public int getCurrentState() { + return this.currentState; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODUnsupportedCodepageException.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODUnsupportedCodepageException.java new file mode 100644 index 0000000..a7ca724 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/converters/HODUnsupportedCodepageException.java @@ -0,0 +1,20 @@ +package haus.nightmare.lib3270j.converters; + +import java.io.UnsupportedEncodingException; + +/** + * Exception thrown when a requested character encoding or codepage identifier cannot be resolved. + * Conforms to com.ibm.eNetwork.HOD.common.HODUnsupportedCodepageException. + */ +public class HODUnsupportedCodepageException extends UnsupportedEncodingException { + + private static final long serialVersionUID = 1L; + + public HODUnsupportedCodepageException() { + super(); + } + + public HODUnsupportedCodepageException(String encoding) { + super(encoding); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index ae6d525..d34518a 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -63,6 +63,14 @@ public class DataStreamProcessor { this.graphicsPlane.setProgramSymbolManager(programSymbolManager); } + public ScreenBuffer getScreen() { + return screen; + } + + public ScreenBuffer getScreenBuffer() { + return screen; + } + public QueryReplyBuilder getQueryReplyBuilder() { return qrBuilder; } @@ -81,6 +89,13 @@ public class DataStreamProcessor { public void setOutputSender(OutputSender sender) { this.outputSender = sender; + if (inputProcessor != null) { + inputProcessor.setOutputSender(sender); + } + } + + public OutputSender getOutputSender() { + return outputSender; } public void setFTDft(haus.nightmare.lib3270j.ft.FTDft ftDft) { @@ -99,6 +114,9 @@ public class DataStreamProcessor { public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) { this.inputProcessor = inputProcessor; + if (inputProcessor != null && outputSender != null) { + inputProcessor.setOutputSender(outputSender); + } } public haus.nightmare.lib3270j.input.InputProcessor getInputProcessor() { @@ -1516,4 +1534,233 @@ public class DataStreamProcessor { public void processNullStructuredField() { log.fine("Processed null structured field"); } + + // ========== Phase 3: HoD DS3270 Order & Data Stream Functions ========== + + /** Process Write Control Character (WCC). */ + public void processWCC(int wcc) { + boolean alarm = wccSoundAlarm(wcc); + boolean kbdRestore = wccKeyboardRestore(wcc); + boolean resetMdt = wccResetMDT(wcc); + + log.fine("processWCC: " + String.format("0x%02x", wcc) + + " reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt); + + if (kbdRestore && inputProcessor != null) { + inputProcessor.setKeyboardLocked(false); + } + if (resetMdt) { + resetAllMDT(); + } + if (wccReset(wcc)) { + log.fine("WCC reset: clearing default attributes"); + } + } + + public void processWCC(short wcc) { + processWCC(wcc & 0xFFFF); + } + + /** Process Set Buffer Address (SBA) order. */ + public void processSBA(int baddr) { + int size = screen.getRows() * screen.getCols(); + if (size > 0) { + screen.setBufferAddress(baddr % size); + } + } + + public void processSBA(int b1, int b2) { + processSBA(decodeAddress(b1, b2)); + } + + public void processSBA() { + // No-op or maintains current buffer address + } + + /** Process Start Field (SF) order. */ + public void processSF(byte fa) { + int size = screen.getRows() * screen.getCols(); + if (size <= 0) return; + int baddr = screen.getBufferAddress(); + ExtendedAttribute ea = screen.getCell(baddr); + ea.clear(); + ea.fa = (byte) (fa & FA_MASK); + ea.ec = 0; + ea.ucs4 = ' '; + screen.setFormatted(true); + screen.setBufferAddress((baddr + 1) % size); + } + + public void processSF() { + processSF((byte) FA_PRINTABLE); + } + + /** Process Start Field Extended (SFE) order. */ + public void processSFE(byte[] pairs) { + int size = screen.getRows() * screen.getCols(); + if (size <= 0) return; + int baddr = screen.getBufferAddress(); + ExtendedAttribute ea = screen.getCell(baddr); + ea.clear(); + ea.ec = 0; + ea.ucs4 = ' '; + if (pairs != null) { + for (int i = 0; i + 1 < pairs.length; i += 2) { + int attrType = pairs[i] & 0xFF; + int attrValue = pairs[i + 1] & 0xFF; + applyExtendedAttribute(ea, attrType, attrValue); + } + } + if (ea.fa == 0) { + ea.fa = (byte) FA_PRINTABLE; + } + screen.setFormatted(true); + screen.setBufferAddress((baddr + 1) % size); + } + + public void processSFE() { + processSFE(new byte[0]); + } + + /** Process Set Attribute (SA) order. */ + public void processSA(int attrType, int attrValue) { + int baddr = screen.getBufferAddress(); + ExtendedAttribute ea = screen.getCell(baddr); + applyExtendedAttribute(ea, attrType, attrValue); + } + + public void processSA() { + // Default attributes + } + + /** Process Modify Field (MF) order. */ + public void processMF(byte[] pairs) { + int baddr = screen.getBufferAddress(); + int faAddr = screen.findFieldAttribute(baddr); + if (faAddr >= 0 && pairs != null) { + ExtendedAttribute ea = screen.getCell(faAddr); + for (int i = 0; i + 1 < pairs.length; i += 2) { + int attrType = pairs[i] & 0xFF; + int attrValue = pairs[i + 1] & 0xFF; + applyExtendedAttribute(ea, attrType, attrValue); + } + } + } + + public void processMF() { + processMF(new byte[0]); + } + + /** Process Insert Cursor (IC) order. */ + public void processIC() { + screen.setCursorAddress(screen.getBufferAddress()); + } + + /** Process Program Tab (PT) order. */ + public void processPT() { + int baddr = screen.findNextUnprotected(screen.getBufferAddress()); + screen.setBufferAddress(baddr); + } + + /** Process Repeat to Address (RA) order. */ + public void processRA(int toAddr, int fillChar) { + int size = screen.getRows() * screen.getCols(); + if (size <= 0) return; + toAddr = ((toAddr % size) + size) % size; + int baddr = screen.getBufferAddress(); + char ucs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar & 0xFF) : (char) fillChar; + do { + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; + ea.ec = (byte) fillChar; + ea.ucs4 = ucs4; + baddr = (baddr + 1) % size; + } while (baddr != toAddr); + screen.setBufferAddress(baddr); + } + + public void processRA() { + processRA(0, 0); + } + + /** Process Erase Unprotected to Address (EUA) order. */ + public void processEUA(int toAddr) { + int size = screen.getRows() * screen.getCols(); + if (size <= 0) return; + toAddr = ((toAddr % size) + size) % size; + int baddr = screen.getBufferAddress(); + do { + ExtendedAttribute ea = screen.getCell(baddr); + if (!ea.isFieldAttribute()) { + int faAddr = screen.findFieldAttribute(baddr); + byte faVal = faAddr >= 0 ? screen.getCell(faAddr).fa : 0; + if (!faIsProtected(faVal & 0xFF)) { + ea.ec = 0; + ea.ucs4 = 0; + ea.fg = 0; + ea.bg = 0; + ea.gr = 0; + ea.cs = 0; + } + } + baddr = (baddr + 1) % size; + } while (baddr != toAddr); + screen.setBufferAddress(baddr); + } + + public void processEUA() { + processEUA(0); + } + + /** Process Graphic Escape (GE) order. */ + public void processGE(int geChar) { + int size = screen.getRows() * screen.getCols(); + if (size <= 0) return; + int baddr = screen.getBufferAddress(); + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; + ea.ec = (byte) geChar; + ea.cs = CS_GE; + ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar; + screen.setBufferAddress((baddr + 1) % size); + } + + public void processGE() { + processGE(0); + } + + /** Process Write Structured Field (WSF) from short buffer. */ + public void processWSF(short[] data, int off, int len) { + if (data == null || len <= 0) return; + byte[] bdata = new byte[len]; + for (int i = 0; i < len; i++) { + bdata[i] = (byte) (data[off + i] & 0xFF); + } + processWriteStructuredField(bdata, 0, len); + } + + public void processWSF(byte[] data, int off, int len) { + processWriteStructuredField(data, off, len); + } + + /** Process raw inbound data stream chunk (short[] representation). */ + public void processData(short[] data, int off, int len) { + if (data == null || len <= 0) return; + byte[] bdata = new byte[len]; + for (int i = 0; i < len; i++) { + bdata[i] = (byte) (data[off + i] & 0xFF); + } + processRecord(bdata, 0, len, true); + } + + public void processData(byte[] data, int off, int len) { + processRecord(data, off, len, true); + } + + /** Send AID key with explicit cursor address. */ + public void sendAid(short aid, int cursorAddress) { + if (inputProcessor != null) { + inputProcessor.sendAid(aid & 0xFFFF, cursorAddress); + } + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConnection.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConnection.java index 16e08e9..6493340 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConnection.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLConnection.java @@ -7,6 +7,22 @@ import haus.nightmare.lib3270j.Telnet3270Client; */ public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection { + public ECLConnection() { + super(); + } + + public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) { + super((haus.nightmare.lib3270j.ecl.ECLSession) session, session != null ? session.getClient() : null); + } + + public ECLConnection(String host, int port) { + super(host, port); + } + + public ECLConnection(java.util.Properties props) { + super(props); + } + public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session, Telnet3270Client client) { super(session, client); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLErr.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLErr.java new file mode 100644 index 0000000..825768b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLErr.java @@ -0,0 +1,33 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for 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 tag, String id, String text) { + super(tag, id, text); + } + + public ECLErr(String tag, String id, String text, String extra) { + super(tag, id, text, extra); + } + + public ECLErr(Throwable cause) { + super(cause); + } + + public ECLErr(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIA.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIA.java index bea2ab5..08aa01c 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIA.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIA.java @@ -12,4 +12,8 @@ public class ECLOIA extends haus.nightmare.lib3270j.ecl.ECLOIA { public ECLOIA(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { super(screen, inputProcessor, fsm); } + + public ECLOIA(haus.nightmare.lib3270j.eNetwork.ECL.ECLSession session) { + super(session); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIANotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIANotify.java new file mode 100644 index 0000000..6c28a96 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLOIANotify.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLOIANotify. + */ +public interface ECLOIANotify extends haus.nightmare.lib3270j.ecl.ECLOIANotify { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPS.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPS.java index 1115390..68ee8c6 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPS.java @@ -12,4 +12,8 @@ public class ECLPS extends haus.nightmare.lib3270j.ecl.ECLPS { public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) { super(screen, inputProcessor, translator); } + + public ECLPS(haus.nightmare.lib3270j.eNetwork.ECL.ECLSession session) { + super(session); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSBIDIServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSBIDIServices.java new file mode 100644 index 0000000..a149deb --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSBIDIServices.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLPSBIDIServices. + */ +public interface ECLPSBIDIServices extends haus.nightmare.lib3270j.ecl.ECLPSBIDIServices { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSGraphicsServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSGraphicsServices.java new file mode 100644 index 0000000..ffad6a1 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSGraphicsServices.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsServices. + */ +public interface ECLPSGraphicsServices extends haus.nightmare.lib3270j.ecl.ECLPSGraphicsServices { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSHindiServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSHindiServices.java new file mode 100644 index 0000000..fa75d93 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSHindiServices.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLPSHindiServices. + */ +public interface ECLPSHindiServices extends haus.nightmare.lib3270j.ecl.ECLPSHindiServices { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSTHAIServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSTHAIServices.java new file mode 100644 index 0000000..b6f15aa --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSTHAIServices.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLPSTHAIServices. + */ +public interface ECLPSTHAIServices extends haus.nightmare.lib3270j.ecl.ECLPSTHAIServices { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSUpdate.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSUpdate.java new file mode 100644 index 0000000..2a3725c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLPSUpdate.java @@ -0,0 +1,18 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLPSUpdate. + */ +public class ECLPSUpdate extends haus.nightmare.lib3270j.ecl.ECLPSUpdate { + + private static final long serialVersionUID = 1L; + + public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPS ps, int startRow, int startCol, + int endRow, int endCol, int start, int end, boolean fullUpdate, String text) { + super(ps, startRow, startCol, endRow, endCol, start, end, fullUpdate, text); + } + + public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean fullUpdate) { + super(startRow, startCol, endRow, endCol, fullUpdate); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenNotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenNotify.java new file mode 100644 index 0000000..16d3803 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenNotify.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLScreenNotify. + */ +public interface ECLScreenNotify extends haus.nightmare.lib3270j.ecl.ECLScreenNotify { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenReco.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenReco.java new file mode 100644 index 0000000..7e84a4e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenReco.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLScreenReco. + */ +public class ECLScreenReco extends haus.nightmare.lib3270j.ecl.ECLScreenReco { + + public ECLScreenReco() { + super(); + } + + public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLSession session) { + super(session); + } + + public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLPS ps) { + super(ps); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenRecoEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenRecoEvent.java new file mode 100644 index 0000000..a83bdb5 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/ECLScreenRecoEvent.java @@ -0,0 +1,13 @@ +package haus.nightmare.lib3270j.eNetwork.ECL; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLScreenRecoEvent. + */ +public class ECLScreenRecoEvent extends haus.nightmare.lib3270j.ecl.ECLScreenRecoEvent { + + public ECLScreenRecoEvent(haus.nightmare.lib3270j.ecl.ECLScreenReco source, + haus.nightmare.lib3270j.ecl.ECLScreenDesc screenDesc, + haus.nightmare.lib3270j.ecl.ECLPS ps) { + super(source, screenDesc, ps); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/bidi/ECLOIABIDI.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/bidi/ECLOIABIDI.java new file mode 100644 index 0000000..483cc8c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/bidi/ECLOIABIDI.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.bidi; + +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.telnet.TelnetFSM; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLOIABIDI. + */ +public class ECLOIABIDI extends haus.nightmare.lib3270j.ecl.ECLOIABIDI { + + public ECLOIABIDI(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { + super(screen, inputProcessor, fsm); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIANotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIANotify.java new file mode 100644 index 0000000..ec64b02 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLOIANotify.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.event; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLOIANotify in event package. + */ +public interface ECLOIANotify extends haus.nightmare.lib3270j.ecl.ECLOIANotify { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSEvent.java index 83bd4e7..49321b3 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSEvent.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSEvent.java @@ -1,5 +1,7 @@ package haus.nightmare.lib3270j.eNetwork.ECL.event; +import haus.nightmare.lib3270j.ecl.ECLPSUpdate; + /** * IBM Host On-Demand ECLPSEvent compatibility class. */ @@ -7,6 +9,15 @@ public class ECLPSEvent extends haus.nightmare.lib3270j.ecl.ECLPSEvent { private static final long serialVersionUID = 1L; + public ECLPSEvent(Object source, int eventType, int type, int startRow, int startCol, + int endRow, int endCol, int oldCursorAddress, int newCursorAddress, + int rows, int cols, boolean fullUpdate, boolean cursorVisible, + int ringCounter, boolean startPrinterBit, ECLPSUpdate psUpdate) { + super(source, eventType, type, startRow, startCol, endRow, endCol, + oldCursorAddress, newCursorAddress, rows, cols, fullUpdate, + cursorVisible, ringCounter, startPrinterBit, psUpdate); + } + public ECLPSEvent(Object source, int eventType, int startRow, int startCol, int endRow, int endCol, int oldCursorAddress, int newCursorAddress, int rows, int cols, boolean fullUpdate) { super(source, eventType, startRow, startCol, endRow, endCol, oldCursorAddress, newCursorAddress, rows, cols, fullUpdate); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSGraphicsEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSGraphicsEvent.java new file mode 100644 index 0000000..6026ae7 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSGraphicsEvent.java @@ -0,0 +1,22 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.event; + +import java.awt.Image; +import java.awt.Rectangle; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsEvent. + */ +public class ECLPSGraphicsEvent extends haus.nightmare.lib3270j.ecl.ECLPSGraphicsEvent { + + public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id) { + super(source, id); + } + + public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image) { + super(source, id, image); + } + + public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image, Rectangle rectangle) { + super(source, id, image, rectangle); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSGraphicsListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSGraphicsListener.java new file mode 100644 index 0000000..0e60646 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/event/ECLPSGraphicsListener.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.event; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsListener. + */ +public interface ECLPSGraphicsListener extends haus.nightmare.lib3270j.ecl.ECLPSGraphicsListener { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hindi/ECLOIAHindi.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hindi/ECLOIAHindi.java new file mode 100644 index 0000000..845d9d9 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hindi/ECLOIAHindi.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hindi; + +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.telnet.TelnetFSM; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLOIAHindi. + */ +public class ECLOIAHindi extends haus.nightmare.lib3270j.ecl.ECLOIAHindi { + + public ECLOIAHindi(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { + super(screen, inputProcessor, fsm); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/Edge.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/Edge.java new file mode 100644 index 0000000..9936fa5 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/Edge.java @@ -0,0 +1,10 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +/** + * Drop-in IBM Host On-Demand compatible facade for Edge. + */ +public class Edge extends haus.nightmare.lib3270j.graphics.Edge { + public Edge(int x1, int y1, int x2, int y2) { + super(x1, y1, x2, y2); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/FillArea.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/FillArea.java new file mode 100644 index 0000000..102f00f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/FillArea.java @@ -0,0 +1,20 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +import java.awt.Color; + +/** + * Drop-in IBM Host On-Demand compatible facade for FillArea. + */ +public class FillArea extends haus.nightmare.lib3270j.graphics.FillArea { + public FillArea() { + super(); + } + + public FillArea(int fillRule) { + super(fillRule); + } + + public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, Color color) { + super(px, py, polyCounts, numPolys, color); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/FilletPts.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/FilletPts.java new file mode 100644 index 0000000..e04b8f4 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/FilletPts.java @@ -0,0 +1,10 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +/** + * Drop-in IBM Host On-Demand compatible facade for FilletPts. + */ +public class FilletPts extends haus.nightmare.lib3270j.graphics.FilletPts { + public FilletPts() { + super(); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODBitImage.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODBitImage.java new file mode 100644 index 0000000..d120a5c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODBitImage.java @@ -0,0 +1,12 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +import java.awt.Component; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODBitImage. + */ +public class HODBitImage extends haus.nightmare.lib3270j.graphics.HODBitImage { + public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) { + super(comp, width, height, data, baseColor, depth, useGraphicColors); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODBounds.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODBounds.java new file mode 100644 index 0000000..dbdc978 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODBounds.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODBounds. + */ +public class HODBounds extends haus.nightmare.lib3270j.graphics.HODBounds { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODColorChangeFilter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODColorChangeFilter.java new file mode 100644 index 0000000..505702a --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODColorChangeFilter.java @@ -0,0 +1,10 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODColorChangeFilter. + */ +public class HODColorChangeFilter extends haus.nightmare.lib3270j.graphics.HODColorChangeFilter { + public HODColorChangeFilter(int color) { + super(color); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODGraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODGraphicsPlane.java new file mode 100644 index 0000000..5c5b33e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODGraphicsPlane.java @@ -0,0 +1,20 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +import haus.nightmare.lib3270j.graphics.GraphicsPlane; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODGraphicsPlane. + */ +public class HODGraphicsPlane extends haus.nightmare.lib3270j.graphics.HODGraphicsPlane { + public HODGraphicsPlane() { + super(); + } + + public HODGraphicsPlane(int width, int height) { + super(width, height); + } + + public HODGraphicsPlane(GraphicsPlane delegate) { + super(delegate); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODPart.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODPart.java new file mode 100644 index 0000000..aa2f9eb --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODPart.java @@ -0,0 +1,30 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Rectangle; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODPart. + */ +public class HODPart extends haus.nightmare.lib3270j.graphics.HODPart { + public HODPart() { + super(); + } + + public HODPart(Component component) { + super(component); + } + + public HODPart(Component component, Dimension dimension) { + super(component, dimension); + } + + public HODPart(Component component, Rectangle rectangle) { + super(component, rectangle); + } + + public HODPart(HODPart hODPart) { + super(hODPart); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODProgramSymbolManager.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODProgramSymbolManager.java new file mode 100644 index 0000000..a2525b0 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODProgramSymbolManager.java @@ -0,0 +1,16 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +import haus.nightmare.lib3270j.graphics.ProgramSymbolManager; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODProgramSymbolManager. + */ +public class HODProgramSymbolManager extends haus.nightmare.lib3270j.graphics.HODProgramSymbolManager { + public HODProgramSymbolManager() { + super(); + } + + public HODProgramSymbolManager(ProgramSymbolManager delegate) { + super(delegate); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODTransform.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODTransform.java new file mode 100644 index 0000000..fc674d0 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODTransform.java @@ -0,0 +1,10 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODTransform. + */ +public class HODTransform extends haus.nightmare.lib3270j.graphics.HODTransform { + public HODTransform(int charW, int charH, int defaultCharW, int defaultCharH) { + super(charW, charH, defaultCharW, defaultCharH); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODTransparentColorFilter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODTransparentColorFilter.java new file mode 100644 index 0000000..16304fa --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODTransparentColorFilter.java @@ -0,0 +1,10 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODTransparentColorFilter. + */ +public class HODTransparentColorFilter extends haus.nightmare.lib3270j.graphics.HODTransparentColorFilter { + public HODTransparentColorFilter(int transparentColor) { + super(transparentColor); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODWallpaper.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODWallpaper.java new file mode 100644 index 0000000..1c0f107 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/hostgraphics/HODWallpaper.java @@ -0,0 +1,20 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics; + +import java.awt.Image; + +/** + * Drop-in IBM Host On-Demand compatible facade for HODWallpaper. + */ +public class HODWallpaper extends haus.nightmare.lib3270j.graphics.HODWallpaper { + public HODWallpaper() { + super(); + } + + public HODWallpaper(int displayMode) { + super(displayMode); + } + + public HODWallpaper(Image image, int displayMode) { + super(image, displayMode); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/thai/ECLOIATHAI.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/thai/ECLOIATHAI.java new file mode 100644 index 0000000..ee01dac --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/thai/ECLOIATHAI.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.thai; + +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.telnet.TelnetFSM; + +/** + * Drop-in IBM Host On-Demand compatible facade for ECLOIATHAI. + */ +public class ECLOIATHAI extends haus.nightmare.lib3270j.ecl.ECLOIATHAI { + + public ECLOIATHAI(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { + super(screen, inputProcessor, fsm); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/DS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/DS3270.java new file mode 100644 index 0000000..6032f1c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/DS3270.java @@ -0,0 +1,29 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.screen.ScreenBuffer; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270.DS3270. + */ +public class DS3270 extends haus.nightmare.lib3270j.tn3270.DS3270 { + + public DS3270() { + super(); + } + + public DS3270(ScreenBuffer screen, EbcdicTranslator translator) { + super(screen, translator); + } + + public DS3270(DataStreamProcessor delegate) { + super(delegate); + } + + public DS3270(ECLSession session, ECLPS ps) { + super(session, ps); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/NVT3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/NVT3270.java new file mode 100644 index 0000000..563ce1f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/NVT3270.java @@ -0,0 +1,23 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270; + +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.nvt.NvtProcessor; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270.NVT3270. + */ +public class NVT3270 extends haus.nightmare.lib3270j.tn3270.NVT3270 { + + public NVT3270() { + super(); + } + + public NVT3270(NvtProcessor nvtProcessor) { + super(nvtProcessor); + } + + public NVT3270(String host, ECLSession session, ECLPS ps, haus.nightmare.lib3270j.tn3270.DS3270 ds) { + super(host, session, ps, ds); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/PS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/PS3270.java new file mode 100644 index 0000000..4093de8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/PS3270.java @@ -0,0 +1,24 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270.PS3270. + */ +public class PS3270 extends haus.nightmare.lib3270j.tn3270.PS3270 { + + public PS3270() { + super(); + } + + public PS3270(ECLSession session) { + super(session); + } + + public PS3270(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) { + super(screen, inputProcessor, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/Telnet3270E.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/Telnet3270E.java new file mode 100644 index 0000000..6a9d52e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/Telnet3270E.java @@ -0,0 +1,24 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270; + +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.telnet.TelnetConnection; +import haus.nightmare.lib3270j.telnet.TelnetFSM; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270.Telnet3270E. + */ +public class Telnet3270E extends haus.nightmare.lib3270j.tn3270.Telnet3270E { + + public Telnet3270E() { + super(); + } + + public Telnet3270E(TelnetFSM fsm, TelnetConnection connection) { + super(fsm, connection); + } + + public Telnet3270E(String host, ECLSession session, ECLPS ps, haus.nightmare.lib3270j.tn3270.DS3270 ds) { + super(host, session, ps, ds); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/qr_elem.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/qr_elem.java new file mode 100644 index 0000000..560e77f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270/qr_elem.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270.qr_elem. + */ +public class qr_elem extends haus.nightmare.lib3270j.tn3270.qr_elem { + + public qr_elem() { + super(); + } + + public qr_elem(int type, int sendFlag) { + super(type, sendFlag); + } + + public qr_elem(byte type, byte sendFlag) { + super(type, sendFlag); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/DS3270P.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/DS3270P.java new file mode 100644 index 0000000..e574599 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/DS3270P.java @@ -0,0 +1,31 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrintPS3270; +import haus.nightmare.lib3270j.printer.PrintSCS3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; +import haus.nightmare.lib3270j.printer.Telnet3270EP; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.DS3270P. + */ +public class DS3270P extends haus.nightmare.lib3270j.printer.DS3270P { + + public DS3270P() { + super(); + } + + public DS3270P(PrinterConfig config) { + super(config); + } + + public DS3270P(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } + + public DS3270P(Telnet3270EP telnet, PrinterConfig config, PD3270 pd, + PrintSCS3270 scs, PrintPS3270 printPs, EbcdicTranslator translator) { + super(telnet, config, pd, scs, printPs, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PD3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PD3270.java new file mode 100644 index 0000000..25c0d67 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PD3270.java @@ -0,0 +1,17 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.PD3270. + */ +public class PD3270 extends haus.nightmare.lib3270j.printer.PD3270 { + + public PD3270() { + super(); + } + + public PD3270(PrinterConfig config) { + super(config); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PDT.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PDT.java new file mode 100644 index 0000000..bacbb57 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PDT.java @@ -0,0 +1,11 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.PDT. + */ +public class PDT extends PrinterDefinitionTable { + + public PDT(String name, String description) { + super(name, description); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintPS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintPS3270.java new file mode 100644 index 0000000..bc2181f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintPS3270.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.PrintPS3270. + */ +public class PrintPS3270 extends haus.nightmare.lib3270j.printer.PrintPS3270 { + + public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintPS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintPS3270DB.java new file mode 100644 index 0000000..eb2f680 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintPS3270DB.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.PrintPS3270DB. + */ +public class PrintPS3270DB extends haus.nightmare.lib3270j.printer.PrintPS3270DB { + + public PrintPS3270DB(PrinterConfig config) { + super(config); + } + + public PrintPS3270DB(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintSCS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintSCS3270.java new file mode 100644 index 0000000..979c2ed --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintSCS3270.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.PrintSCS3270. + */ +public class PrintSCS3270 extends haus.nightmare.lib3270j.printer.PrintSCS3270 { + + public PrintSCS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintSCS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintSCS3270DB.java new file mode 100644 index 0000000..27c33d7 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrintSCS3270DB.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.PrintSCS3270DB. + */ +public class PrintSCS3270DB extends haus.nightmare.lib3270j.printer.PrintSCS3270DB { + + public PrintSCS3270DB(PrinterConfig config) { + super(config); + } + + public PrintSCS3270DB(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrinterDefinitionTable.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrinterDefinitionTable.java new file mode 100644 index 0000000..d6f7d5b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/PrinterDefinitionTable.java @@ -0,0 +1,11 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.PrinterDefinitionTable. + */ +public class PrinterDefinitionTable extends haus.nightmare.lib3270j.printer.PrinterDefinitionTable { + + public PrinterDefinitionTable(String name, String description) { + super(name, description); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/Telnet3270EP.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/Telnet3270EP.java new file mode 100644 index 0000000..97ac19a --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/Telnet3270EP.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.Telnet3270EP. + */ +public class Telnet3270EP extends haus.nightmare.lib3270j.printer.Telnet3270EP { + + public Telnet3270EP(PrinterConfig config) { + super(config); + } + + public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/Timer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/Timer.java new file mode 100644 index 0000000..74841ea --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/Timer.java @@ -0,0 +1,29 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +import haus.nightmare.lib3270j.printer.TimerListener; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.Timer. + */ +public class Timer extends haus.nightmare.lib3270j.printer.Timer { + + public Timer() { + super(); + } + + public Timer(long intervalMs) { + super(intervalMs); + } + + public Timer(long intervalMs, TimerListener listener) { + super(intervalMs, listener); + } + + public Timer(long intervalMs, TimerListener listener, boolean repeating) { + super(intervalMs, listener, repeating); + } + + public Timer(long intervalMs, TimerListener listener, boolean repeating, String timerId) { + super(intervalMs, listener, repeating, timerId); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/TimerEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/TimerEvent.java new file mode 100644 index 0000000..c681b32 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/TimerEvent.java @@ -0,0 +1,21 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.TimerEvent. + */ +public class TimerEvent extends haus.nightmare.lib3270j.printer.TimerEvent { + + private static final long serialVersionUID = 1L; + + public TimerEvent(Object source) { + super(source); + } + + public TimerEvent(Object source, String timerId) { + super(source, timerId); + } + + public TimerEvent(Object source, String timerId, long timestamp) { + super(source, timerId, timestamp); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/TimerListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/TimerListener.java new file mode 100644 index 0000000..0fe5fdc --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/tn3270p/TimerListener.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for com.ibm.eNetwork.ECL.tn3270p.TimerListener. + */ +public interface TimerListener extends haus.nightmare.lib3270j.printer.TimerListener { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferFileObject.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferFileObject.java new file mode 100644 index 0000000..f2fbb8f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferFileObject.java @@ -0,0 +1,34 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +import haus.nightmare.lib3270j.ft.FTConfig; + +/** + * Drop-in IBM Host On-Demand compatibility class for + * com.ibm.eNetwork.ECL.xfer.FileTransferFileObject. + */ +public class FileTransferFileObject extends haus.nightmare.lib3270j.xfer.FileTransferFileObject { + + public FileTransferFileObject() { + super(); + } + + public FileTransferFileObject(String name) { + super(name); + } + + public FileTransferFileObject(String name, long size) { + super(name, size); + } + + public FileTransferFileObject(String name, long size, boolean isDirectory) { + super(name, size, isDirectory); + } + + public FileTransferFileObject(String localFile, String hostDatasetName) { + super(localFile, hostDatasetName); + } + + public FileTransferFileObject(FTConfig config) { + super(config); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferHostDirectoryInterface.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferHostDirectoryInterface.java new file mode 100644 index 0000000..35db1ba --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferHostDirectoryInterface.java @@ -0,0 +1,8 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +/** + * Drop-in IBM Host On-Demand compatibility interface for + * com.ibm.eNetwork.ECL.xfer.FileTransferHostDirectoryInterface. + */ +public interface FileTransferHostDirectoryInterface extends haus.nightmare.lib3270j.xfer.FileTransferHostDirectoryInterface { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferInterface.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferInterface.java new file mode 100644 index 0000000..3df825f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferInterface.java @@ -0,0 +1,8 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +/** + * Drop-in IBM Host On-Demand compatibility interface for + * com.ibm.eNetwork.ECL.xfer.FileTransferInterface. + */ +public interface FileTransferInterface extends haus.nightmare.lib3270j.xfer.FileTransferInterface { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferStatusInterface.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferStatusInterface.java new file mode 100644 index 0000000..416fe7d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/FileTransferStatusInterface.java @@ -0,0 +1,8 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +/** + * Drop-in IBM Host On-Demand compatibility interface for + * com.ibm.eNetwork.ECL.xfer.FileTransferStatusInterface. + */ +public interface FileTransferStatusInterface extends haus.nightmare.lib3270j.xfer.FileTransferStatusInterface { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileInputStream.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileInputStream.java new file mode 100644 index 0000000..a20b00d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileInputStream.java @@ -0,0 +1,27 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +import java.io.File; +import java.io.FileNotFoundException; + +/** + * Drop-in IBM Host On-Demand compatibility class for + * com.ibm.eNetwork.ECL.xfer.XferFileInputStream. + */ +public class XferFileInputStream extends haus.nightmare.lib3270j.xfer.XferFileInputStream { + + public XferFileInputStream(String name, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(name, nonIbmText, nonIbmTerminator, asciiTransfer); + } + + public XferFileInputStream(File file, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(file, nonIbmText, nonIbmTerminator, asciiTransfer); + } + + public XferFileInputStream(String name, byte[] terminators) throws FileNotFoundException { + super(name, terminators); + } + + public XferFileInputStream(File file, byte[] terminators) throws FileNotFoundException { + super(file, terminators); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileInputUnicode.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileInputUnicode.java new file mode 100644 index 0000000..0e4f504 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileInputUnicode.java @@ -0,0 +1,25 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +import haus.nightmare.lib3270j.charset.CodePage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.UnsupportedEncodingException; + +/** + * Drop-in IBM Host On-Demand compatibility class for + * com.ibm.eNetwork.ECL.xfer.XferFileInputUnicode. + */ +public class XferFileInputUnicode extends haus.nightmare.lib3270j.xfer.XferFileInputUnicode { + + public XferFileInputUnicode(String filename, byte[] terminators, CodePage cp, + int unicodeType, int sessionType, boolean noso) + throws FileNotFoundException, UnsupportedEncodingException { + super(filename, terminators, cp, unicodeType, sessionType, noso); + } + + public XferFileInputUnicode(File file, byte[] terminators, CodePage cp, + int unicodeType, int sessionType, boolean noso) + throws FileNotFoundException, UnsupportedEncodingException { + super(file, terminators, cp, unicodeType, sessionType, noso); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileOutputStream.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileOutputStream.java new file mode 100644 index 0000000..92bad41 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileOutputStream.java @@ -0,0 +1,27 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +import java.io.File; +import java.io.FileNotFoundException; + +/** + * Drop-in IBM Host On-Demand compatibility class for + * com.ibm.eNetwork.ECL.xfer.XferFileOutputStream. + */ +public class XferFileOutputStream extends haus.nightmare.lib3270j.xfer.XferFileOutputStream { + + public XferFileOutputStream(String name, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(name, nonIbmText, nonIbmTerminator, asciiTransfer); + } + + public XferFileOutputStream(File file, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(file, nonIbmText, nonIbmTerminator, asciiTransfer); + } + + public XferFileOutputStream(String name, boolean append, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(name, append, nonIbmText, nonIbmTerminator, asciiTransfer); + } + + public XferFileOutputStream(File file, boolean append, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(file, append, nonIbmText, nonIbmTerminator, asciiTransfer); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileOutputUnicode.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileOutputUnicode.java new file mode 100644 index 0000000..7a6fb82 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer/XferFileOutputUnicode.java @@ -0,0 +1,27 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer; + +import haus.nightmare.lib3270j.charset.CodePage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.UnsupportedEncodingException; + +/** + * Drop-in IBM Host On-Demand compatibility class for + * com.ibm.eNetwork.ECL.xfer.XferFileOutputUnicode. + */ +public class XferFileOutputUnicode extends haus.nightmare.lib3270j.xfer.XferFileOutputUnicode { + + public XferFileOutputUnicode(String filename, boolean append, byte[] terminators, + boolean asciiTransfer, CodePage cp, int unicodeType, + boolean soFlag, boolean soAlt) + throws FileNotFoundException, UnsupportedEncodingException { + super(filename, append, terminators, asciiTransfer, cp, unicodeType, soFlag, soAlt); + } + + public XferFileOutputUnicode(File file, boolean append, byte[] terminators, + boolean asciiTransfer, CodePage cp, int unicodeType, + boolean soFlag, boolean soAlt) + throws FileNotFoundException, UnsupportedEncodingException { + super(file, append, terminators, asciiTransfer, cp, unicodeType, soFlag, soAlt); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer3270/CMSPrintXfer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer3270/CMSPrintXfer.java new file mode 100644 index 0000000..32b1e7c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer3270/CMSPrintXfer.java @@ -0,0 +1,27 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer3270; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLXfer; + +/** + * Drop-in IBM Host On-Demand compatibility class for + * com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer. + */ +public class CMSPrintXfer extends haus.nightmare.lib3270j.xfer3270.CMSPrintXfer { + + public CMSPrintXfer() { + super(); + } + + public CMSPrintXfer(ECLXfer xfer) { + super(xfer); + } + + public CMSPrintXfer(ECLXfer xfer, EbcdicTranslator translator) { + super(xfer, translator); + } + + public CMSPrintXfer(Xfer3270 xfer3270) { + super(xfer3270); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer3270/Xfer3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer3270/Xfer3270.java new file mode 100644 index 0000000..6bd8b4f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/ECL/xfer3270/Xfer3270.java @@ -0,0 +1,37 @@ +package haus.nightmare.lib3270j.eNetwork.ECL.xfer3270; + +import haus.nightmare.lib3270j.charset.CodePage; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.ecl.ECLXfer; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import java.net.URL; + +/** + * Drop-in IBM Host On-Demand compatibility class for + * com.ibm.eNetwork.ECL.xfer3270.Xfer3270. + */ +public class Xfer3270 extends haus.nightmare.lib3270j.xfer3270.Xfer3270 { + + public Xfer3270() { + super(); + } + + public Xfer3270(ECLSession session) { + super(session); + } + + public Xfer3270(ECLSession session, URL url) { + super(session, url); + } + + public Xfer3270(ECLXfer xfer) { + super(xfer); + } + + public Xfer3270(ScreenBuffer screen, InputProcessor input, + DataStreamProcessor dsProcessor, CodePage codePage) { + super(screen, input, dsProcessor, codePage); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODByteToCharConverter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODByteToCharConverter.java new file mode 100644 index 0000000..7d37cbd --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODByteToCharConverter.java @@ -0,0 +1,28 @@ +package haus.nightmare.lib3270j.eNetwork.HOD.common; + +import haus.nightmare.lib3270j.charset.CodePage; +import haus.nightmare.lib3270j.converters.HODUnsupportedCodepageException; + +/** + * IBM Host On-Demand drop-in compatibility facade for HODByteToCharConverter. + */ +public abstract class HODByteToCharConverter extends haus.nightmare.lib3270j.converters.HODByteToCharConverter { + + public HODByteToCharConverter() { + super(); + } + + public HODByteToCharConverter(CodePage codePage) { + super(codePage); + } + + public static haus.nightmare.lib3270j.converters.HODByteToCharConverter getHODConverter(String encoding) + throws HODUnsupportedCodepageException { + return haus.nightmare.lib3270j.converters.HODByteToCharConverter.getHODConverter(encoding); + } + + public static haus.nightmare.lib3270j.converters.HODByteToCharConverter getConverter(String encoding) + throws HODUnsupportedCodepageException { + return haus.nightmare.lib3270j.converters.HODByteToCharConverter.getConverter(encoding); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODCharConversionException.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODCharConversionException.java new file mode 100644 index 0000000..fda938d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODCharConversionException.java @@ -0,0 +1,17 @@ +package haus.nightmare.lib3270j.eNetwork.HOD.common; + +/** + * IBM Host On-Demand drop-in compatibility facade for HODCharConversionException. + */ +public class HODCharConversionException extends haus.nightmare.lib3270j.converters.HODCharConversionException { + + private static final long serialVersionUID = 1L; + + public HODCharConversionException() { + super(); + } + + public HODCharConversionException(String message) { + super(message); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODCharToByteConverter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODCharToByteConverter.java new file mode 100644 index 0000000..b3d9d25 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODCharToByteConverter.java @@ -0,0 +1,28 @@ +package haus.nightmare.lib3270j.eNetwork.HOD.common; + +import haus.nightmare.lib3270j.charset.CodePage; +import haus.nightmare.lib3270j.converters.HODUnsupportedCodepageException; + +/** + * IBM Host On-Demand drop-in compatibility facade for HODCharToByteConverter. + */ +public abstract class HODCharToByteConverter extends haus.nightmare.lib3270j.converters.HODCharToByteConverter { + + public HODCharToByteConverter() { + super(); + } + + public HODCharToByteConverter(CodePage codePage) { + super(codePage); + } + + public static haus.nightmare.lib3270j.converters.HODCharToByteConverter getHODConverter(String encoding) + throws HODUnsupportedCodepageException { + return haus.nightmare.lib3270j.converters.HODCharToByteConverter.getHODConverter(encoding); + } + + public static haus.nightmare.lib3270j.converters.HODCharToByteConverter getConverter(String encoding) + throws HODUnsupportedCodepageException { + return haus.nightmare.lib3270j.converters.HODCharToByteConverter.getConverter(encoding); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODUnsupportedCodepageException.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODUnsupportedCodepageException.java new file mode 100644 index 0000000..3fd8cb7 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/HOD/common/HODUnsupportedCodepageException.java @@ -0,0 +1,17 @@ +package haus.nightmare.lib3270j.eNetwork.HOD.common; + +/** + * IBM Host On-Demand drop-in compatibility facade for HODUnsupportedCodepageException. + */ +public class HODUnsupportedCodepageException extends haus.nightmare.lib3270j.converters.HODUnsupportedCodepageException { + + private static final long serialVersionUID = 1L; + + public HODUnsupportedCodepageException() { + super(); + } + + public HODUnsupportedCodepageException(String encoding) { + super(encoding); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/security/ssl/HODSSLECLSessionImpl.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/security/ssl/HODSSLECLSessionImpl.java new file mode 100644 index 0000000..95bfc8e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/eNetwork/security/ssl/HODSSLECLSessionImpl.java @@ -0,0 +1,33 @@ +package haus.nightmare.lib3270j.eNetwork.security.ssl; + +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.ecl.ECLConnection; +import haus.nightmare.lib3270j.ecl.ECLSession; +import java.util.Properties; + +/** + * IBM Host On-Demand (HoD) canonical package compatibility drop-in facade for + * com.ibm.eNetwork.security.ssl.HODSSLECLSessionImpl. + */ +public class HODSSLECLSessionImpl extends haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl { + + public HODSSLECLSessionImpl() { + super(); + } + + public HODSSLECLSessionImpl(ConnectionConfig config) { + super(config); + } + + public HODSSLECLSessionImpl(ECLSession session) { + super(session); + } + + public HODSSLECLSessionImpl(ECLConnection connection) { + super(connection); + } + + public HODSSLECLSessionImpl(Properties props) { + super(props); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSBIDIServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSBIDIServices.java new file mode 100644 index 0000000..6a52c14 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSBIDIServices.java @@ -0,0 +1,131 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Standard implementation of ECLPSBIDIServices conforming to IBM Host On-Demand ECL. + */ +public class DefaultPSBIDIServices implements ECLPSBIDIServices { + + private final ECLPS ps; + private String numeralShape = NOMINAL; + private String textType = VISUAL; + private String textOrientation = LEFT_TO_RIGHT; + private String roundTrip = ROUNDTRIP_OFF; + private String lamAlef = LAMALEF_ON; + private String rtlUnicode = RTLUNICODE_ON; + private boolean macroBidiEnabled = false; + private boolean numericSwap = false; + private boolean symmetricSwap = false; + + public DefaultPSBIDIServices(ECLPS ps) { + this.ps = ps; + } + + @Override + public void SetNumeralShape(String shape) throws ECLErr { + this.numeralShape = shape != null ? shape : NOMINAL; + } + + @Override + public String GetNumeralShape() { + return numeralShape; + } + + @Override + public void SetTextType(String type) throws ECLErr { + this.textType = type != null ? type : VISUAL; + } + + @Override + public String GetTextType() { + return textType; + } + + @Override + public void SetTextOrientation(String orientation) throws ECLErr { + this.textOrientation = orientation != null ? orientation : LEFT_TO_RIGHT; + } + + @Override + public String GetTextOrientation() { + return textOrientation; + } + + @Override + public void setMacroBidiEnabled(boolean enabled) { + this.macroBidiEnabled = enabled; + } + + @Override + public boolean isMacroBidiEnabled() { + return macroBidiEnabled; + } + + @Override + public void SetRoundTrip(String rt) throws ECLErr { + this.roundTrip = rt != null ? rt : ROUNDTRIP_OFF; + } + + @Override + public String GetRoundTrip() { + return roundTrip; + } + + @Override + public void SetBIDICursorPos(int pos, boolean visual) throws ECLErr { + if (ps != null) { + ps.setCursorPos(pos - 1); + } + } + + @Override + public void SetBIDICursorPos(int pos) throws ECLErr { + SetBIDICursorPos(pos, false); + } + + @Override + public void SetBIDICursorPos(int row, int col) throws ECLErr { + if (ps != null) { + ps.setCursorPos(row - 1, col - 1); + } + } + + @Override + public void SetLamAlef(String mode) throws ECLErr { + this.lamAlef = mode != null ? mode : LAMALEF_ON; + } + + @Override + public String GetLamAlef() { + return lamAlef; + } + + @Override + public void SetRTLUnicode(String mode) throws ECLErr { + this.rtlUnicode = mode != null ? mode : RTLUNICODE_ON; + } + + @Override + public String GetRTLUnicode() { + return rtlUnicode; + } + + @Override + public void setNumericSwap(boolean swap) { + this.numericSwap = swap; + } + + @Override + public boolean getNumericSwap() { + return numericSwap; + } + + @Override + public void setSymmetricSwap(boolean swap) { + this.symmetricSwap = swap; + } + + @Override + public boolean getSymmetricSwap() { + return symmetricSwap; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSGraphicsServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSGraphicsServices.java new file mode 100644 index 0000000..d001ab1 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSGraphicsServices.java @@ -0,0 +1,73 @@ +package haus.nightmare.lib3270j.ecl; + +import java.awt.Color; +import java.awt.Component; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Standard implementation of ECLPSGraphicsServices conforming to IBM Host On-Demand ECL. + */ +public class DefaultPSGraphicsServices implements ECLPSGraphicsServices { + + private final ECLPS ps; + private Component visualComponent; + private Color[] colors; + private final List listeners = new CopyOnWriteArrayList<>(); + + public DefaultPSGraphicsServices(ECLPS ps) { + this.ps = ps; + } + + @Override + public void setVisualComponent(Component comp) { + this.visualComponent = comp; + } + + public Component getVisualComponent() { + return visualComponent; + } + + @Override + public void setGraphicColor(Color[] colors, boolean b) { + this.colors = (colors != null) ? colors.clone() : null; + } + + public Color[] getGraphicColors() { + return (colors != null) ? colors.clone() : null; + } + + @Override + public void mousePressed(int x, int y, int button) { + if (ps != null && ps.getInputProcessor() != null) { + ps.getInputProcessor().sendGraphicMouseAid(x, y, button == 1, false); + } + ECLPSGraphicsEvent event = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED); + for (ECLPSGraphicsListener l : listeners) { + try { + l.graphicsUpdated(event); + } catch (Exception ignored) {} + } + } + + @Override + public void addGraphicsListener(ECLPSGraphicsListener listener) { + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + } + } + + @Override + public void removeGraphicsListener(ECLPSGraphicsListener listener) { + listeners.remove(listener); + } + + public void fireGraphicsEvent(int id) { + ECLPSGraphicsEvent event = new ECLPSGraphicsEvent(ps, id); + for (ECLPSGraphicsListener l : listeners) { + try { + l.graphicsEvent(event); + } catch (Exception ignored) {} + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSHindiServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSHindiServices.java new file mode 100644 index 0000000..0245986 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSHindiServices.java @@ -0,0 +1,33 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Standard implementation of ECLPSHindiServices conforming to IBM Host On-Demand ECL. + */ +public class DefaultPSHindiServices implements ECLPSHindiServices { + + private final ECLPS ps; + + public DefaultPSHindiServices(ECLPS ps) { + this.ps = ps; + } + + @Override + public int GetHindiCursorCol(int row, int col) { + return col; + } + + @Override + public byte GetHindiCursorLevel(int row, int col) { + return 0; + } + + @Override + public int GetNormalCursorCol(int row, int col) { + return col; + } + + @Override + public void switchToHindiLayer() { + // Layer switch stub + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSTHAIServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSTHAIServices.java new file mode 100644 index 0000000..024e101 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/DefaultPSTHAIServices.java @@ -0,0 +1,39 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Standard implementation of ECLPSTHAIServices conforming to IBM Host On-Demand ECL. + */ +public class DefaultPSTHAIServices implements ECLPSTHAIServices { + + private final ECLPS ps; + private int displayMode = 0; + + public DefaultPSTHAIServices(ECLPS ps) { + this.ps = ps; + } + + @Override + public void SetThaiDisplayMode(int mode) throws ECLErr { + this.displayMode = mode; + } + + @Override + public int GetThaiDisplayMode() { + return displayMode; + } + + @Override + public int GetThaiCursorCol(int row, int col) { + return col; + } + + @Override + public byte GetThaiCursorLevel(int row, int col) { + return 0; + } + + @Override + public int GetNormalCursorCol(int row, int col) { + return col; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommEvent.java index b8cdc6e..be20744 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommEvent.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommEvent.java @@ -5,6 +5,7 @@ import java.util.EventObject; /** * Event object dispatched on communication lifecycle and state transitions. + * Conforms 1:1 to IBM Host On-Demand ECLCommEvent specification. */ public class ECLCommEvent extends EventObject { @@ -35,35 +36,60 @@ public class ECLCommEvent extends EventObject { public ECLCommEvent(Object source, int eventType, ConnectionState oldState, ConnectionState newState, String message, String deviceType, String deviceName) { - super(source); + super(source != null ? source : "ECLConnection"); this.eventType = eventType; this.oldState = oldState; this.newState = newState; - this.message = message; - this.deviceType = deviceType; - this.deviceName = deviceName; + this.message = message != null ? message : ""; + this.deviceType = deviceType != null ? deviceType : ""; + this.deviceName = deviceName != null ? deviceName : ""; } public int getEventType() { return eventType; } + public int GetType() { return eventType; } + public int getType() { return eventType; } + public ConnectionState getOldState() { return oldState; } + public ConnectionState GetOldState() { return oldState; } + public ConnectionState getNewState() { return newState; } + public ConnectionState GetNewState() { return newState; } + public String getMessage() { return message; } + public String GetMessage() { return message; } + public String getErrorMessage() { return message; } + public String GetErrorMessage() { return message; } + public String getDeviceType() { return deviceType; } + public String GetDeviceType() { return deviceType; } + public String getDeviceName() { return deviceName; } + public String GetDeviceName() { return deviceName; } + public String getLUName() { return deviceName; } + public String GetLUName() { return deviceName; } public boolean isConnected() { return newState != null && newState.isConnected(); } + public boolean IsConnected() { + return isConnected(); + } public boolean isFullSession() { return newState != null && newState.isFullSession(); } + public boolean IsFullSession() { + return isFullSession(); + } public ECLConnection getConnection() { return (getSource() instanceof ECLConnection) ? (ECLConnection) getSource() : null; } + public ECLConnection GetConnection() { + return getConnection(); + } @Override public String toString() { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommListener.java index 617a1e3..16bddec 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommListener.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommListener.java @@ -2,14 +2,39 @@ package haus.nightmare.lib3270j.ecl; /** * Listener interface for communication lifecycle events. + * Conforms 1:1 to IBM Host On-Demand ECLCommListener specification. */ public interface ECLCommListener { + /** + * Primary HoD notification callback invoked when connection state changes. + * @param event ECLCommEvent containing transition details + */ + default void CommNotifyEvent(ECLCommEvent event) { + commEvent(event); + } + + /** + * Called when an error condition occurs on the connection. + * @param conn ECLConnection instance + * @param err ECLErr error descriptor + */ + default void CommNotifyError(ECLConnection conn, ECLErr err) {} + + /** + * Called when communication event generation has stopped. + * @param conn ECLConnection instance + * @param reason Stop reason code + */ + default void CommNotifyStop(ECLConnection conn, int reason) {} + + // ========== Backward-Compatibility Bridge Methods ========== + /** * Called when the communication connection state or status changes. * @param event ECLCommEvent containing transition details */ - void commEvent(ECLCommEvent event); + default void commEvent(ECLCommEvent event) {} /** * Called specifically when the connection is established. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommNotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommNotify.java index 44a2a87..d87a95f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommNotify.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLCommNotify.java @@ -11,4 +11,26 @@ public interface ECLCommNotify { * @param connected true if session is connected, false otherwise */ void CommNotify(boolean connected); + + /** + * Optional event notification callback invoked on communication state change. + * @param event ECLCommEvent + */ + default void CommNotifyEvent(ECLCommEvent event) { + CommNotify(event != null && event.isConnected()); + } + + /** + * Optional error callback invoked on communication error. + * @param conn ECLConnection instance + * @param err ECLErr error descriptor + */ + default void CommNotifyError(ECLConnection conn, ECLErr err) {} + + /** + * Optional stop callback invoked when communication event processing terminates. + * @param conn ECLConnection instance + * @param reason Stop reason code + */ + default void CommNotifyStop(ECLConnection conn, int reason) {} } 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 a9ece00..ff99799 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConnection.java @@ -7,6 +7,7 @@ import haus.nightmare.lib3270j.listener.ConnectionListener; import java.io.IOException; import java.util.List; +import java.util.Properties; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -19,6 +20,60 @@ public class ECLConnection { private final List commListeners = new CopyOnWriteArrayList<>(); private final List commNotifies = new CopyOnWriteArrayList<>(); + private String host; + private int port = 23; + private String codePage; + private String deviceName; + private String luName; + private String workstationId = ""; + private boolean ssl = false; + private boolean contentionResolution = false; + private boolean luluSession = false; + private boolean isNegCR = false; + private boolean isBIND7FArchitectureViolation = false; + private String keyRemap = ""; + private String certificateName = ""; + private String certificateSource = ""; + private String certificateURL = ""; + private String certificatePassword = ""; + private boolean certificateProvided = false; + private String securityProtocol = "TLS"; + private String tlsProtocolVersion = "TLSv1.2"; + private boolean useJSSE = true; + private String jsseTrustStore = ""; + private String jsseTrustStoreType = "JKS"; + private String jsseTrustStorePassword = ""; + private String proxyType = ""; + private String proxyServerName = ""; + private String proxyServerPort = ""; + private String proxyUserId = ""; + private String proxyUserPassword = ""; + private String proxyAuthenMethod = ""; + private String proxySecurityProtocol = ""; + private Properties properties = new Properties(); + + public ECLConnection() { + this(null, null); + } + + public ECLConnection(ECLSession session) { + this(session, session != null ? session.getClient() : null); + } + + public ECLConnection(String host, int port) { + this(null, null); + this.host = host; + this.port = port; + } + + public ECLConnection(Properties props) { + this(null, null); + if (props != null) { + this.properties.putAll(props); + convertData(this.properties); + } + } + public ECLConnection(ECLSession session, Telnet3270Client client) { this.session = session; this.client = client; @@ -70,17 +125,20 @@ public class ECLConnection { public Telnet3270Client getClient() { return client; } public String GetHost() { - return (client != null && client.getConfig() != null) ? client.getConfig().getHost() : ""; + if (client != null && client.getConfig() != null) return client.getConfig().getHost(); + return host != null ? host : ""; } public String getHost() { return GetHost(); } public int GetPort() { - return (client != null && client.getConfig() != null) ? client.getConfig().getPort() : 23; + if (client != null && client.getConfig() != null) return client.getConfig().getPort(); + return port; } public int getPort() { return GetPort(); } public String GetCodePage() { - return (client != null) ? client.getCodePage() : "037"; + if (client != null) return client.getCodePage(); + return codePage != null ? codePage : "037"; } public String getCodePage() { return GetCodePage(); } @@ -93,7 +151,10 @@ public class ECLConnection { if (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) { return client.getTelnetFSM().getConnectedLu(); } - return (client != null && client.getConfig() != null) ? client.getConfig().getLuName() : null; + if (client != null && client.getConfig() != null && client.getConfig().getLuName() != null) { + return client.getConfig().getLuName(); + } + return luName; } public String getLUName() { return GetLUName(); } @@ -149,7 +210,8 @@ public class ECLConnection { public boolean isDisconnecting() { return IsDisconnecting(); } public boolean IsSSL() { - return client != null && client.getConfig() != null && client.getConfig().isUseTls(); + if (client != null && client.getConfig() != null) return client.getConfig().isUseTls(); + return ssl; } public boolean isSSL() { return IsSSL(); } @@ -176,6 +238,10 @@ public class ECLConnection { public void StopCommunication() { Disconnect(); } public void stopCommunication() { Disconnect(); } + public static final int STOP_UNREGISTER = 1; + public static final int STOP_DISCONNECT = 2; + public static final int STOP_ERROR = 3; + // ========== Event Listener Management ========== public void RegisterCommEvent(ECLCommListener listener) { @@ -186,7 +252,12 @@ public class ECLConnection { public void registerCommEvent(ECLCommListener listener) { RegisterCommEvent(listener); } public void UnregisterCommEvent(ECLCommListener listener) { - commListeners.remove(listener); + if (listener != null) { + commListeners.remove(listener); + try { + listener.CommNotifyStop(this, STOP_UNREGISTER); + } catch (Exception ignored) {} + } } public void unregisterCommEvent(ECLCommListener listener) { UnregisterCommEvent(listener); } @@ -198,30 +269,292 @@ public class ECLConnection { public void registerCommEvent(ECLCommNotify notify, boolean sync) { RegisterCommEvent(notify, sync); } public void UnregisterCommEvent(ECLCommNotify notify) { - commNotifies.remove(notify); + if (notify != null) { + commNotifies.remove(notify); + try { + notify.CommNotifyStop(this, STOP_UNREGISTER); + } catch (Exception ignored) {} + } } public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); } + public void notifyCommError(ECLErr err) { + for (ECLCommListener l : commListeners) { + try { + l.CommNotifyError(this, err); + } catch (Exception ignored) {} + } + for (ECLCommNotify n : commNotifies) { + try { + n.CommNotifyError(this, err); + } catch (Exception ignored) {} + } + } + + public void notifyCommStop(int reason) { + for (ECLCommListener l : commListeners) { + try { + l.CommNotifyStop(this, reason); + } catch (Exception ignored) {} + } + for (ECLCommNotify n : commNotifies) { + try { + n.CommNotifyStop(this, reason); + } catch (Exception ignored) {} + } + } + private void notifyCommEvent(ECLCommEvent event) { for (ECLCommListener l : commListeners) { try { - l.commEvent(event); + l.CommNotifyEvent(event); if (event.getEventType() == ECLCommEvent.COMM_CONNECTED) { l.commConnected(event); } else if (event.getEventType() == ECLCommEvent.COMM_DISCONNECTED) { l.commDisconnected(event); } else if (event.getEventType() == ECLCommEvent.COMM_ERROR) { l.commError(event); + l.CommNotifyError(this, new ECLErr("ECLConnection", "COMM0001", event.getMessage())); } } catch (Exception ignored) {} } for (ECLCommNotify n : commNotifies) { try { n.CommNotify(event.isConnected()); + if (event.getEventType() == ECLCommEvent.COMM_ERROR) { + n.CommNotifyError(this, new ECLErr("ECLConnection", "COMM0001", event.getMessage())); + } } catch (Exception ignored) {} } } + public void convertData(Properties properties) { + if (properties == null) return; + for (String key : properties.stringPropertyNames()) { + String val = properties.getProperty(key); + if (val != null) { + if ("true".equalsIgnoreCase(val)) { + properties.put(key, "1"); + } else if ("false".equalsIgnoreCase(val)) { + properties.put(key, "0"); + } + } + } + } + + public void SetHost(String host) { + this.host = host; + if (client != null && client.getConfig() != null) { + client.getConfig().setHost(host); + } + } + public void setHost(String host) { SetHost(host); } + + public void SetPort(int port) { + this.port = port; + if (client != null && client.getConfig() != null) { + client.getConfig().setPort(port); + } + } + public void setPort(int port) { SetPort(port); } + + public void SetCodePage(String codePage) { + this.codePage = codePage; + if (client != null) { + client.setCodePage(codePage); + } + } + public void setCodePage(String codePage) { SetCodePage(codePage); } + + public void SetDeviceName(String name) { + this.deviceName = name; + this.properties.put("deviceName", name != null ? name : ""); + if (client != null && client.getConfig() != null) { + client.getConfig().setTerminalName(name); + } + } + public void setDeviceName(String name) { SetDeviceName(name); } + public String GetDeviceName() { return deviceName != null ? deviceName : GetLUName(); } + public String getDeviceName() { return GetDeviceName(); } + + public void SetLUName(String lu) { + this.luName = lu; + if (client != null && client.getConfig() != null) { + client.getConfig().setLuName(lu); + } + } + public void setLUName(String lu) { SetLUName(lu); } + + public void SetWorkstationID(String wid) { this.workstationId = wid; } + public void setWorkstationID(String wid) { SetWorkstationID(wid); } + public String GetWorkstationID() { return workstationId; } + public String getWorkstationID() { return workstationId; } + + public void SetSSL(boolean ssl) { + this.ssl = ssl; + if (client != null && client.getConfig() != null) { + client.getConfig().setUseTls(ssl); + } + } + public void setSSL(boolean ssl) { SetSSL(ssl); } + + public void setContentionResolution(boolean bl) { this.contentionResolution = bl; } + public void SetContentionResolution(boolean bl) { setContentionResolution(bl); } + public boolean getContentionResolution() { return contentionResolution; } + public boolean isContentionResolution() { return contentionResolution; } + + public void set_LULU_Session(boolean bl) { this.luluSession = bl; } + public boolean is_LULU_Session() { return luluSession; } + public boolean get_LULU_Session() { return luluSession; } + + public boolean isNegotiateCResolution() { return isNegCR; } + public void setNegotiatedCResolution(boolean bl) { this.isNegCR = bl; } + + public boolean isBIND7FArchitectureViolation() { return isBIND7FArchitectureViolation; } + public void setBIND7FArchitectureViolation(boolean bl) { this.isBIND7FArchitectureViolation = bl; } + + public void setKeyRemap(String remap) { this.keyRemap = remap; } + public void SetKeyRemap(String remap) { setKeyRemap(remap); } + public String getKeyRemap() { return keyRemap; } + public String GetKeyRemap() { return keyRemap; } + + public void setCertificateName(String name) { + this.certificateName = name; + if (client != null && client.getConfig() != null) client.getConfig().setKeyStoreAlias(name); + } + public void SetCertificateName(String name) { setCertificateName(name); } + public String getCertificateName() { return certificateName; } + public String GetCertificateName() { return certificateName; } + + public void setCertificateSource(String src) { this.certificateSource = src; } + public void SetCertificateSource(String src) { setCertificateSource(src); } + public String getCertificateSource() { return certificateSource; } + public String GetCertificateSource() { return certificateSource; } + + public void setCertificateURL(String url) { + this.certificateURL = url; + if (client != null && client.getConfig() != null) client.getConfig().setKeyStorePath(url); + } + public void SetCertificateURL(String url) { setCertificateURL(url); } + public String getCertificateURL() { return certificateURL; } + public String GetCertificateURL() { return certificateURL; } + + public void setCertificatePassword(String pwd) { + this.certificatePassword = pwd; + if (client != null && client.getConfig() != null) client.getConfig().setKeyStorePassword(pwd); + } + public void SetCertificatePassword(String pwd) { setCertificatePassword(pwd); } + public String getCertificatePassword() { return certificatePassword; } + public String GetCertificatePassword() { return certificatePassword; } + + public void setCertificateProvided(boolean prov) { this.certificateProvided = prov; } + public void SetCertificateProvided(boolean prov) { setCertificateProvided(prov); } + public boolean isCertificateProvided() { return certificateProvided; } + + public void setSecurityProtocol(String prot) { + this.securityProtocol = prot; + if (client != null && client.getConfig() != null) client.getConfig().setSslProtocol(prot); + } + public void SetSecurityProtocol(String prot) { setSecurityProtocol(prot); } + public String getSecurityProtocol() { return securityProtocol; } + public String GetSecurityProtocol() { return securityProtocol; } + + public void setTLSProtocolVersion(String ver) { + this.tlsProtocolVersion = ver; + if (client != null && client.getConfig() != null && ver != null) { + client.getConfig().setSslProtocol(ver); + client.getConfig().setEnabledProtocols(ver); + } + } + public void SetTLSProtocolVersion(String ver) { setTLSProtocolVersion(ver); } + public String getTLSProtocolVersion() { return tlsProtocolVersion; } + public String GetTLSProtocolVersion() { return tlsProtocolVersion; } + + public void setUseJSSE(boolean jsse) { this.useJSSE = jsse; } + public void SetUseJSSE(boolean jsse) { setUseJSSE(jsse); } + public boolean isUseJSSE() { return useJSSE; } + + public void setJSSETrustStore(String ts) { + this.jsseTrustStore = ts; + if (client != null && client.getConfig() != null) client.getConfig().setTrustStorePath(ts); + } + public void SetJSSETrustStore(String ts) { setJSSETrustStore(ts); } + public String getJSSETrustStore() { return jsseTrustStore; } + public String GetJSSETrustStore() { return jsseTrustStore; } + + public void setJSSETrustStoreType(String type) { + this.jsseTrustStoreType = type; + if (client != null && client.getConfig() != null) client.getConfig().setTrustStoreType(type); + } + public void SetJSSETrustStoreType(String type) { setJSSETrustStoreType(type); } + public String getJSSETrustStoreType() { return jsseTrustStoreType; } + public String GetJSSETrustStoreType() { return jsseTrustStoreType; } + + public void setJSSETrustStorePassword(String pwd) { + this.jsseTrustStorePassword = pwd; + if (client != null && client.getConfig() != null) client.getConfig().setTrustStorePassword(pwd); + } + public void SetJSSETrustStorePassword(String pwd) { setJSSETrustStorePassword(pwd); } + public String getJSSETrustStorePassword() { return jsseTrustStorePassword; } + public String GetJSSETrustStorePassword() { return jsseTrustStorePassword; } + + public haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl getSSLSessionImpl() { + return (session != null) ? session.getSSLSessionImpl() : new haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl(this); + } + public haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl GetSSLSessionImpl() { + return getSSLSessionImpl(); + } + + public void setProxy(String proxy) { this.proxyServerName = proxy; } + public void SetProxy(String proxy) { setProxy(proxy); } + public String getProxy() { return proxyServerName; } + public String GetProxy() { return proxyServerName; } + + public void setProxyType(String type) { this.proxyType = type; } + public void SetProxyType(String type) { setProxyType(type); } + public String getProxyType() { return proxyType; } + public String GetProxyType() { return proxyType; } + + public void setProxyServerName(String name) { this.proxyServerName = name; } + public void SetProxyServerName(String name) { setProxyServerName(name); } + public String getProxyServerName() { return proxyServerName; } + public String GetProxyServerName() { return proxyServerName; } + + public void setProxyServerPort(String port) { this.proxyServerPort = port; } + public void SetProxyServerPort(String port) { setProxyServerPort(port); } + public String getProxyServerPort() { return proxyServerPort; } + public String GetProxyServerPort() { return proxyServerPort; } + + public void setProxyUserID(String uid) { this.proxyUserId = uid; } + public void SetProxyUserID(String uid) { setProxyUserID(uid); } + public String getProxyUserID() { return proxyUserId; } + public String GetProxyUserID() { return proxyUserId; } + + public void setProxyUserPassword(String pwd) { this.proxyUserPassword = pwd; } + public void SetProxyUserPassword(String pwd) { setProxyUserPassword(pwd); } + public String getProxyUserPassword() { return proxyUserPassword; } + public String GetProxyUserPassword() { return proxyUserPassword; } + + public void setProxyAuthenMethod(String method) { this.proxyAuthenMethod = method; } + public void SetProxyAuthenMethod(String method) { setProxyAuthenMethod(method); } + public String getProxyAuthenMethod() { return proxyAuthenMethod; } + public String GetProxyAuthenMethod() { return proxyAuthenMethod; } + + public void setProxySecurityProtocol(String prot) { this.proxySecurityProtocol = prot; } + public void SetProxySecurityProtocol(String prot) { setProxySecurityProtocol(prot); } + public String getProxySecurityProtocol() { return proxySecurityProtocol; } + public String GetProxySecurityProtocol() { return proxySecurityProtocol; } + + public void setProperties(Properties props) { + if (props != null) { + this.properties.putAll(props); + convertData(this.properties); + } + } + public void SetProperties(Properties props) { setProperties(props); } + public Properties getProperties() { return properties; } + public Properties GetProperties() { return properties; } + @Override public String toString() { return String.format("ECLConnection[host=%s, port=%d, state=%s, lu=%s, ssl=%b]", diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErr.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErr.java new file mode 100644 index 0000000..aa11f04 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLErr.java @@ -0,0 +1,70 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Conforms to IBM Host On-Demand ECLErr checked exception. + */ +public class ECLErr extends Exception { + + private static final long serialVersionUID = 1L; + + private String tag = ""; + private String id = ""; + private String text = ""; + + public ECLErr() { + super(); + } + + public ECLErr(String text) { + super(text); + this.text = text; + } + + public ECLErr(String tag, String id, String text) { + super(formatMsg(tag, id, text)); + this.tag = tag; + this.id = id; + 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(Throwable cause) { + super(cause); + } + + public ECLErr(String message, Throwable cause) { + super(message, cause); + this.text = message; + } + + private static String formatMsg(String tag, String id, String text) { + StringBuilder sb = new StringBuilder(); + if (tag != null && !tag.isEmpty()) sb.append(tag).append(" "); + if (id != null && !id.isEmpty()) sb.append(id).append(": "); + if (text != null) sb.append(text); + return sb.toString(); + } + + public String getTag() { return tag; } + public String GetTag() { return tag; } + + public String getID() { return id; } + public String GetID() { return id; } + + public String getText() { return text; } + public String GetText() { return text; } + + public String getErrorText() { return getMessage(); } + public String GetErrorText() { return getMessage(); } + + @Override + public String toString() { + return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + "]"; + } +} 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 f78f3bd..5b1a717 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java @@ -106,6 +106,36 @@ public class ECLField { } public boolean IsDisplay() { return isDisplay(); } + public boolean isPenDetectable() { + return !isProtected() && (getLiveAttribute() & 0x0C) != 0; + } + public boolean IsPenDetectable() { return isPenDetectable(); } + + public void setFieldAttribute(char attr) { + if (ps != null && ps.getScreenBuffer() != null) { + ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos); + if (cell != null) { + cell.fa = (byte) (attr & 0xFF); + ps.getScreenBuffer().markAllChanged(); + ps.getScreenBuffer().updateDisplaySnapshot(); + } + } + } + public void SetFieldAttribute(char attr) { setFieldAttribute(attr); } + + public int getStartFieldPos() { + return startPos; + } + public int GetStartFieldPos() { return getStartFieldPos(); } + + public char[] copyPlanes(int plane) { + if (length <= 0 || ps == null) return new char[0]; + char[] buf = new char[length]; + ps.getPlane(plane, buf, dataStart, length); + return buf; + } + public char[] CopyPlanes(int plane) { return copyPlanes(plane); } + public boolean isPenSelectable() { return faIsSelectable(getLiveAttribute() & 0xFF); } 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 a70aea6..59a0874 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java @@ -225,4 +225,72 @@ public class ECLFieldList { return null; } public ECLField FindField(String text, int startPos) { return findField(text, startPos); } + + public boolean matchAttributes(ECLField f, int mask) { + if (f == null) return false; + if (mask == 0) return true; + boolean ok = true; + if ((mask & 0x01) == 1) { + ok &= f.IsModified(); + } else if ((mask & 0x100) == 0x100) { + ok &= !f.IsModified(); + } + if ((mask & 0x02) == 2) { + ok &= !f.IsNumeric(); + } else if ((mask & 0x200) == 0x200) { + ok &= f.IsNumeric(); + } + if ((mask & 0x10) == 0x10) { + ok &= f.IsHighIntensity(); + } else if ((mask & 0x1000) == 0x1000) { + ok &= !f.IsHighIntensity(); + } + if ((mask & 0x20) == 0x20) { + ok &= f.IsProtected(); + } else if ((mask & 0x2000) == 0x2000) { + ok &= !f.IsProtected(); + } + if ((mask & 0x40) == 0x40) { + ok &= f.IsDisplay(); + } else if ((mask & 0x4000) == 0x4000) { + ok &= !f.IsDisplay(); + } + if ((mask & 0x80) == 0x80) { + ok &= f.IsPenDetectable(); + } else if ((mask & 0x8000) == 0x8000) { + ok &= !f.IsPenDetectable(); + } + return ok; + } + public boolean MatchAttributes(ECLField f, int mask) { return matchAttributes(f, mask); } + + public synchronized ECLField locateField(int attrMask, ECLField prev) { + if (fields.isEmpty()) return null; + int startIndex = 0; + if (prev != null) { + int idx = fields.indexOf(prev); + if (idx >= 0 && idx + 1 < fields.size()) { + startIndex = idx + 1; + } else { + return null; + } + } + for (int i = startIndex; i < fields.size(); i++) { + ECLField f = fields.get(i); + if (matchAttributes(f, attrMask)) { + return f; + } + } + return null; + } + public ECLField LocateField(int attrMask, ECLField prev) { return locateField(attrMask, prev); } + + public synchronized void copyPlanes(int plane) { + if (ps != null && screen != null) { + int size = screen.getRows() * screen.getCols(); + char[] buf = new char[size]; + ps.getPlane(plane, buf, 0, size); + } + } + public void CopyPlanes(int plane) { copyPlanes(plane); } } 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 caa5c90..6b9f5d7 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java @@ -15,14 +15,79 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; */ public class ECLOIA implements ECLConstants { + public static final int INHIBIT_NOTINHIBITED = 0; + public static final int INHIBIT_SYSTEMWAIT = 1; + public static final int INHIBIT_COMMCHECK = 2; + public static final int INHIBIT_PROGCHECK = 3; + public static final int INHIBIT_MACHCHECK = 4; + public static final int INHIBIT_OTHERINHIBIT = 5; + + public static final int STATE_NO_CHANGE = 0; + public static final int STATE_CONTROLLER_READY = 1; + public static final int STATE_ONLINE = 2; + public static final int STATE_A_ONLINE = 2; + public static final int STATE_MY_JOB = 4; + public static final int STATE_OP_SYS = 8; + public static final int STATE_UNOWNED = 16; + public static final int STATE_TIME = 32; + public static final int STATE_SYS_LOCK = 64; + public static final int STATE_COMM_CHECK = 128; + public static final int STATE_PROG_CHECK = 256; + public static final int STATE_ELSEWHERE = 512; + public static final int STATE_FN_MINUS = 1024; + public static final int STATE_WHAT_KEY = 2048; + public static final int STATE_MORE_THAN = 4096; + public static final int STATE_SYM_MINUS = 8192; + public static final int STATE_INPUT_ERROR = 16384; + public static final int STATE_OIA_SUPPRESS = 32768; + public static final int STATE_HOST_CONTROL = 65536; + public static final int STATE_HOST_WRITE = 131072; + public static final int STATE_HOD_CONTROL = 262144; + public static final int STATE_DO_NOT_ENTER = 32736; + public static final int STATE_CLEAR_DO_NOT_ENTER = -32737; + public static final int STATE_INSERT = 32768; + public static final int STATE_UPSHIFT = 65536; + public static final int STATE_APL = 131072; + public static final int STATE_GR_CURSOR = 262144; + public static final int STATE_CAPSLOCK = 524288; + public static final int STATE_NUMLOCK = 0x100000; + public static final int STATE_COMM_ERR_REM = 0x200000; + public static final int STATE_MSG_WAITING = 0x400000; + public static final int STATE_SCREEN_REVERSE = 0x800000; + public static final int STATE_LANGUAGE_LAYER = 0x1000000; + public static final int STATE_CURSOR_DIRECTION = 0x2000000; + public static final int STATE_AUTOREVERSE = 0x4000000; + public static final int STATE_NUMFIELD = 0x8000000; + public static final int STATE_AUTOPUSH = 0x10000000; + public static final int STATE_AUTOSHAPE = 0x20000000; + public static final int STATE_PUSH = 0x40000000; + public static final int STATE_TEXT_MODE = 0x40000000; + public static final int STATE_COLUMNHEAD = 0x2000000; + public static final int STATE_B_ONLINE = 0x40000000; + public static final int STATE_ENCRYPT = Integer.MIN_VALUE; + public static final long STATE_DOC_MODE = 0x100000000L; + public static final long STATE_WORDWRAP = 0x200000000L; + + public static final int STOP_UNREGISTER = 1; + public static final int STOP_DISCONNECT = 2; + public static final int STOP_ERROR = 3; + private final ScreenBuffer screen; private final InputProcessor inputProcessor; private final TelnetFSM fsm; - private final List listeners = new ArrayList<>(); + private final List listeners = new java.util.concurrent.CopyOnWriteArrayList<>(); private final List oiaListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); - public interface ECLOIANotify { - void onOIAChanged(ECLOIA oia); + protected long state = STATE_ONLINE | STATE_CONTROLLER_READY; + protected long previousState = 0L; + protected String stateData = null; + private ECLOIABIDI oiaBidi; + private ECLOIATHAI oiaThai; + private ECLOIAHindi oiaHindi; + private String oiaText = ""; + private boolean oiaInvisible = false; + + public interface ECLOIANotify extends haus.nightmare.lib3270j.ecl.ECLOIANotify { } public ECLOIA(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { @@ -35,14 +100,33 @@ public class ECLOIA implements ECLConstants { } } - public synchronized void registerOIAEvent(ECLOIANotify listener) { + public ECLOIA(ECLSession session) { + this(session != null && session.getClient() != null ? session.getClient().getScreenBuffer() : null, + session != null && session.getClient() != null ? session.getClient().getInputProcessor() : null, + session != null && session.getClient() != null ? session.getClient().getTelnetFSM() : null); + } + + public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) { if (listener != null && !listeners.contains(listener)) { listeners.add(listener); } } - public synchronized void unregisterOIAEvent(ECLOIANotify listener) { - listeners.remove(listener); + public void registerOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) { + RegisterOIAEvent(listener); + } + + public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) { + if (listener != null) { + listeners.remove(listener); + try { + listener.OIANotifyStop(this, STOP_UNREGISTER); + } catch (Exception ignored) {} + } + } + + public void unregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) { + UnregisterOIAEvent(listener); } public void RegisterOIAEvent(ECLOIAListener listener) { @@ -52,7 +136,12 @@ public class ECLOIA implements ECLConstants { } public void UnregisterOIAEvent(ECLOIAListener listener) { - oiaListeners.remove(listener); + if (listener != null) { + oiaListeners.remove(listener); + try { + listener.OIANotifyStop(this, STOP_UNREGISTER); + } catch (Exception ignored) {} + } } public void registerOIAListener(ECLOIAListener listener) { @@ -63,17 +152,43 @@ public class ECLOIA implements ECLConstants { UnregisterOIAEvent(listener); } - private synchronized void notifyOIAChanged() { - for (ECLOIANotify l : listeners) { + public void notifyOIAError(ECLErr err) { + for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) { try { - l.onOIAChanged(this); + l.OIANotifyError(this, err); } catch (Exception ignored) {} } - ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(), - getAlphanumericType(), isInsertMode(), getStatusString()); for (ECLOIAListener l : oiaListeners) { try { - l.oiaChanged(event); + l.OIANotifyError(this, err); + } catch (Exception ignored) {} + } + } + + public void notifyOIAStop(int reason) { + for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) { + try { + l.OIANotifyStop(this, reason); + } catch (Exception ignored) {} + } + for (ECLOIAListener l : oiaListeners) { + try { + l.OIANotifyStop(this, reason); + } catch (Exception ignored) {} + } + } + + private synchronized void notifyOIAChanged() { + ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(), + getAlphanumericType(), isInsertMode(), getStatusString()); + for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) { + try { + l.OIANotifyEvent(event); + } catch (Exception ignored) {} + } + for (ECLOIAListener l : oiaListeners) { + try { + l.OIANotifyEvent(event); if (inputProcessor != null) { l.oiaLockStateChanged(event); } @@ -362,4 +477,140 @@ public class ECLOIA implements ECLConstants { public boolean WaitForTransition(long timeoutMs) { return waitForTransition(timeoutMs); } + + public synchronized long GetStatusFlagsEx() { + long s = this.state; + if (isInsertMode()) s |= STATE_INSERT; + if (isNumeric()) s |= STATE_NUMFIELD; + if (screen != null && screen.isEntryAssistDOCmode()) s |= STATE_DOC_MODE; + if (screen != null && screen.isEntryAssistWordWrap()) s |= STATE_WORDWRAP; + if (isXSystem()) s |= STATE_SYS_LOCK; + if (isXComm()) s |= STATE_COMM_CHECK; + return s; + } + public int GetStatusFlags() { + return (int) GetStatusFlagsEx(); + } + public int getStatusFlags() { return GetStatusFlags(); } + public long getStatusFlagsEx() { return GetStatusFlagsEx(); } + + public synchronized void setBitmaskState(long flag, boolean on) { + this.previousState = this.state; + if (on) { + this.state |= flag; + } else { + this.state &= ~flag; + } + notifyOIAChanged(); + } + + public synchronized void setDoNotEnter(int n, int n2) { + long l = 0L; + switch (n) { + case 7: l = 32L; break; // STATE_TIME + case 8: l = 64L; break; // STATE_SYS_LOCK + case 9: l = 0x200080L; break; // STATE_COMM_CHECK | STATE_COMM_ERR_REM + case 10: l = 256L; break; // STATE_PROG_CHECK + case 13: l = 512L; break; // STATE_ELSEWHERE + case 12: l = 1024L; break; // STATE_FN_MINUS + case 11: l = 2048L; break; // STATE_WHAT_KEY + case 14: l = 4096L; break; // STATE_MORE_THAN + case 15: l = 8192L; break; // STATE_SYM_MINUS + case 55: l = 16384L; break; // STATE_INPUT_ERROR + default: l = 64L; break; + } + this.previousState = this.state; + this.state &= 0xFFFFFFFFFFFF801FL; + this.state |= l; + this.stateData = String.valueOf(n2); + notifyOIAChanged(); + } + + public synchronized void clearDoNotEnter() { + this.previousState = this.state; + long l = this.state & 0x7FE0L; + this.state &= ~l; + this.stateData = null; + this.state &= ~0x200000L; + notifyOIAChanged(); + } + + public synchronized void setReadyConnect(int n, String string) { + long l = 0L; + boolean clear = false; + switch (n) { + case 1: l = 1L; break; // STATE_CONTROLLER_READY + case 2: l = 2L; break; // STATE_ONLINE + case 3: l = 0x40000000L; break; // STATE_B_ONLINE + case 4: l = 4L; break; // STATE_MY_JOB + case 5: l = 8L; break; // STATE_OP_SYS + case 6: l = 16L; break; // STATE_UNOWNED + case 69: + l = 0x80000000L; // STATE_ENCRYPT + if (" ".equals(string) && (this.state & 0x80000000L) != 0L) { + clear = true; + } + break; + default: break; + } + if (l != 0L) { + this.previousState = this.state; + this.state &= 0xFFFFFFFFFFFFFFE3L; + if (clear) { + this.state &= ~l; + } else { + this.state |= l; + } + this.stateData = string; + notifyOIAChanged(); + } + } + + public synchronized void setMsgWaiting(boolean bl) { + setBitmaskState(STATE_MSG_WAITING, bl); + } + + public synchronized void setOiaInvisible() { + this.oiaInvisible = true; + setBitmaskState(STATE_OIA_SUPPRESS, true); + } + + public synchronized void setOiaHostControl() { + setBitmaskState(STATE_HOST_CONTROL, true); + setBitmaskState(STATE_HOD_CONTROL, false); + } + + public synchronized void setOiaHODControl() { + setBitmaskState(STATE_HOD_CONTROL, true); + setBitmaskState(STATE_HOST_CONTROL, false); + } + + public synchronized void writeToOIA(String text) { + this.oiaText = text != null ? text : ""; + notifyOIAChanged(); + } + + public synchronized ECLOIABIDI GetECLOIABIDI() { + if (oiaBidi == null) { + oiaBidi = new ECLOIABIDI(screen, inputProcessor, fsm); + } + return oiaBidi; + } + public ECLOIABIDI getECLOIABIDI() { return GetECLOIABIDI(); } + + public synchronized ECLOIATHAI GetECLOIATHAI() { + if (oiaThai == null) { + oiaThai = new ECLOIATHAI(screen, inputProcessor, fsm); + } + return oiaThai; + } + public ECLOIATHAI getECLOIATHAI() { return GetECLOIATHAI(); } + + public synchronized ECLOIAHindi GetECLOIAHindi() { + if (oiaHindi == null) { + oiaHindi = new ECLOIAHindi(screen, inputProcessor, fsm); + } + return oiaHindi; + } + public ECLOIAHindi getECLOIAHindi() { return GetECLOIAHindi(); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIABIDI.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIABIDI.java new file mode 100644 index 0000000..9b2c397 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIABIDI.java @@ -0,0 +1,42 @@ +package haus.nightmare.lib3270j.ecl; + +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.telnet.TelnetFSM; + +/** + * Conforms to IBM Host On-Demand ECLOIABIDI. + */ +public class ECLOIABIDI extends ECLOIA { + + private int shapeValue; + + public ECLOIABIDI(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { + super(screen, inputProcessor, fsm); + } + + public synchronized void setBIDIMode(int mode, boolean on) { + switch (mode) { + case 78: setStateBIDI(0x40000000, on); break; + case 70: setStateBIDI(0x800000, on); break; + case 71: setStateBIDI(0x1000000, on); break; + case 79: + case 72: setStateBIDI(0x2000000, on); break; + case 73: setStateBIDI(0x4000000, on); break; + case 74: setStateBIDI(0x8000000, on); break; + case 75: setStateBIDI(0x10000000, on); break; + case 77: setStateBIDI(0x40000000, on); break; + case 76: setStateBIDI(0x20000000, on); break; + default: break; + } + } + + public synchronized void setBIDIShapeMode(int shape) { + this.shapeValue = shape; + setStateBIDI(0x20000000, shape == 0); + } + + private void setStateBIDI(int flag, boolean on) { + setBitmaskState(flag, on); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAEvent.java index cb6d957..8628d0d 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAEvent.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAEvent.java @@ -4,6 +4,7 @@ import java.util.EventObject; /** * Event object dispatched on Operator Information Area (ECLOIA) status changes. + * Conforms 1:1 to IBM Host On-Demand ECLOIAEvent specification. */ public class ECLOIAEvent extends EventObject { @@ -27,29 +28,48 @@ public class ECLOIAEvent extends EventObject { public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType, boolean insertMode, String statusString) { - super(source); + super(source != null ? source : "ECLOIA"); this.eventType = eventType; this.inputInhibited = inputInhibited; this.alphanumericType = alphanumericType; this.insertMode = insertMode; - this.statusString = statusString; + this.statusString = statusString != null ? statusString : ""; } public int getEventType() { return eventType; } + public int GetType() { return eventType; } + public int getType() { return eventType; } + public int getInputInhibited() { return inputInhibited; } + public int GetInputInhibited() { return inputInhibited; } + public int getInhibitedReason() { return inputInhibited; } + public int GetInhibitedReason() { return inputInhibited; } + public int getAlphanumericType() { return alphanumericType; } + public int GetAlphanumericType() { return alphanumericType; } + public boolean isInsertMode() { return insertMode; } + public boolean IsInsertMode() { return insertMode; } + public String getStatusString() { return statusString; } + public String GetStatusString() { return statusString; } public boolean isInputInhibited() { return inputInhibited != ECLConstants.INHIBIT_NOT_INHIBITED; } + public boolean IsInputInhibited() { + return isInputInhibited(); + } public ECLOIA getOIA() { return (getSource() instanceof ECLOIA) ? (ECLOIA) getSource() : null; } + public ECLOIA GetOIA() { + return getOIA(); + } + @Override public String toString() { return String.format("ECLOIAEvent[type=%d, status='%s', inhibited=%d, insert=%b]", diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAHindi.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAHindi.java new file mode 100644 index 0000000..1338b6f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAHindi.java @@ -0,0 +1,21 @@ +package haus.nightmare.lib3270j.ecl; + +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.telnet.TelnetFSM; + +/** + * Conforms to IBM Host On-Demand ECLOIAHindi. + */ +public class ECLOIAHindi extends ECLOIA { + + public ECLOIAHindi(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { + super(screen, inputProcessor, fsm); + } + + public synchronized void setHindiMode(int mode, boolean on) { + if (mode == 71) { + setBitmaskState(0x1000000, on); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAListener.java index 788f7eb..5f76ba0 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAListener.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIAListener.java @@ -2,14 +2,39 @@ package haus.nightmare.lib3270j.ecl; /** * Listener interface for Operator Information Area (ECLOIA) status change events. + * Conforms 1:1 to IBM Host On-Demand ECLOIAListener specification. */ public interface ECLOIAListener { + /** + * Primary HoD notification callback invoked when OIA status changes. + * @param event ECLOIAEvent containing OIA status + */ + default void OIANotifyEvent(ECLOIAEvent event) { + oiaChanged(event); + } + + /** + * Called when an error condition occurs during OIA event generation. + * @param oia ECLOIA instance + * @param err ECLErr error descriptor + */ + default void OIANotifyError(ECLOIA oia, ECLErr err) {} + + /** + * Called when OIA event generation has stopped. + * @param oia ECLOIA instance + * @param reason Stop reason code + */ + default void OIANotifyStop(ECLOIA oia, int reason) {} + + // ========== Backward-Compatibility Bridge Methods ========== + /** * Called when the OIA status, input inhibited flag, or keyboard lock state changes. * @param event ECLOIAEvent containing OIA status information */ - void oiaChanged(ECLOIAEvent event); + default void oiaChanged(ECLOIAEvent event) {} /** * Called when the input inhibited condition changes specifically. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIANotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIANotify.java new file mode 100644 index 0000000..6126661 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIANotify.java @@ -0,0 +1,29 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Interface for receiving Operator Information Area (ECLOIA) notifications. + * Conforms 1:1 to IBM Host On-Demand ECLOIANotify specification. + */ +@FunctionalInterface +public interface ECLOIANotify { + + /** + * Primary HoD notification callback invoked when OIA status changes. + * @param event ECLOIAEvent containing OIA status + */ + void OIANotifyEvent(ECLOIAEvent event); + + /** + * Called when an error condition occurs during OIA event generation. + * @param oia ECLOIA instance + * @param err ECLErr error descriptor + */ + default void OIANotifyError(ECLOIA oia, ECLErr err) {} + + /** + * Called when OIA event generation has stopped. + * @param oia ECLOIA instance + * @param reason Stop reason code + */ + default void OIANotifyStop(ECLOIA oia, int reason) {} +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIATHAI.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIATHAI.java new file mode 100644 index 0000000..9699225 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIATHAI.java @@ -0,0 +1,21 @@ +package haus.nightmare.lib3270j.ecl; + +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.telnet.TelnetFSM; + +/** + * Conforms to IBM Host On-Demand ECLOIATHAI. + */ +public class ECLOIATHAI extends ECLOIA { + + public ECLOIATHAI(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) { + super(screen, inputProcessor, fsm); + } + + public synchronized void setTHAIMode(int mode, boolean on) { + if (mode == 71) { + setBitmaskState(0x1000000, on); + } + } +} 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 3220cb1..1dadc65 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java @@ -18,11 +18,41 @@ public class ECLPS implements ECLConstants { private final EbcdicTranslator translator; private final ECLFieldList fieldList; + private ECLSession session; + private Object screenHistory; + private final ECLPSGraphicsServices graphicsServices; + private final ECLPSBIDIServices bidiServices; + private final ECLPSHindiServices hindiServices; + private final ECLPSTHAIServices thaiServices; + public static final int USER_EVENTS = 1; + public static final int HOST_EVENTS = 2; + public static final int ALL_EVENTS = 3; + + public static final int STOP_UNREGISTER = 1; + public static final int STOP_DISCONNECT = 2; + public static final int STOP_ERROR = 3; + + private boolean cursorVisible = true; + private final java.util.concurrent.atomic.AtomicInteger ringCounter = new java.util.concurrent.atomic.AtomicInteger(0); + private final java.util.Map descriptorListeners = new java.util.concurrent.ConcurrentHashMap<>(); + private final java.util.Map listenerEventTypes = new java.util.concurrent.ConcurrentHashMap<>(); + public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) { this.screen = screen; this.inputProcessor = inputProcessor; this.translator = translator; this.fieldList = new ECLFieldList(this, screen); + this.graphicsServices = new DefaultPSGraphicsServices(this); + this.bidiServices = new DefaultPSBIDIServices(this); + this.hindiServices = new DefaultPSHindiServices(this); + this.thaiServices = new DefaultPSTHAIServices(this); + } + + public ECLPS(ECLSession session) { + this(session != null && session.getClient() != null ? session.getClient().getScreenBuffer() : null, + session != null && session.getClient() != null ? session.getClient().getInputProcessor() : null, + session != null && session.getClient() != null ? session.getClient().getTranslator() : null); + this.session = session; } public ScreenBuffer getScreenBuffer() { return screen; } @@ -46,6 +76,35 @@ public class ECLPS implements ECLConstants { this.nvtMode = nvt; } + public ECLSession GetParent() { return session; } + public ECLSession getParent() { return session; } + public void setSession(ECLSession session) { this.session = session; } + public ECLSession getSession() { return session; } + + public haus.nightmare.lib3270j.tn3270.DS3270 GetDS() { + return session != null ? session.GetDS() : null; + } + public haus.nightmare.lib3270j.tn3270.DS3270 getDS() { return GetDS(); } + + public void setScreenHistory(Object hist) { this.screenHistory = hist; } + public Object getScreenHistory() { return screenHistory; } + + public ECLPSGraphicsServices GetPSGraphicsServices() { return graphicsServices; } + public ECLPSGraphicsServices GetECLPSGraphicsServices() { return graphicsServices; } + public ECLPSGraphicsServices getGraphicsServices() { return graphicsServices; } + + public ECLPSBIDIServices GetPSBIDIServices() { return bidiServices; } + public ECLPSBIDIServices GetECLPSBIDIServices() { return bidiServices; } + public ECLPSBIDIServices getBIDIServices() { return bidiServices; } + + public ECLPSHindiServices GetPSHindiServices() { return hindiServices; } + public ECLPSHindiServices GetECLPSHindiServices() { return hindiServices; } + public ECLPSHindiServices getHindiServices() { return hindiServices; } + + public ECLPSTHAIServices GetPSTHAIServices() { return thaiServices; } + 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(); } @@ -108,6 +167,40 @@ public class ECLPS implements ECLConstants { return copyLen; } + public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) { + if (cArray == null || len <= 0 || screen == null) return 0; + int minR = Math.max(1, Math.min(sRow, eRow)); + int maxR = Math.min(getRows(), Math.max(sRow, eRow)); + int minC = Math.max(1, Math.min(sCol, eCol)); + int maxC = Math.min(getCols(), Math.max(sCol, eCol)); + + int width = maxC - minC + 1; + int count = 0; + + for (int r = minR; r <= maxR; r++) { + char[] rowBuf = new char[width]; + int sAddr = (r - 1) * getCols() + (minC - 1); + getPlane(plane, rowBuf, sAddr, width); + int copyLen = Math.min(width, len - count); + if (copyLen <= 0) break; + System.arraycopy(rowBuf, 0, cArray, count, copyLen); + count += copyLen; + } + return count; + } + + public int getScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) { + return GetScreenRect(cArray, len, sRow, sCol, eRow, eCol, plane); + } + + public String GetScreenRect(int startRow, int startCol, int endRow, int endCol) { + return copyString(startRow - 1, startCol - 1, endRow - 1, endCol - 1); + } + + public String getScreenRect(int startRow, int startCol, int endRow, int endCol) { + return GetScreenRect(startRow, startCol, endRow, endCol); + } + /** * Get a string of characters from the presentation space starting at address pos. */ @@ -126,6 +219,14 @@ public class ECLPS implements ECLConstants { return getString(pos, length); } + public String getScreen(int pos, int length) { + return getString(pos, length); + } + + public String GetScreen(int pos, int length) { + return getString(pos, length); + } + /** * Insert text directly into unprotected fields in the presentation space starting at pos. */ @@ -151,6 +252,23 @@ public class ECLPS implements ECLConstants { setText(text, pos); } + /** + * Insert text at 1-based (row, col) position. + */ + public void SetText(String text, int row, int col) { + int r = (row > 0) ? row - 1 : 0; + int c = (col > 0) ? col - 1 : 0; + setText(text, r, c); + } + + /** + * Insert text at 1-based linear buffer position. + */ + public void SetText(String text, int pos) { + int p = (pos > 0) ? pos - 1 : 0; + setText(text, p); + } + /** * Search for a string in the presentation space (0-based indexing). * Returns 0-based position, or -1 if not found. @@ -460,6 +578,88 @@ public class ECLPS implements ECLConstants { return pasteLineWrap(text, startPos, endCol, wordWrap); } + public synchronized int pasteInDocMode(String text, int row, int col) { + if (text == null || text.isEmpty()) return 0; + int r = (row > 0) ? row - 1 : 0; + int c = (col > 0) ? col - 1 : 0; + if (!isEntryAssistDOCmode()) { + return pasteString(text, r, c); + } + int pos = (screen != null) ? screen.rowColToAddress(r, c) : 0; + return pasteLineWrap(text, pos, getEntryAssistEndColumn(), isEntryAssistWordWrap()); + } + public int PasteInDocMode(String text, int row, int col) { return pasteInDocMode(text, row, col); } + + public boolean enableTrimOnPaste() { + String prop = System.getProperty("trimPastedChar"); + if (prop == null && session != null && session.getProperties() != null) { + prop = session.getProperties().getProperty("trimPastedChar"); + } + return "true".equalsIgnoreCase(prop); + } + public boolean EnableTrimOnPaste() { return enableTrimOnPaste(); } + + public String handleTabs(String string, String string2) { + if (string == null || string2 == null) { + return string; + } + if (string2.equals("2")) { + int n = 4; + if (session != null && session.getProperties() != null) { + String sp = session.getProperties().getProperty("pasteTabSpaces"); + if (sp != null) { + try { n = Integer.parseInt(sp); } catch (NumberFormatException ignored) {} + } + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < string.length(); ++i) { + char c = string.charAt(i); + if (c == '\t') { + for (int j = 0; j < n; ++j) { + sb.append(' '); + } + } else { + sb.append(c); + } + } + return sb.toString(); + } else if (string2.equals("1")) { + int n = 8; + if (session != null && session.getProperties() != null) { + String colStr = session.getProperties().getProperty("pasteTabColumns"); + if (colStr != null) { + try { n = Integer.parseInt(colStr); } catch (NumberFormatException ignored) {} + } + } + StringBuilder sb = new StringBuilder(); + int n4 = 0; + for (int i = 0; i < string.length(); ++i) { + char c = string.charAt(i); + ++n4; + if (c == '\t') { + while (n4 < n + 1) { + sb.append(' '); + ++n4; + } + n4 = 0; + continue; + } + if (c == '\n') { + sb.append(c); + n4 = 0; + continue; + } + sb.append(c); + if (n4 == n && i + 1 < string.length() && string.charAt(i + 1) != '\t') { + n4 = 0; + } + } + return sb.toString(); + } + return string; + } + public String HandleTabs(String string, String string2) { return handleTabs(string, string2); } + // ========== Entry Assist & DOC Mode Operations ========== public boolean isEntryAssistDOCmode() { return screen != null && screen.isEntryAssistDOCmode(); } @@ -562,28 +762,105 @@ public class ECLPS implements ECLConstants { private final java.util.List psListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + public boolean isCursorVisible() { return cursorVisible; } + public boolean GetCursorVisible() { return cursorVisible; } + public void setCursorVisible(boolean visible) { this.cursorVisible = visible; } + public void SetCursorVisible(boolean visible) { setCursorVisible(visible); } + + public int getRingCounter() { return ringCounter.get(); } + public int GetRingCounter() { return getRingCounter(); } + public void RegisterPSEvent(ECLPSListener listener) { - if (listener != null && !psListeners.contains(listener)) { - psListeners.add(listener); - } + RegisterPSEvent(listener, ALL_EVENTS); } public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); } + public void RegisterPSEvent(ECLPSListener listener, int eventType) { + if (listener != null) { + listenerEventTypes.put(listener, eventType); + if (!psListeners.contains(listener)) { + psListeners.add(listener); + } + } + } + + public void registerPSEvent(ECLPSListener listener, int eventType) { + RegisterPSEvent(listener, eventType); + } + + public void RegisterPSEvent(ECLPSListener listener, ECLScreenDesc desc) { + RegisterPSEvent(listener, desc, ALL_EVENTS); + } + + public void registerPSEvent(ECLPSListener listener, ECLScreenDesc desc) { + RegisterPSEvent(listener, desc); + } + + public void RegisterPSEvent(ECLPSListener listener, ECLScreenDesc desc, int eventType) { + if (listener != null) { + if (desc != null) { + descriptorListeners.put(listener, desc); + } else { + descriptorListeners.remove(listener); + } + listenerEventTypes.put(listener, eventType); + if (!psListeners.contains(listener)) { + psListeners.add(listener); + } + } + } + + public void registerPSEvent(ECLPSListener listener, ECLScreenDesc desc, int eventType) { + RegisterPSEvent(listener, desc, eventType); + } + public void UnregisterPSEvent(ECLPSListener listener) { - psListeners.remove(listener); + if (listener != null) { + psListeners.remove(listener); + descriptorListeners.remove(listener); + listenerEventTypes.remove(listener); + try { + listener.PSNotifyStop(this, STOP_UNREGISTER); + } catch (Exception ignored) {} + } } public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); } + public void UnregisterPSEvent(ECLPSListener listener, ECLScreenDesc desc) { + UnregisterPSEvent(listener); + } + + public void unregisterPSEvent(ECLPSListener listener, ECLScreenDesc desc) { + UnregisterPSEvent(listener); + } + + public void UnregisterPSEvent(ECLPSListener listener, int eventType) { + UnregisterPSEvent(listener); + } + + public void unregisterPSEvent(ECLPSListener listener, int eventType) { + UnregisterPSEvent(listener); + } + public void notifyPSEvent(ECLPSEvent event) { for (ECLPSListener l : psListeners) { + ECLScreenDesc desc = descriptorListeners.get(l); + if (desc != null && !desc.Matches(this, session != null ? session.GetOIA() : null)) { + continue; + } + int filter = listenerEventTypes.getOrDefault(l, ALL_EVENTS); + int evtCategory = event.GetType(); + if (filter != ALL_EVENTS && (filter & evtCategory) == 0) { + continue; + } try { - l.psChanged(event); + l.PSNotifyEvent(event); if (event.getEventType() == ECLPSEvent.PS_CURSOR) { l.psCursorMoved(event); } else if (event.getEventType() == ECLPSEvent.PS_ALARM) { @@ -597,11 +874,34 @@ public class ECLPS implements ECLConstants { } } + public void notifyPSError(ECLErr err) { + for (ECLPSListener l : psListeners) { + try { + l.PSNotifyError(this, err); + } catch (Exception ignored) {} + } + } + + public void notifyPSStop(int reason) { + for (ECLPSListener l : psListeners) { + try { + l.PSNotifyStop(this, reason); + } catch (Exception ignored) {} + } + } + public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full) { + notifyPSUpdate(startRow, startCol, endRow, endCol, full, HOST_EVENTS, false); + } + + public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full, int type, boolean startPrinter) { int r = (screen != null) ? screen.getRows() : 0; int c = (screen != null) ? screen.getCols() : 0; int cur = (screen != null) ? screen.getCursorAddress() : 0; - notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, startRow, startCol, endRow, endCol, cur, cur, r, c, full)); + int ring = ringCounter.incrementAndGet(); + ECLPSEvent evt = new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, type, startRow, startCol, endRow, endCol, + cur, cur, r, c, full, cursorVisible, ring, startPrinter, null); + notifyPSEvent(evt); } public void notifyCursorMoved(int oldAddress, int newAddress) { @@ -609,16 +909,22 @@ public class ECLPS implements ECLConstants { int c = (screen != null) ? screen.getCols() : 0; int row = (c > 0) ? newAddress / c : 0; int col = (c > 0) ? newAddress % c : 0; - notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, row, col, row, col, oldAddress, newAddress, r, c, false)); + int ring = ringCounter.incrementAndGet(); + notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, USER_EVENTS, row, col, row, col, + oldAddress, newAddress, r, c, false, cursorVisible, ring, false, null)); } public void notifyAlarm() { - notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM)); + int ring = ringCounter.incrementAndGet(); + notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM, HOST_EVENTS, 0, 0, 0, 0, + 0, 0, 0, 0, false, cursorVisible, ring, false, null)); } public void notifyScreenResized(int rows, int cols) { int cur = (screen != null) ? screen.getCursorAddress() : 0; - notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, 0, 0, rows - 1, cols - 1, cur, cur, rows, cols, true)); + int ring = ringCounter.incrementAndGet(); + notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, HOST_EVENTS, 0, 0, rows - 1, cols - 1, + cur, cur, rows, cols, true, cursorVisible, ring, false, null)); } /** @@ -713,16 +1019,58 @@ public class ECLPS implements ECLConstants { } } + public synchronized int CheckBeforeSendKeys(String string) { + if (inputProcessor != null && inputProcessor.isKeyboardLocked()) { + return 1; + } + return 0; + } + public synchronized int CheckBeforeSendKeys(String string, int pos) { + return CheckBeforeSendKeys(string); + } + public synchronized int CheckBeforeSendKeys(String string, int row, int col) { + return CheckBeforeSendKeys(string); + } + + public synchronized void BadgeReader(String string) throws ECLErr { + BadgeReader(string, getCursorPos() + 1); + } + + public synchronized void BadgeReader(String string, int pos) throws ECLErr { + if (pos > getSize() || pos < 1) { + throw new ECLErr("ECLPS", "ECL0010", "\"pos\"", String.valueOf(pos)); + } + setCursorPos(pos - 1); + SendKeys(string + "[enter]"); + } + + public synchronized void BadgeReader(String string, int row, int col) throws ECLErr { + BadgeReader(string, (row - 1) * getCols() + col); + } + + public synchronized void asisBadgeReader(String string, String featureKey) throws ECLErr { + BadgeReader(string); + } + // ========== Synchronization & ECL Automation Waits ========== /** * Block until the specified screen descriptor conditions are met. */ + public boolean waitForScreen(ECLScreenDesc desc) { + return waitForScreen(desc, -1L); + } + public boolean WaitForScreen(ECLScreenDesc desc) { + return waitForScreen(desc); + } + public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { if (desc == null) return true; + long limit = (timeoutMs <= 0) ? 120000L : timeoutMs; long start = System.currentTimeMillis(); - while (System.currentTimeMillis() - start < timeoutMs) { - if (desc.Matches(this, null)) { + ECLOIA oia = (session != null) ? session.GetOIA() : null; + while (System.currentTimeMillis() - start < limit) { + if (desc.Matches(this, oia)) { return true; } try { @@ -732,13 +1080,43 @@ public class ECLPS implements ECLConstants { return false; } } - return desc.Matches(this, null); + return desc.Matches(this, oia); } public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) { return waitForScreen(desc, timeoutMs); } + public boolean waitWhileScreen(ECLScreenDesc desc) { + return waitWhileScreen(desc, -1L); + } + public boolean WaitWhileScreen(ECLScreenDesc desc) { + return waitWhileScreen(desc); + } + + public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { + if (desc == null) return true; + long limit = (timeoutMs <= 0) ? 120000L : timeoutMs; + long start = System.currentTimeMillis(); + ECLOIA oia = (session != null) ? session.GetOIA() : null; + while (System.currentTimeMillis() - start < limit) { + if (!desc.Matches(this, oia)) { + return true; + } + try { + Thread.sleep(25); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return !desc.Matches(this, oia); + } + + public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) { + return waitWhileScreen(desc, timeoutMs); + } + /** * Block until the specified text appears anywhere on the presentation space. */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSBIDIServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSBIDIServices.java new file mode 100644 index 0000000..55a5f0d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSBIDIServices.java @@ -0,0 +1,51 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Presentation Space BIDI services interface conforming to IBM Host On-Demand ECL. + */ +public interface ECLPSBIDIServices { + String NOMINAL = "NOMINAL"; + String NATIONAL = "NATIONAL"; + String CONTEXTUAL = "CONTEXTUAL"; + String VISUAL = "VISUAL"; + String LOGICAL = "LOGICAL"; + String LEFT_TO_RIGHT = "LEFTTORIGHT"; + String RIGHT_TO_LEFT = "RIGHTTOLEFT"; + String LAMALEF_ON = "LAMALEFON"; + String LAMALEF_OFF = "LAMALEFOFF"; + String RTLUNICODE_ON = "RTLUNICODEON"; + String RTLUNICODE_OFF = "RTLUNICODEOFF"; + String ROUNDTRIP_ON = "ON"; + String ROUNDTRIP_OFF = "OFF"; + + void SetNumeralShape(String shape) throws ECLErr; + String GetNumeralShape(); + + void SetTextType(String type) throws ECLErr; + String GetTextType(); + + void SetTextOrientation(String orientation) throws ECLErr; + String GetTextOrientation(); + + void setMacroBidiEnabled(boolean enabled); + boolean isMacroBidiEnabled(); + + void SetRoundTrip(String rt) throws ECLErr; + String GetRoundTrip(); + + void SetBIDICursorPos(int pos, boolean visual) throws ECLErr; + void SetBIDICursorPos(int pos) throws ECLErr; + void SetBIDICursorPos(int row, int col) throws ECLErr; + + void SetLamAlef(String mode) throws ECLErr; + String GetLamAlef(); + + void SetRTLUnicode(String mode) throws ECLErr; + String GetRTLUnicode(); + + void setNumericSwap(boolean swap); + boolean getNumericSwap(); + + void setSymmetricSwap(boolean swap); + boolean getSymmetricSwap(); +} 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 9905da6..a6b5eaf 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSEvent.java @@ -4,11 +4,18 @@ import java.util.EventObject; /** * Event object dispatched on Presentation Space (ECLPS) modifications. + * Conforms 1:1 to IBM Host On-Demand ECLPSEvent specification. */ public class ECLPSEvent extends EventObject { private static final long serialVersionUID = 1L; + // Standard IBM Host On-Demand event category masks + public static final int USER_EVENTS = 1; + public static final int HOST_EVENTS = 2; + public static final int ALL_EVENTS = 3; + + // Granular presentation space event types public static final int PS_UPDATE = 1; public static final int PS_CURSOR = 2; public static final int PS_ALARM = 3; @@ -22,6 +29,7 @@ public class ECLPSEvent extends EventObject { public static final int EVENT_CLOSE = PS_CLOSE; private final int eventType; + private final int type; private final int startRow; private final int startCol; private final int endRow; @@ -31,11 +39,18 @@ public class ECLPSEvent extends EventObject { private final int rows; private final int cols; private final boolean fullUpdate; + private final boolean cursorVisible; + private final int ringCounter; + private final boolean startPrinterBit; + private ECLPSUpdate psUpdate; - public ECLPSEvent(Object source, int eventType, int startRow, int startCol, int endRow, int endCol, - int oldCursorAddress, int newCursorAddress, int rows, int cols, boolean fullUpdate) { - super(source); + public ECLPSEvent(Object source, int eventType, int type, int startRow, int startCol, + int endRow, int endCol, int oldCursorAddress, int newCursorAddress, + int rows, int cols, boolean fullUpdate, boolean cursorVisible, + int ringCounter, boolean startPrinterBit, ECLPSUpdate psUpdate) { + super(source != null ? source : "ECLPS"); this.eventType = eventType; + this.type = (type != 0) ? type : HOST_EVENTS; this.startRow = startRow; this.startCol = startCol; this.endRow = endRow; @@ -45,30 +60,152 @@ public class ECLPSEvent extends EventObject { this.rows = rows; this.cols = cols; this.fullUpdate = fullUpdate; + this.cursorVisible = cursorVisible; + this.ringCounter = ringCounter; + this.startPrinterBit = startPrinterBit; + this.psUpdate = psUpdate; + } + + public ECLPSEvent(Object source, int eventType, int startRow, int startCol, int endRow, int endCol, + int oldCursorAddress, int newCursorAddress, int rows, int cols, boolean fullUpdate) { + this(source, eventType, HOST_EVENTS, startRow, startCol, endRow, endCol, + oldCursorAddress, newCursorAddress, rows, cols, fullUpdate, true, 0, false, null); } public ECLPSEvent(Object source, int eventType) { this(source, eventType, 0, 0, 0, 0, 0, 0, 0, 0, true); } + /** + * Returns the HoD event category (USER_EVENTS, HOST_EVENTS, or ALL_EVENTS). + */ + public int GetType() { return type; } + public int getType() { return type; } + + /** + * Returns the granular event type (PS_UPDATE, PS_CURSOR, PS_ALARM, PS_RESIZE, PS_CLOSE). + */ public int getEventType() { return eventType; } + + /** + * Returns 0-based start row index. + */ public int getStartRow() { return startRow; } + + /** + * Returns 1-based start row index conforming to HoD specification. + */ + public int GetStartRow() { return startRow + 1; } + + /** + * Returns 0-based start column index. + */ public int getStartCol() { return startCol; } + + /** + * Returns 1-based start column index conforming to HoD specification. + */ + public int GetStartCol() { return startCol + 1; } + + /** + * Returns 0-based end row index. + */ public int getEndRow() { return endRow; } + + /** + * Returns 1-based end row index conforming to HoD specification. + */ + public int GetEndRow() { return endRow + 1; } + + /** + * Returns 0-based end column index. + */ public int getEndCol() { return endCol; } + + /** + * Returns 1-based end column index conforming to HoD specification. + */ + public int GetEndCol() { return endCol + 1; } + + /** + * Returns 0-based linear start position within presentation space. + */ + public int getStart() { + return (cols > 0) ? (startRow * cols + startCol) : 0; + } + + /** + * Returns 1-based linear start position conforming to HoD specification. + */ + public int GetStart() { + return getStart() + 1; + } + + /** + * Returns 0-based linear end position within presentation space. + */ + public int getEnd() { + return (cols > 0) ? (endRow * cols + endCol) : 0; + } + + /** + * Returns 1-based linear end position conforming to HoD specification. + */ + public int GetEnd() { + return getEnd() + 1; + } + public int getOldCursorAddress() { return oldCursorAddress; } public int getNewCursorAddress() { return newCursorAddress; } + public int getRows() { return rows; } + public int GetRows() { return rows; } + public int getCols() { return cols; } + public int GetCols() { return cols; } + public boolean isFullUpdate() { return fullUpdate; } + public boolean IsFullUpdate() { return fullUpdate; } + + public boolean getCursorVisible() { return cursorVisible; } + public boolean GetCursorVisible() { return cursorVisible; } + public boolean isCursorVisible() { return cursorVisible; } + + public int getRingCounter() { return ringCounter; } + public int GetRingCounter() { return ringCounter; } + + public boolean isStartPrinterBit() { return startPrinterBit; } + public boolean IsStartPrinterBit() { return startPrinterBit; } + + public synchronized ECLPSUpdate getECLPSUpdate() { + if (psUpdate == null) { + ECLPS p = getPS(); + String snippet = ""; + if (p != null && cols > 0 && rows > 0) { + try { + snippet = p.getString(getStart(), Math.max(1, getEnd() - getStart() + 1)); + } catch (Exception ignored) {} + } + psUpdate = new ECLPSUpdate(p, startRow, startCol, endRow, endCol, getStart(), getEnd(), fullUpdate, snippet); + } + return psUpdate; + } + + public ECLPSUpdate GetECLPSUpdate() { + return getECLPSUpdate(); + } public ECLPS getPS() { return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null; } + public ECLPS GetPS() { + return getPS(); + } + @Override public String toString() { - return String.format("ECLPSEvent[type=%d, start=(%d,%d), end=(%d,%d), full=%b]", - eventType, startRow, startCol, endRow, endCol, fullUpdate); + return String.format("ECLPSEvent[type=%d, eventType=%d, start=(%d,%d), end=(%d,%d), full=%b, ring=%d]", + type, eventType, startRow, startCol, endRow, endCol, fullUpdate, ringCounter); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsEvent.java new file mode 100644 index 0000000..f9a815b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsEvent.java @@ -0,0 +1,62 @@ +package haus.nightmare.lib3270j.ecl; + +import java.awt.Image; +import java.awt.Rectangle; + +/** + * Conforms to IBM Host On-Demand ECLPSGraphicsEvent. + */ +public class ECLPSGraphicsEvent { + public static final int GRAPHICS_CURSOR_ON = 1; + public static final int GRAPHICS_CURSOR_OFF = 2; + public static final int GRAPHICS_ACTIVATED = 3; + public static final int GRAPHICS_DEACTIVATED = 4; + public static final int GRAPHICS_UPDATED = 5; + + private int id; + private Image image; + private Rectangle rect; + private ECLPS source; + + public ECLPSGraphicsEvent(ECLPS source, int id) { + this.source = source; + this.id = id; + } + + public ECLPSGraphicsEvent(ECLPS source, int id, Image image) { + this.source = source; + this.id = id; + this.image = image; + } + + public ECLPSGraphicsEvent(ECLPS source, int id, Image image, Rectangle rectangle) { + this.source = source; + this.id = id; + this.image = image; + this.rect = rectangle; + } + + public void setSource(ECLPS source) { this.source = source; } + public ECLPS getSource() { return this.source; } + public ECLPS GetSource() { return this.source; } + public ECLPS getPS() { return this.source; } + public ECLPS GetPS() { return this.source; } + + public void setID(int id) { this.id = id; } + public int getID() { return this.id; } + public int GetID() { return this.id; } + + public void setImage(Image image) { this.image = image; } + public Image getImage() { return this.image; } + public Image GetImage() { return this.image; } + + public void setRectangle(Rectangle rect) { this.rect = rect; } + public Rectangle getRectangle() { return this.rect; } + public Rectangle GetRectangle() { return this.rect; } + + @Override + public String toString() { + return String.format("ECLPSGraphicsEvent[id=%d, rect=%s, hasImage=%b]", + id, rect, image != null); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsListener.java new file mode 100644 index 0000000..6b2bd8f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsListener.java @@ -0,0 +1,9 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Conforms to IBM Host On-Demand ECLPSGraphicsListener. + */ +public interface ECLPSGraphicsListener { + default void graphicsEvent(ECLPSGraphicsEvent event) {} + default void graphicsUpdated(ECLPSGraphicsEvent event) {} +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsServices.java new file mode 100644 index 0000000..902bc96 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSGraphicsServices.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.ecl; + +import java.awt.Color; +import java.awt.Component; + +/** + * Presentation Space graphics services interface conforming to IBM Host On-Demand ECL. + */ +public interface ECLPSGraphicsServices { + void setVisualComponent(Component comp); + void setGraphicColor(Color[] colors, boolean b); + void mousePressed(int x, int y, int button); + void addGraphicsListener(ECLPSGraphicsListener listener); + void removeGraphicsListener(ECLPSGraphicsListener listener); +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSHindiServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSHindiServices.java new file mode 100644 index 0000000..c68358d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSHindiServices.java @@ -0,0 +1,11 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Presentation Space Hindi services interface conforming to IBM Host On-Demand ECL. + */ +public interface ECLPSHindiServices { + int GetHindiCursorCol(int row, int col); + byte GetHindiCursorLevel(int row, int col); + int GetNormalCursorCol(int row, int col); + void switchToHindiLayer(); +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSListener.java index 0832127..599fdc5 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSListener.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSListener.java @@ -2,14 +2,38 @@ package haus.nightmare.lib3270j.ecl; /** * Listener interface for Presentation Space (ECLPS) update events. + * Conforms 1:1 to IBM Host On-Demand ECLPSListener specification. */ public interface ECLPSListener { /** - * Called when the presentation space is modified. + * Primary HoD notification callback invoked when presentation space changes occur. * @param event ECLPSEvent containing update boundaries and state */ - void psChanged(ECLPSEvent event); + default void PSNotifyEvent(ECLPSEvent event) { + psChanged(event); + } + + /** + * Called when an error condition occurs during event generation. + * @param ps Presentation Space instance + * @param err ECLErr error descriptor + */ + default void PSNotifyError(ECLPS ps, ECLErr err) {} + + /** + * Called when event notification has stopped. + * @param ps Presentation Space instance + * @param reason Stop reason code (STOP_UNREGISTER, STOP_DISCONNECT, STOP_ERROR) + */ + default void PSNotifyStop(ECLPS ps, int reason) {} + + // ========== Backward-Compatibility Bridge Methods ========== + + /** + * Legacy callback called when the presentation space is modified. + */ + default void psChanged(ECLPSEvent event) {} /** * Called when the cursor position changes within the presentation space. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSTHAIServices.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSTHAIServices.java new file mode 100644 index 0000000..637145c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSTHAIServices.java @@ -0,0 +1,12 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Presentation Space Thai services interface conforming to IBM Host On-Demand ECL. + */ +public interface ECLPSTHAIServices { + void SetThaiDisplayMode(int mode) throws ECLErr; + int GetThaiDisplayMode(); + int GetThaiCursorCol(int row, int col); + byte GetThaiCursorLevel(int row, int col); + int GetNormalCursorCol(int row, int col); +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSUpdate.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSUpdate.java new file mode 100644 index 0000000..c0a4802 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPSUpdate.java @@ -0,0 +1,86 @@ +package haus.nightmare.lib3270j.ecl; + +import java.io.Serializable; + +/** + * Conforms to IBM Host On-Demand ECLPSUpdate. + * Encapsulates presentation space update coordinates, linear offsets, and affected data. + */ +public class ECLPSUpdate implements Serializable { + + private static final long serialVersionUID = 1L; + + 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 : ""; + } + + public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean fullUpdate) { + this(null, startRow, startCol, endRow, endCol, -1, -1, fullUpdate, ""); + } + + 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() { + if (start >= 0) return start; + if (ps != null && ps.getCols() > 0) return (startRow * ps.getCols() + startCol); + return 0; + } + + public int GetStart() { + return getStart() + 1; + } + + public int getEnd() { + if (end >= 0) return end; + if (ps != null && ps.getCols() > 0) return (endRow * ps.getCols() + endCol); + return 0; + } + + public int GetEnd() { + return getEnd() + 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/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java index fe69072..fade815 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java @@ -1,6 +1,7 @@ package haus.nightmare.lib3270j.ecl; import haus.nightmare.lib3270j.Telnet3270Client; +import haus.nightmare.lib3270j.screen.ScreenBuffer; import java.util.ArrayList; import java.util.List; @@ -229,6 +230,49 @@ public class ECLScreenDesc { 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 synchronized int getConditionCount() { return conditions.size(); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenNotify.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenNotify.java new file mode 100644 index 0000000..8bcfe4e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenNotify.java @@ -0,0 +1,8 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Listener interface for ECLScreenReco matching notifications. + */ +public interface ECLScreenNotify { + void screenMatching(ECLScreenRecoEvent event); +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenReco.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenReco.java new file mode 100644 index 0000000..6e3a22d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenReco.java @@ -0,0 +1,148 @@ +package haus.nightmare.lib3270j.ecl; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Screen Recognition Engine matching IBM Host On-Demand ECLScreenReco. + * Provides static screen comparison routines and dynamic asynchronous screen detection callbacks. + */ +public class ECLScreenReco { + + private final ECLSession session; + private final ECLPS ps; + private final Map notifyMap = new ConcurrentHashMap<>(); + private final Map listenerMap = new ConcurrentHashMap<>(); + private final ECLPSListener internalListener; + + public ECLScreenReco() { + this((ECLPS) null); + } + + public ECLScreenReco(ECLSession session) { + this(session != null ? session.GetPS() : null); + } + + public ECLScreenReco(ECLPS ps) { + this.ps = ps; + this.session = (ps != null) ? ps.GetParent() : null; + + this.internalListener = new ECLPSListener() { + @Override + public void psChanged(ECLPSEvent event) { + checkAllScreens(); + } + }; + + if (this.ps != null) { + this.ps.RegisterPSEvent(this.internalListener); + } + } + + // ========== Static Recognition Helpers ========== + + public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) { + if (desc == null) return true; + if (ps == null) return false; + ECLOIA oia = (ps.GetParent() != null) ? ps.GetParent().GetOIA() : null; + return desc.Matches(ps, oia); + } + + public static boolean isMatch(ECLPS ps, ECLScreenDesc desc) { + return IsMatch(ps, desc); + } + + public static boolean compareTextAt(ECLPS ps, String text, int row, int col, boolean caseSense) { + if (ps == null || text == 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()); + if (onScreen == null) return false; + return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen); + } + + public static boolean compareTextInRect(ECLPS ps, String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) { + if (ps == null || text == 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()); + } + + // ========== Dynamic Registration & Callbacks ========== + + public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) { + if (desc != null && notify != null) { + notifyMap.put(desc, notify); + if (IsMatch(desc)) { + notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)); + } + } + } + public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) { + RegisterScreen(desc, notify); + } + + public void UnregisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) { + if (desc != null) { + notifyMap.remove(desc); + } + } + public void unregisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) { + UnregisterScreen(desc, notify); + } + + public void RegisterScreen(ECLScreenDesc desc, ECLPSListener listener) { + if (desc != null && listener != null) { + listenerMap.put(desc, listener); + if (ps != null) { + ps.RegisterPSEvent(listener, desc); + } + } + } + public void registerScreen(ECLScreenDesc desc, ECLPSListener listener) { + RegisterScreen(desc, listener); + } + + public void UnregisterScreen(ECLScreenDesc desc, ECLPSListener listener) { + if (desc != null) { + listenerMap.remove(desc); + if (ps != null && listener != null) { + ps.UnregisterPSEvent(listener); + } + } + } + public void unregisterScreen(ECLScreenDesc desc, ECLPSListener listener) { + UnregisterScreen(desc, listener); + } + + public boolean IsMatch(ECLScreenDesc desc) { + return IsMatch(ps, desc); + } + public boolean isMatch(ECLScreenDesc desc) { + return IsMatch(desc); + } + + public synchronized void checkAllScreens() { + if (ps == null) return; + for (Map.Entry entry : notifyMap.entrySet()) { + ECLScreenDesc desc = entry.getKey(); + if (IsMatch(desc)) { + try { + entry.getValue().screenMatching(new ECLScreenRecoEvent(this, desc, ps)); + } catch (Exception ignored) {} + } + } + } + + public void dispose() { + if (ps != null && internalListener != null) { + ps.UnregisterPSEvent(internalListener); + } + notifyMap.clear(); + listenerMap.clear(); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenRecoEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenRecoEvent.java new file mode 100644 index 0000000..ad181d2 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenRecoEvent.java @@ -0,0 +1,26 @@ +package haus.nightmare.lib3270j.ecl; + +/** + * Event delivered when an ECLScreenDesc matches the Presentation Space. + */ +public class ECLScreenRecoEvent { + + private final ECLScreenReco source; + private final ECLScreenDesc screenDesc; + private final ECLPS ps; + + public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) { + this.source = source; + this.screenDesc = screenDesc; + this.ps = ps; + } + + public ECLScreenReco getSource() { return source; } + public ECLScreenReco GetSource() { 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/haus/nightmare/lib3270j/ecl/ECLSession.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java index 23fb156..0170259 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java @@ -34,6 +34,15 @@ public class ECLSession { private final ECLConnection connection; private Properties properties = new Properties(); + private String sessionName = "A"; + private String sessionLabel = "j3270 Session"; + private String associatedDeviceName = ""; + private static short sessCounter = 1; + private int macroId = 0; + private int keyStrength = 0; + private ClassLoader customizedCAsClassLoader; + private haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl sslSessionImpl; + public ECLSession() { this(new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4)); } @@ -89,6 +98,26 @@ public class ECLSession { String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true"); config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr)); + String certUrl = getProp(props, "certificateURL", "CERTIFICATE_URL", "certificate_url", null); + if (certUrl != null) config.setKeyStorePath(certUrl); + String certPwd = getProp(props, "certificatePassword", "CERTIFICATE_PASSWORD", "certificate_password", null); + if (certPwd != null) config.setKeyStorePassword(certPwd); + String certAlias = getProp(props, "certificateName", "CERTIFICATE_NAME", "certificate_name", null); + if (certAlias != null) config.setKeyStoreAlias(certAlias); + + String tsPath = getProp(props, "jsseTrustStore", "TRUSTSTORE", "truststore", null); + if (tsPath != null) config.setTrustStorePath(tsPath); + String tsPwd = getProp(props, "jsseTrustStorePassword", "TRUSTSTORE_PASSWORD", "truststore_password", null); + if (tsPwd != null) config.setTrustStorePassword(tsPwd); + String tsType = getProp(props, "jsseTrustStoreType", "TRUSTSTORE_TYPE", "truststore_type", null); + if (tsType != null) config.setTrustStoreType(tsType); + + String tlsVer = getProp(props, "tlsProtocolVersion", "TLS_VERSION", "tls_version", null); + if (tlsVer != null) { + config.setSslProtocol(tlsVer); + config.setEnabledProtocols(tlsVer); + } + return config; } @@ -110,7 +139,9 @@ public class ECLSession { private static TerminalModel parseModelString(String m) { if (m == null || m.trim().isEmpty()) return TerminalModel.IBM_3279_4; String s = m.trim().toUpperCase(); - if (s.equals("2") || s.contains("3278-2") || s.contains("3279-2") || s.equals("24X80")) { + if (s.equals("DYNAMIC") || s.contains("DYNAMIC") || s.equals("0")) { + return TerminalModel.IBM_DYNAMIC; + } else if (s.equals("2") || s.contains("3278-2") || s.contains("3279-2") || s.equals("24X80")) { return TerminalModel.IBM_3279_2; } else if (s.equals("3") || s.contains("3278-3") || s.contains("3279-3") || s.equals("32X80")) { return TerminalModel.IBM_3279_3; @@ -161,6 +192,21 @@ public class ECLSession { public ECLFieldList GetFieldList() { return client.getFieldList(); } public ECLFieldList getFieldList() { return client.getFieldList(); } + public haus.nightmare.lib3270j.tn3270.DS3270 GetDS() { + return new haus.nightmare.lib3270j.tn3270.DS3270(client.getDataStreamProcessor()); + } + public haus.nightmare.lib3270j.tn3270.DS3270 getDS() { return GetDS(); } + + public haus.nightmare.lib3270j.tn3270.Telnet3270E GetTelnet() { + return new haus.nightmare.lib3270j.tn3270.Telnet3270E(client.getTelnetFSM(), client.getConnection()); + } + public haus.nightmare.lib3270j.tn3270.Telnet3270E getTelnet() { return GetTelnet(); } + + public haus.nightmare.lib3270j.tn3270.PS3270 GetPS3270() { + return new haus.nightmare.lib3270j.tn3270.PS3270(client.getScreenBuffer(), client.getInputProcessor(), client.getTranslator()); + } + public haus.nightmare.lib3270j.tn3270.PS3270 getPS3270() { return GetPS3270(); } + public Telnet3270Client GetClient() { return client; } public Telnet3270Client getClient() { return client; } @@ -182,22 +228,36 @@ public class ECLSession { * Synchronous blocking connection conforming to HoD ECLSession.StartCommunicationWithBlocking. * Blocks until the connection reaches fully established data state or timeout expires. */ - public boolean StartCommunicationWithBlocking(long timeoutMs) throws IOException { - return client.connect(timeoutMs); + public boolean StartCommunicationWithBlocking(long timeout) throws IOException, ECLErr, InterruptedException { + return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null); } - public boolean startCommunicationWithBlocking(long timeoutMs) throws IOException { - return StartCommunicationWithBlocking(timeoutMs); + public boolean startCommunicationWithBlocking(long timeout) throws IOException, ECLErr, InterruptedException { + return StartCommunicationWithBlocking(timeout); } /** * Synchronous blocking connection matching HoD ECLSession.StartCommunicationWithBlocking(timeout, desc). * Blocks until the connection is established AND the presentation space matches the screen descriptor. */ - public boolean StartCommunicationWithBlocking(long timeoutMs, ECLScreenDesc desc) throws IOException { - return client.connect(timeoutMs, desc); + public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws IOException, ECLErr, InterruptedException { + long timeoutMs = (timeout > 0) ? (timeout < 10000 ? timeout * 1000L : timeout) : 120000L; + long start = System.currentTimeMillis(); + boolean ok = client.connect(timeoutMs); + if (!ok) { + throw new ECLErr("ECLSession.StartCommunicationWithBlocking(long timeout)", "ECL0001", "Timeout reached"); + } + if (desc != null) { + long remainingMs = timeoutMs - (System.currentTimeMillis() - start); + if (remainingMs <= 0) remainingMs = 1000; + boolean matched = WaitForScreen(desc, remainingMs); + if (!matched) { + throw new ECLErr("ECLSession.StartCommunicationWithBlocking(long timeout)", "ECL0001", "Timeout reached"); + } + } + return true; } - public boolean startCommunicationWithBlocking(long timeoutMs, ECLScreenDesc desc) throws IOException { - return StartCommunicationWithBlocking(timeoutMs, desc); + public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws IOException, ECLErr, InterruptedException { + return StartCommunicationWithBlocking(timeout, desc); } /** @@ -228,11 +288,11 @@ public class ECLSession { /** * Reconnect the communication session synchronously. */ - public boolean RestartCommunicationWithBlocking(long timeoutMs) throws IOException { + public boolean RestartCommunicationWithBlocking(long timeoutMs) throws IOException, ECLErr, InterruptedException { StopCommunicationWithBlocking(500); return StartCommunicationWithBlocking(timeoutMs); } - public boolean restartCommunicationWithBlocking(long timeoutMs) throws IOException { + public boolean restartCommunicationWithBlocking(long timeoutMs) throws IOException, ECLErr, InterruptedException { return RestartCommunicationWithBlocking(timeoutMs); } @@ -246,6 +306,71 @@ public class ECLSession { } public boolean isCommStarted() { return IsCommStarted(); } + // ========== Session Metadata & Profile Properties ========== + + public String getSessionName() { return sessionName; } + public String GetSessionName() { return sessionName; } + public void setSessionName(String name) { this.sessionName = name != null ? name : "A"; } + public void SetSessionName(String name) { setSessionName(name); } + + public String getSessionLabel() { return sessionLabel; } + public String GetSessionLabel() { return sessionLabel; } + public void setSessionLabel(String label) { this.sessionLabel = label != null ? label : ""; } + public void SetSessionLabel(String label) { setSessionLabel(label); } + + public String getAssociatedDeviceName() { return associatedDeviceName; } + public String GetAssociatedDeviceName() { return associatedDeviceName; } + public void setAssociatedDeviceName(String name) { this.associatedDeviceName = name != null ? name : ""; } + public void SetAssociatedDeviceName(String name) { setAssociatedDeviceName(name); } + + public short getSessionCount() { return sessCounter; } + public short GetSessionCount() { return sessCounter; } + public void setSessionCounter(int n) { sessCounter = (short) n; } + public void SetSessionCounter(int n) { setSessionCounter(n); } + + public int getMacroID() { return macroId; } + public int GetMacroID() { return macroId; } + public void setMacroID(int id) { this.macroId = id; } + public void SetMacroID(int id) { setMacroID(id); } + + public int getKeyStrength() { return keyStrength; } + public int GetKeyStrength() { return keyStrength; } + public void setKeyStrength(int strength) { this.keyStrength = strength; } + public void SetKeyStrength(int strength) { setKeyStrength(strength); } + + public ClassLoader GetCustomizedCAsClassLoader() { return customizedCAsClassLoader; } + public ClassLoader getCustomizedCAsClassLoader() { return customizedCAsClassLoader; } + public void SetCustomizedCAsClassLoader(ClassLoader cl) { + this.customizedCAsClassLoader = cl; + if (client != null && client.getConfig() != null) { + client.getConfig().setCustomizedCAsClassLoader(cl); + } + if (sslSessionImpl != null) { + sslSessionImpl.setCustomizedCAsClassLoader(cl); + } + } + public void setCustomizedCAsClassLoader(ClassLoader cl) { SetCustomizedCAsClassLoader(cl); } + + public synchronized haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl getSSLSessionImpl() { + if (sslSessionImpl == null) { + sslSessionImpl = new haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl(this); + } + return sslSessionImpl; + } + public haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl GetSSLSessionImpl() { + return getSSLSessionImpl(); + } + + public void sendNewEnvironmentVariables(Properties props) { + if (props != null) { + this.properties.putAll(props); + if (client != null && client.getTelnetFSM() != null) { + // Propagate to telnet state machine if active + client.getTelnetFSM().sendNewEnvironmentVariables(props); + } + } + } + // ========== Automation Keystrokes & Waits ========== /** @@ -264,6 +389,15 @@ public class ECLSession { } public void sendKeys(String text, int row, int col) { SendKeys(text, row, col); } + /** + * Block until the specified screen descriptor conditions are met on screen. + * Default timeout is infinite / 120 seconds. + */ + public boolean WaitForScreen(ECLScreenDesc desc) { + return WaitForScreen(desc, -1L); + } + public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); } + /** * Block until the specified screen descriptor conditions are met on screen. */ @@ -272,6 +406,36 @@ public class ECLSession { } public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); } + /** + * Block while the specified screen descriptor matches on screen. + */ + public boolean WaitWhileScreen(ECLScreenDesc desc) { + return WaitWhileScreen(desc, -1L); + } + public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); } + + public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) { + return client.getPS().waitWhileScreen(desc, timeoutMs); + } + public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); } + + /** + * Synchronous blocking connection matching HoD ECLSession.StartCommunicationWithBlocking(timeout, stringArray). + */ + public boolean StartCommunicationWithBlocking(long timeout, String[] stringArray) throws IOException, ECLErr, InterruptedException { + ECLScreenDesc desc = null; + if (stringArray != null && stringArray.length > 0) { + desc = new ECLScreenDesc(); + for (String s : stringArray) { + desc.AddStringInRect(s, 1, 1, -1, -1, false); + } + } + return StartCommunicationWithBlocking(timeout, desc); + } + public boolean startCommunicationWithBlocking(long timeout, String[] stringArray) throws IOException, ECLErr, InterruptedException { + return StartCommunicationWithBlocking(timeout, stringArray); + } + /** * Block until the cursor moves to (row, col). */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java index 003d5e2..e406d8d 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXfer.java @@ -121,6 +121,53 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener { return startTransferInternal(config); } + public void RegisterXferEvent(ECLXferListener listener) { + addXferListener(listener); + } + + public void UnregisterXferEvent(ECLXferListener listener) { + removeXferListener(listener); + } + + private String pcCodePage = "1252"; + + public void SetPCCodePage(String cp) { + this.pcCodePage = cp; + } + + public String GetPCCodePage() { + return pcCodePage; + } + + public int SendFile(haus.nightmare.lib3270j.xfer.FileTransferFileObject fileObj, String options) { + if (fileObj == null) return FTConstants.ECL_ERR_XFER_INVALID_PARAM; + FTConfig config = fileObj.toFTConfig(); + config.setDirection(FTConfig.Direction.SEND); + if (options != null && !options.isEmpty()) { + config.parseOptions(options); + } + return startTransferInternal(config); + } + + public int ReceiveFile(haus.nightmare.lib3270j.xfer.FileTransferFileObject fileObj, String options) { + if (fileObj == null) return FTConstants.ECL_ERR_XFER_INVALID_PARAM; + FTConfig config = fileObj.toFTConfig(); + config.setDirection(FTConfig.Direction.RECEIVE); + if (options != null && !options.isEmpty()) { + config.parseOptions(options); + } + return startTransferInternal(config); + } + + private haus.nightmare.lib3270j.xfer3270.Xfer3270 xfer3270Instance; + + public haus.nightmare.lib3270j.xfer3270.Xfer3270 getXfer3270() { + if (xfer3270Instance == null) { + xfer3270Instance = new haus.nightmare.lib3270j.xfer3270.Xfer3270(this); + } + return xfer3270Instance; + } + /** * Convenience method to download a file with listener and codepage parameters. */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferEvent.java index 120529f..e7f8f77 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferEvent.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferEvent.java @@ -67,6 +67,19 @@ public class ECLXferEvent extends EventObject { return hostFilename; } + public ECLXferEvent(Object source, long bytesTransferred, String hostFile, String localFile, boolean completed) { + this(source, completed ? XFER_COMPLETED : XFER_PROGRESS, bytesTransferred, 0, 0, + completed ? "Transfer complete" : "Transfer in progress", localFile, hostFile); + } + + public boolean isCompleted() { + return eventType == XFER_COMPLETED; + } + + public boolean isAborted() { + return eventType == XFER_ABORTED || eventType == XFER_CANCELLED; + } + public boolean isSuccessful() { return eventType == XFER_COMPLETED && returnCode == 0; } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferListener.java index 61fefbc..bf55330 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferListener.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLXferListener.java @@ -10,4 +10,12 @@ public interface ECLXferListener { * @param event ECLXferEvent containing status and progress metrics */ void xferEvent(ECLXferEvent event); + + /** + * IBM HoD legacy callback signature alias. + */ + default void XferNotifyEvent(ECLXferEvent event) { + xferEvent(event); + } } + diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java index 2e852d0..b337b52 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java @@ -61,6 +61,8 @@ public class FTConfig { private int dftBufferSize = FTConstants.DFT_BUF; private String otherOptions = null; private String codePage = null; + private boolean unicode = false; + private int unicodeType = 0; // 0 = UCS2, 1 = UTF8 // ========== Derived convenience getters ========== @@ -68,6 +70,10 @@ public class FTConfig { public boolean isSend() { return direction == Direction.SEND; } public boolean isAscii() { return transferMode == TransferMode.ASCII; } public boolean isBinary() { return transferMode == TransferMode.BINARY; } + public boolean isUnicode() { return unicode; } + public void setUnicode(boolean unicode) { this.unicode = unicode; } + public int getUnicodeType() { return unicodeType; } + public void setUnicodeType(int unicodeType) { this.unicodeType = unicodeType; } public boolean isCrFlag() { // CR processing is only applicable for ASCII transfers return isAscii() && crAction != CrAction.KEEP; @@ -269,6 +275,22 @@ public class FTConfig { this.transferMode = TransferMode.ASCII; } + if (upper.contains("UNICODE")) { + this.unicode = true; + int uIdx = upper.indexOf("UNICODE"); + int pStart = upper.indexOf('(', uIdx); + int pEnd = upper.indexOf(')', uIdx); + String uSub = ""; + if (pStart > uIdx && pEnd > pStart) { + uSub = upper.substring(pStart + 1, pEnd); + } + if (uSub.contains("UTF8") || uSub.contains("UTF-8")) { + this.unicodeType = 1; + } else { + this.unicodeType = 0; + } + } + if (upper.contains("NOCRLF")) { this.crAction = CrAction.KEEP; } else if (upper.contains("CRLF")) { @@ -437,6 +459,11 @@ public class FTConfig { opts.append("BINARY"); } + if (isUnicode()) { + if (opts.length() > 0) opts.append(" "); + opts.append("UNICODE(").append(unicodeType == 1 ? "UTF-8" : "UCS2").append(")"); + } + // CR/LF handling if (isAscii() && isCrFlag()) { if (opts.length() > 0) opts.append(" "); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java index 91565d7..e8a11df 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java @@ -150,7 +150,20 @@ public class FTCut { FTConfig config = listener.getConfig(); resetState(); - if (config.isReceive()) { + if (config != null && config.isUnicode()) { + haus.nightmare.lib3270j.charset.CodePage cp = translator != null ? translator.getCodePage() : null; + if (config.isReceive()) { + outputStream = new haus.nightmare.lib3270j.xfer.XferFileOutputUnicode( + localFile, config.isAppend(), new byte[]{13, 10}, config.isAscii(), + cp, config.getUnicodeType(), cp != null && cp.isDBCS(), false); + inputStream = null; + } else { + inputStream = new haus.nightmare.lib3270j.xfer.XferFileInputUnicode( + localFile, new byte[]{13, 10}, cp, + config.getUnicodeType(), 1, false); + outputStream = null; + } + } else if (config.isReceive()) { boolean append = config.isAppend(); outputStream = new FileOutputStream(localFile, append); inputStream = null; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java index 78aedd1..6508eaa 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java @@ -124,7 +124,20 @@ public class FTDft { FTConfig config = listener.getConfig(); resetState(); - if (config.isReceive()) { + if (config != null && config.isUnicode()) { + haus.nightmare.lib3270j.charset.CodePage cp = translator != null ? translator.getCodePage() : null; + if (config.isReceive()) { + outputStream = new haus.nightmare.lib3270j.xfer.XferFileOutputUnicode( + localFile, config.isAppend(), new byte[]{13, 10}, config.isAscii(), + cp, config.getUnicodeType(), cp != null && cp.isDBCS(), false); + inputStream = null; + } else { + inputStream = new haus.nightmare.lib3270j.xfer.XferFileInputUnicode( + localFile, new byte[]{13, 10}, cp, + config.getUnicodeType(), 1, false); + outputStream = null; + } + } else if (config != null && config.isReceive()) { outputStream = new FileOutputStream(localFile, config.isAppend()); inputStream = null; } else { @@ -435,6 +448,11 @@ public class FTDft { FTConfig config) throws IOException { if (outputStream == null) return; + if (config != null && config.isUnicode()) { + outputStream.write(data, offset, length); + return; + } + if (!config.isAscii()) { outputStream.write(data, offset, length); return; @@ -490,7 +508,14 @@ public class FTDft { try { while (!dftEof && totalRead < numbytes) { - if (config.isAscii()) { + if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) { + int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead); + if (n <= 0) { + dftEof = true; + break; + } + totalRead += n; + } else if (config.isAscii()) { int b = dftAsciiRead(config); if (b == -1) { dftEof = true; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/Edge.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/Edge.java new file mode 100644 index 0000000..2d5511c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/Edge.java @@ -0,0 +1,82 @@ +package haus.nightmare.lib3270j.graphics; + +/** + * Scanline edge representation matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.Edge). + */ +public class Edge { + public int ymin; + public int ymax; + public int xi; + public int si; + public int r; + public int inc; + public int dec; + public int dy; + + public Edge(int x1, int y1, int x2, int y2) { + int tmp; + if (y1 > y2) { + tmp = y2; y2 = y1; y1 = tmp; + tmp = x2; x2 = x1; x1 = tmp; + } + int dx = x2 - x1; + this.dy = y2 - y1; + this.ymin = y1; + this.ymax = y2; + if (this.dy != 0) { + this.si = this.floor_div(dx, this.dy); + this.xi = x1 + this.si; + tmp = dx - this.si * this.dy; + this.r = 2 * tmp - this.dy; + this.inc = tmp; + this.dec = tmp - this.dy; + } + } + + @Override + public String toString() { + return "xi=" + this.xi + " ymin=" + this.ymin + " ymax=" + this.ymax; + } + + public int edgeScan() { + int n = this.xi; + if (this.r >= 0) { + this.xi += this.si + 1; + this.r += this.dec; + } else { + this.xi += this.si; + this.r += this.inc; + } + return n; + } + + public int floor_div(int n, int n2) { + if (n >= 0) { + return n / n2; + } + return n / n2 + (n % n2 == 0 ? 0 : -1); + } + + public static void quicksort(Edge[] edgeArray, int n) { + quicksort(edgeArray, 0, n - 1); + } + + private static void swap(Edge[] edgeArray, int n, int n2) { + Edge edge = edgeArray[n]; + edgeArray[n] = edgeArray[n2]; + edgeArray[n2] = edge; + } + + private static void quicksort(Edge[] edgeArray, int n, int n2) { + if (n >= n2) return; + swap(edgeArray, n, (n + n2) / 2); + int n3 = n; + for (int i = n + 1; i <= n2; ++i) { + if (edgeArray[i].xi >= edgeArray[n].xi) continue; + swap(edgeArray, ++n3, i); + } + swap(edgeArray, n, n3); + quicksort(edgeArray, n, n3 - 1); + quicksort(edgeArray, n3 + 1, n2); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java index bf77d47..029470d 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java @@ -35,12 +35,113 @@ public class FillArea { private final List subpathsX = new ArrayList<>(); private final List subpathsY = new ArrayList<>(); + private int fillColor = -1; + private boolean fillModeOR = false; + private boolean solidFill = true; + private int[] pixelPattern; + private int patternWidth = 8; + private int patternHeight = 8; + public FillArea() {} public FillArea(int fillRule) { this.fillRule = fillRule; } + /** + * IBM Host On-Demand multi-polygon constructor. + */ + public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, java.awt.Color color) { + this(); + if (color != null) { + this.fillColor = color.getRGB(); + } + if (px != null && py != null && polyCounts != null) { + int offset = 0; + for (int i = 0; i < numPolys && i < polyCounts.length; i++) { + int count = polyCounts[i]; + if (count >= 2 && offset + count <= px.length && offset + count <= py.length) { + int[] sx = new int[count]; + int[] sy = new int[count]; + System.arraycopy(px, offset, sx, 0, count); + System.arraycopy(py, offset, sy, 0, count); + addPolygon(sx, sy, count); + } + offset += count; + } + } + } + + public synchronized java.awt.Rectangle getBounds() { + if (edges.isEmpty()) { + return new java.awt.Rectangle(0, 0, 0, 0); + } + double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE; + double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE; + for (Edge e : edges) { + minX = Math.min(minX, Math.min(e.x1, e.x2)); + minY = Math.min(minY, Math.min(e.y1, e.y2)); + maxX = Math.max(maxX, Math.max(e.x1, e.x2)); + maxY = Math.max(maxY, Math.max(e.y1, e.y2)); + } + int x = (int) Math.floor(minX); + int y = (int) Math.floor(minY); + int w = (int) Math.ceil(maxX) - x + 1; + int h = (int) Math.ceil(maxY) - y + 1; + return new java.awt.Rectangle(x, y, Math.max(0, w), Math.max(0, h)); + } + + public synchronized void setFillModeOR() { + this.fillModeOR = true; + } + + public synchronized boolean isFillModeOR() { + return this.fillModeOR; + } + + public synchronized void set8x8Pattern(byte[] pat) { + this.solidFill = false; + this.patternWidth = 8; + this.patternHeight = 8; + this.pixelPattern = new int[64]; + if (pat != null) { + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 8; j++) { + if (j < pat.length) { + this.pixelPattern[i + j * 8] = ((pat[j] >> (7 - i)) & 1) == 1 ? this.fillColor : 0; + } + } + } + } + } + + public synchronized void setRGBPattern(int[] pat, int w, int h) { + if (w <= 0 || h <= 0 || pat == null) return; + this.solidFill = false; + this.patternWidth = w; + this.patternHeight = h; + this.pixelPattern = pat; + } + + public synchronized java.awt.Image getImage() { + java.awt.Rectangle b = getBounds(); + if (b.width <= 0 || b.height <= 0) { + return new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_ARGB); + } + GraphicsPlane tempPlane = new GraphicsPlane(b.x + b.width, b.y + b.height); + fill(tempPlane, fillColor, 0, solidFill ? GocaConstants.PT_SOLID : 0, false, 0, 0, 1, 0, 0, null); + java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(b.width, b.height, java.awt.image.BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics g = img.getGraphics(); + g.drawImage(tempPlane.getImage(), -b.x, -b.y, null); + g.dispose(); + return img; + } + + public synchronized void dispose() { + clear(); + this.pixelPattern = null; + } + public synchronized void setFillRule(int fillRule) { this.fillRule = fillRule; } @@ -171,10 +272,12 @@ public class FillArea { int bg = bgColorArgb; // Background mix / transparency rule for Black fills: - // BMX_LEAVE / 0 or 2 / MIX_DEFAULT: Transparent black - // BMX_OVER / 1 or 5: Opaque background overpaint + // BMX_TRANSPARENT / 0 or 2 / MIX_DEFAULT: Transparent black + // BMX_OPAQUE / 1: Opaque background overpaint boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) && - (bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0); + (bgMix == GocaConstants.BMX_DEFAULT || bgMix == GocaConstants.BMX_TRANSPARENT || + bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0 || bgMix == 2); + boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1); if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { double minY = Double.MAX_VALUE; @@ -238,7 +341,7 @@ public class FillArea { boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); if (bit) { plane.setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { plane.setPixel(x, y, bg); } } else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { @@ -247,7 +350,7 @@ public class FillArea { int b = patRows[y & 7] & 0xFF; if (((b >> (7 - (x & 7))) & 1) != 0) { plane.setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { plane.setPixel(x, y, bg); } } @@ -272,7 +375,7 @@ public class FillArea { boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); if (bit) { plane.setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { plane.setPixel(x, y, bg); } } else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { @@ -281,7 +384,7 @@ public class FillArea { int b = patRows[y & 7] & 0xFF; if (((b >> (7 - (x & 7))) & 1) != 0) { plane.setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { plane.setPixel(x, y, bg); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java index 05c4935..aff5e30 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java @@ -106,4 +106,27 @@ public class FilletPts { } return new int[][] { rx, ry }; } + + /** + * Estimates the maximum number of points required for fillet interpolation. + * Matches IBM Host On-Demand FilletPts.getPointsRequired. + */ + public int getPointsRequired(int n) { + return Math.max(0, n * 20); + } + + /** + * Interpolates fillet points into provided output buffers. + * Matches IBM Host On-Demand FilletPts.getFilletPoints. + */ + public int getFilletPoints(int[] xIn, int[] yIn, int n, int[] xOut, int[] yOut) { + if (xIn == null || yIn == null || n <= 0 || xOut == null || yOut == null) { + return 0; + } + int[][] pts = calculate(xIn, yIn, n, 16); + int count = Math.min(pts[0].length, Math.min(xOut.length, yOut.length)); + System.arraycopy(pts[0], 0, xOut, 0, count); + System.arraycopy(pts[1], 0, yOut, 0, count); + return count; + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java index a9e8b26..3d0392f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java @@ -211,4 +211,64 @@ public class GddmCoordinateTransform { int ny = (int) Math.round((double) canvasY * totalH / (canvasHeight > 0 ? canvasHeight : 1)); return new Point(nx - xMax, yMax - ny); } + + // ========================================================================= + // IBM 3179G / PS3179G Coordinate Transformations + // ========================================================================= + + /** + * Converts a linear presentation space buffer address to a 1-based screen row. + * Matches IBM Host On-Demand PS3179G.convertAddressToRow. + */ + public int convertAddressToRow(int address) { + return (screenCols > 0) ? (address / screenCols) + 1 : 1; + } + + /** + * Converts a linear presentation space buffer address to a 1-based screen column. + * Matches IBM Host On-Demand PS3179G.convertAddressToColumn. + */ + public int convertAddressToColumn(int address) { + return (screenCols > 0) ? (address % screenCols) + 1 : 1; + } + + /** + * Converts a 0-based or 1-based column index to physical presentation space X coordinate. + * Matches IBM Host On-Demand PS3179G.convertColumnToX. + */ + public int convertColumnToX(int col) { + return col * defaultCharWidth; + } + + /** + * Converts a 0-based or 1-based row index to physical presentation space Y coordinate. + * Matches IBM Host On-Demand PS3179G.convertRowToY. + */ + public int convertRowToY(int row) { + return row * defaultCharHeight; + } + + /** + * Converts physical presentation space (x, y) coordinates to 1-based screen row. + * Matches IBM Host On-Demand PS3179G.convertXYToRow / convertYtoRow. + */ + public int convertXYToRow(int x, int y) { + return convertYtoRow(y); + } + + /** + * Converts physical presentation space X pixel coordinate to 1-based column. + * Matches IBM Host On-Demand PS3179G.convertXtoColumn. + */ + public int convertXtoColumn(int x) { + return (defaultCharWidth > 0) ? (x / defaultCharWidth) + 1 : 1; + } + + /** + * Converts physical presentation space Y pixel coordinate to 1-based row. + * Matches IBM Host On-Demand PS3179G.convertYtoRow. + */ + public int convertYtoRow(int y) { + return (defaultCharHeight > 0) ? (y / defaultCharHeight) + 1 : 1; + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaConstants.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaConstants.java index 03ecef2..ad9d459 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaConstants.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaConstants.java @@ -79,8 +79,10 @@ public final class GocaConstants { public static final int G_GCFLT = 0x85; // Fillet at Current Position public static final int G_GCARC = 0x86; // Partial Arc at Current Position public static final int G_GCFARC = 0x87; // Full Arc at Current Position - public static final int G_GEIMG = 0x91; // End Image + public static final int G_GBIMGC = 0x91; // Begin Image at Current Position + public static final int G_GEIMG_ALT = 0x91; // End Image (Alternate) public static final int G_GIMD = 0x92; // Image Data + public static final int G_GEIMG = 0x93; // End Image (Standard / HoD 147) public static final int G_GCRLIN = 0xA1; // Relative Line at Current Position public static final int G_GLINE = 0xC1; // Line (Absolute) public static final int G_GMRK = 0xC2; // Marker (Absolute) @@ -88,9 +90,13 @@ public final class GocaConstants { public static final int G_GFLT = 0xC5; // Fillet (Absolute) public static final int G_GARC = 0xC6; // Partial Arc (Absolute) public static final int G_GFARC = 0xC7; // Full Arc (Absolute) - public static final int G_GBIMG = 0xD1; // Begin Image + public static final int G_GBIMG = 0xD1; // Begin Image (Absolute) public static final int G_GRLINE = 0xE1; // Relative Line (Absolute Start) + // Graphics Cursor Shapes (IBM Host On-Demand setHODGCursorShape) + public static final int GCURSOR_SHAPE_CROSSHAIR = 1; + public static final int GCURSOR_SHAPE_BOX = 2; + // Line Types (GSLT) public static final int LT_DEFAULT = 0; public static final int LT_DOT = 1; @@ -151,7 +157,7 @@ public final class GocaConstants { public static final int CD_RL = 3; // Right to Left public static final int CD_BT = 4; // Bottom to Top - // Foreground / Background Mix Modes (GSMX / GSBMX) + // Foreground Mix Modes (GSMX 0x0C) public static final int MIX_DEFAULT = 0; public static final int MIX_OR = 1; public static final int MIX_OVER = 2; @@ -159,6 +165,11 @@ public final class GocaConstants { public static final int MIX_XOR = 4; public static final int MIX_UNDER = 5; + // Background Mix Modes (GSBMX 0x0D per IBM GOCA spec) + public static final int BMX_DEFAULT = 0; // Default (Transparent / Leave) + public static final int BMX_OPAQUE = 1; // Opaque (Overpaint with background color) + public static final int BMX_TRANSPARENT = 2; // Transparent (Leave underlying pixels unchanged) + // Fill Rules (GBAR 0x68 flags) public static final int FILL_RULE_EVEN_ODD = 0; public static final int FILL_RULE_WINDING = 1; @@ -184,11 +195,11 @@ public final class GocaConstants { 0xFFFFFFFF, // 7: Neutral White (255, 255, 255) 0xFF000000, // 8: Black (0, 0, 0) 0xFF000080, // 9: Deep Blue (0, 0, 128) - 0xFF800000, // 10: Orange / Dark Red (128, 0, 0) + 0xFFFFA200, // 10: Orange (255, 162, 0) 0xFF800080, // 11: Purple (128, 0, 128) 0xFF008000, // 12: Pale Green (0, 128, 0) 0xFF008080, // 13: Pale Cyan (0, 128, 128) - 0xFFD79700, // 14: Mustard (215, 151, 0) + 0xFFA0A000, // 14: Mustard (160, 160, 0) 0xFFC0C0C0, // 15: Grey / Light White (192, 192, 192) 0xFF492400 // 16: Brown (73, 36, 0) }; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java index 2e4d429..4867649 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java @@ -160,6 +160,9 @@ public class GocaDecoder { public synchronized void setGraphicCursorPosition(int x, int y) { this.graphicCursorX = x; this.graphicCursorY = y; + if (plane != null) { + plane.setGraphicCursorPosition(x, y); + } } public synchronized void setGraphicCursorFromPixel(int px, int py) { @@ -361,10 +364,11 @@ public class GocaDecoder { if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) { return 1; } - // Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 91 00) + // Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 93 00, 91 00) if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE || - order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG) { + order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG || + (inImage && order == GocaConstants.G_GEIMG_ALT)) { return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1; } if (idx + 1 >= end) { @@ -510,9 +514,31 @@ public class GocaDecoder { idx += orderLen; break; } - case GocaConstants.G_GEIMG: { - endImage(); - idx += orderLen; + case GocaConstants.G_GEIMG: // 0x93 + case GocaConstants.G_GEIMG_ALT: { // 0x91 + if (inImage || order == GocaConstants.G_GEIMG) { + endImage(); + idx += orderLen; + } else { + // 0x91: Begin Image at Current Position (G_GBIMGC) + if (payloadLen >= 4 && idx + 2 + payloadLen <= end) { + int w = readCoord(inputData, idx + 2); + int h = readCoord(inputData, idx + 4); + int bitDepth = GocaConstants.BPP_1; + int compression = GocaConstants.IMG_UNCOMPRESSED; + if (payloadLen >= 5) { + int fmt = inputData[idx + 6] & 0xFF; + if (fmt == 2) bitDepth = GocaConstants.BPP_2; + else if (fmt == 4) bitDepth = GocaConstants.BPP_4; + else if (fmt == 8) bitDepth = GocaConstants.BPP_8; + } + if (payloadLen >= 6) { + compression = inputData[idx + 7] & 0xFF; + } + beginImage(curX, curY, w, h, bitDepth, compression); + } + idx += orderLen; + } break; } case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70) @@ -955,12 +981,18 @@ public class GocaDecoder { break; } case GocaConstants.P_ATTCUR: { // 0x08: Attach Graphic Cursor - this.graphicsCursorActive = true; + attachGraphicCursor(curX, curY); + if (plane != null) { + plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_CROSSHAIR); + } idx += 2; break; } case GocaConstants.P_DETCUR: { // 0x09: Detach Graphic Cursor - this.graphicsCursorActive = false; + detachGraphicCursor(); + if (plane != null) { + plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_BOX); + } idx += 2; break; } @@ -998,9 +1030,16 @@ public class GocaDecoder { } break; } - case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position (HODGraphicCursorPosition - No-op per HOD architecture) + case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position if (idx + 1 < end) { int len = data[idx + 1] & 0xFF; + if (len >= 4 && idx + 2 + 4 <= end) { + int x = readCoord(data, idx + 2); + int y = readCoord(data, idx + 4); + setGraphicCursorPosition(x, y); + curX = x; + curY = y; + } idx += 2 + len; } else { idx++; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java index df02e62..7d42115 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java @@ -1,5 +1,8 @@ package haus.nightmare.lib3270j.graphics; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -52,6 +55,66 @@ public class GraphicsPlane { private int graphicCursorX = 0; private int graphicCursorY = 0; private int hodCursorShape = 0; + private int currentColorArgb = 0xFF00FF00; + private int currentLineType = GocaConstants.LT_SOLID; + private int currentLineWidth = GocaConstants.LW_NORMAL; + private int currentMixMode = 0; + + public synchronized void setDimensions(int width, int height) { + resize(width, height); + } + + public synchronized void setColor(int argb) { + this.currentColorArgb = argb; + } + + public synchronized int getColor() { + return currentColorArgb; + } + + public synchronized void setLineWidth(int width) { + this.currentLineWidth = Math.max(1, width); + } + + public synchronized int getLineWidth() { + return currentLineWidth; + } + + public synchronized void setLineType(int type) { + this.currentLineType = type; + } + + public synchronized int getLineType() { + return currentLineType; + } + + public synchronized void setMixMode(int mix) { + this.currentMixMode = mix; + } + + public synchronized int getMixMode() { + return currentMixMode; + } + + public synchronized void drawLine(int x1, int y1, int x2, int y2) { + drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth); + } + + public synchronized BufferedImage toBufferedImage() { + BufferedImage img = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB); + if (rgbBuffer != null) { + img.setRGB(0, 0, canvasWidth, canvasHeight, rgbBuffer, 0, canvasWidth); + } + return img; + } + + public synchronized Image getImage() { + return toBufferedImage(); + } + + public synchronized Graphics getGraphics() { + return toBufferedImage().getGraphics(); + } public void setProgramSymbolManager(ProgramSymbolManager psm) { this.programSymbolManager = psm; @@ -746,7 +809,9 @@ public class GraphicsPlane { int bg = bgColorArgb; boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) && - (bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0); + (bgMix == GocaConstants.BMX_DEFAULT || bgMix == GocaConstants.BMX_TRANSPARENT || + bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0 || bgMix == 2); + boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1); if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { int minY = py[0]; @@ -817,7 +882,7 @@ public class GraphicsPlane { boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); if (bit) { setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { setPixel(x, y, bg); } } else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { @@ -826,7 +891,7 @@ public class GraphicsPlane { int b = patRows[y & 7] & 0xFF; if (((b >> (7 - (x & 7))) & 1) != 0) { setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { setPixel(x, y, bg); } } @@ -850,7 +915,7 @@ public class GraphicsPlane { boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); if (bit) { setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { setPixel(x, y, bg); } } else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { @@ -859,7 +924,7 @@ public class GraphicsPlane { int b = patRows[y & 7] & 0xFF; if (((b >> (7 - (x & 7))) & 1) != 0) { setPixel(x, y, fill); - } else if (bgMix == GocaConstants.MIX_OVER) { + } else if (isOpaqueBg) { setPixel(x, y, bg); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODBitImage.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODBitImage.java new file mode 100644 index 0000000..5a689a7 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODBitImage.java @@ -0,0 +1,155 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.FilteredImageSource; +import java.awt.image.MemoryImageSource; + +/** + * Bitmap image container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBitImage). + */ +public class HODBitImage { + protected Component vComponent; + protected Dimension iSize = new Dimension(); + protected Dimension iScaledSize = new Dimension(); + protected int iDepth; + protected int iScanLength; + protected boolean _iUseGraphicColors; + protected byte[] iScaledImageData; + protected Image[] hImage; + protected Image[] iScaledImage; + protected int transparentBG = 0; + protected byte[] hImageData; + protected int iBaseColor; + + public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) { + this.vComponent = comp; + this.iSize.width = width; + this.iSize.height = height; + this.hImageData = (data != null) ? data : new byte[0]; + this.iBaseColor = baseColor; + this.iDepth = depth; + this._iUseGraphicColors = useGraphicColors; + this.hImage = new Image[depth == 1 ? 17 : 1]; + this.iScaledImage = new Image[depth == 1 ? 17 : 1]; + this.buildHODImage(); + } + + public Dimension getImageSize() { + return new Dimension(this.iSize); + } + + public Dimension getImageScaledSize() { + return new Dimension(this.iScaledSize); + } + + public int[] getHODImageData(int colorIdx) { + int total = this.iSize.height * this.iSize.width; + int[] nArray = new int[total]; + if (total == 0) return nArray; + + if (this.iDepth == 1) { + int n2 = this.transparentBG; + int n3 = this.getHODColor(colorIdx); + this.iScanLength = (this.iSize.width + 7) / 8; + int n4 = 0; + int n5 = 0; + for (int i = 0; i < this.hImageData.length && n4 < this.iSize.height; ++n4, i += this.iScanLength) { + for (int j = 0; j < this.iSize.width; ++j) { + nArray[n5++] = getBit(this.hImageData, i, j) ? n3 : n2; + } + } + } else { + int n6 = this.transparentBG; + this.iScanLength = this.iSize.width; + for (int i = 0; i < total && i < this.hImageData.length; ++i) { + int b = this.hImageData[i] & 0xFF; + nArray[i] = (b != 0) ? this.getHODColor(b) : n6; + } + } + return nArray; + } + + public Image getHODImage(int colorIdx) { + return this.getHODImage(this.iSize.width, this.iSize.height, colorIdx); + } + + public Image getHODImage(int w, int h, int colorIdx) { + if (w <= 0 || h <= 0) return null; + boolean diffColor = this.iBaseColor != colorIdx && this.iDepth == 1; + boolean matchesBase = this.iSize.width == w && this.iSize.height == h; + boolean matchesScaled = this.iScaledSize.width == w && this.iScaledSize.height == h; + + if (diffColor) { + if (!matchesBase && !matchesScaled) { + this.iScaledSize.width = w; + this.iScaledSize.height = h; + this.scaleHODImage(); + } + Image image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx]; + if (image == null) { + HODColorChangeFilter filter = new HODColorChangeFilter(this.getHODColor(colorIdx)); + Image base = matchesBase ? this.hImage[this.iBaseColor] : this.iScaledImage[this.iBaseColor]; + if (base != null) { + FilteredImageSource source = new FilteredImageSource(base.getSource(), filter); + image = Toolkit.getDefaultToolkit().createImage(source); + if (matchesBase) { + this.hImage[colorIdx] = image; + } else { + this.iScaledImage[colorIdx] = image; + } + } + } + return image; + } + + if (matchesBase) { + return this.hImage[this.iDepth == 1 ? colorIdx : 0]; + } + if (matchesScaled) { + return this.iScaledImage[this.iDepth == 1 ? colorIdx : 0]; + } + + this.iScaledSize.width = w; + this.iScaledSize.height = h; + this.scaleHODImage(); + return this.iScaledImage[this.iDepth == 1 ? colorIdx : 0]; + } + + private void buildHODImage() { + if (this.iSize.width <= 0 || this.iSize.height <= 0) return; + int[] pixels = getHODImageData(this.iBaseColor); + MemoryImageSource mis = new MemoryImageSource(this.iSize.width, this.iSize.height, pixels, 0, this.iSize.width); + Image img = Toolkit.getDefaultToolkit().createImage(mis); + if (this.iDepth == 1) { + this.hImage[this.iBaseColor] = img; + } else { + this.hImage[0] = img; + } + } + + private void scaleHODImage() { + if (this.iScaledSize.width <= 0 || this.iScaledSize.height <= 0) return; + Image base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0]; + if (base != null) { + this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] = + base.getScaledInstance(this.iScaledSize.width, this.iScaledSize.height, Image.SCALE_FAST); + } + } + + private int getHODColor(int idx) { + if (idx == 0 && this.vComponent != null) { + return this.vComponent.getBackground().getRGB(); + } + return GocaConstants.getGocaColorArgb(idx); + } + + private static boolean getBit(byte[] data, int byteOffset, int bitIndex) { + int byteIdx = byteOffset + (bitIndex / 8); + if (byteIdx < 0 || byteIdx >= data.length) return false; + int mask = 1 << (7 - (bitIndex % 8)); + return (data[byteIdx] & mask) != 0; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODBounds.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODBounds.java new file mode 100644 index 0000000..8d9df0e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODBounds.java @@ -0,0 +1,118 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Point; +import java.awt.Rectangle; + +/** + * Bounding box encapsulation matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBounds). + */ +public class HODBounds { + private int upperLeftx; + private int upperLefty; + private int lowerRightx; + private int lowerRighty; + private int bWidth; + private int bHeight; + + public void set(int x1, int y1, int x2, int y2) { + this.upperLeftx = x1; + this.upperLefty = y1; + this.lowerRightx = x2; + this.lowerRighty = y2; + this.reHODCalc(); + } + + public int getHODLowerRightX() { + return this.lowerRightx; + } + + public void setHODLowerRightX(int n) { + this.lowerRightx = n; + this.calcHODWidth(); + } + + public int getHODLowerRightY() { + return this.lowerRighty; + } + + public int getHODUpperLeftX() { + return this.upperLeftx; + } + + public void setHODUpperLeftX(int n) { + this.upperLeftx = n; + this.calcHODWidth(); + } + + public int getHODUpperLeftY() { + return this.upperLefty; + } + + public void setHODUpperLeftY(int n) { + this.upperLefty = n; + this.calcHODHeight(); + } + + public void setHODLowerRightY(int n) { + this.lowerRighty = n; + this.calcHODHeight(); + } + + public Point getLowerRight() { + return new Point(this.lowerRightx, this.lowerRighty); + } + + public int getWidth() { + return this.bWidth; + } + + public int getHeight() { + return this.bHeight; + } + + public void setUpperLeft(Point point) { + if (point != null) { + this.upperLeftx = point.x; + this.upperLefty = point.y; + this.reHODCalc(); + } + } + + public void setLowerRight(Point point) { + if (point != null) { + this.lowerRightx = point.x; + this.lowerRighty = point.y; + this.reHODCalc(); + } + } + + public Point getUpperLeft() { + return new Point(this.upperLeftx, this.upperLefty); + } + + public Rectangle toHODRectangle() { + return new Rectangle(this.upperLeftx, this.upperLefty, this.bWidth, this.bHeight); + } + + public boolean isHODValid() { + return this.bWidth > 0 && this.bHeight > 0; + } + + private void reHODCalc() { + this.calcHODHeight(); + this.calcHODWidth(); + } + + private void calcHODWidth() { + this.bWidth = this.lowerRightx - this.upperLeftx + 1; + } + + private void calcHODHeight() { + this.bHeight = this.lowerRighty - this.upperLefty + 1; + } + + @Override + public String toString() { + return "ulx - " + this.upperLeftx + ", uly - " + this.upperLefty + ", lrx - " + this.lowerRightx + ", lry - " + this.lowerRighty; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODColorChangeFilter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODColorChangeFilter.java new file mode 100644 index 0000000..9857e39 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODColorChangeFilter.java @@ -0,0 +1,64 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Color; +import java.awt.image.RGBImageFilter; + +/** + * Image filter that replaces occurrences of one color with another color. + * Conforms to IBM Host On-Demand HODColorChangeFilter. + */ +public class HODColorChangeFilter extends RGBImageFilter { + + private int oldRgb; + private int newRgb; + + public HODColorChangeFilter(int newRgb) { + this.canFilterIndexColorModel = true; + this.oldRgb = -1; + this.newRgb = newRgb | 0xFF000000; + } + + public HODColorChangeFilter(Color newColor) { + this(newColor != null ? newColor.getRGB() : 0); + } + + public HODColorChangeFilter(int oldRgb, int newRgb) { + this.canFilterIndexColorModel = true; + this.oldRgb = oldRgb & 0x00FFFFFF; + this.newRgb = newRgb; + } + + public HODColorChangeFilter(Color oldColor, Color newColor) { + this(oldColor != null ? oldColor.getRGB() : 0, newColor != null ? newColor.getRGB() : 0); + } + + public int getOldRgb() { + return oldRgb; + } + + public void setOldRgb(int oldRgb) { + this.oldRgb = oldRgb & 0x00FFFFFF; + } + + public int getNewRgb() { + return newRgb; + } + + public void setNewRgb(int newRgb) { + this.newRgb = newRgb; + } + + @Override + public int filterRGB(int x, int y, int rgb) { + if (oldRgb == -1) { + if ((rgb & 0xFF000000) != 0) { + return newRgb; + } + return rgb; + } + if ((rgb & 0x00FFFFFF) == oldRgb) { + return newRgb; + } + return rgb; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODGraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODGraphicsPlane.java new file mode 100644 index 0000000..d50949b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODGraphicsPlane.java @@ -0,0 +1,171 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +/** + * Headless graphics plane facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODGraphicsPlane). + */ +public class HODGraphicsPlane { + private final GraphicsPlane delegate; + private final HODBounds bounds = new HODBounds(); + private Color currentColor = Color.GREEN; + private int currentColorIndex = 4; + private int currentLineWidth = 1; + private int currentLineType = 0; + private int currentForegroundMix = 0; + private int patternSymbol = 0; + private int symbolSet = 0; + + public HODGraphicsPlane() { + this(new GraphicsPlane(720, 512)); + } + + public HODGraphicsPlane(int width, int height) { + this(new GraphicsPlane(width, height)); + } + + public HODGraphicsPlane(GraphicsPlane delegate) { + this.delegate = (delegate != null) ? delegate : new GraphicsPlane(720, 512); + this.bounds.set(0, 0, this.delegate.getCanvasWidth(), this.delegate.getCanvasHeight()); + } + + public GraphicsPlane getDelegate() { + return delegate; + } + + public void resize(Dimension dim, boolean keepContent) { + if (dim != null) { + delegate.setDimensions(dim.width, dim.height); + this.bounds.set(0, 0, dim.width, dim.height); + } + } + + public void clear() { + delegate.clear(); + this.bounds.set(0, 0, delegate.getCanvasWidth(), delegate.getCanvasHeight()); + } + + public Graphics getHODGraphics() { + return delegate.getGraphics(); + } + + public Image getHODImage() { + return delegate.getImage(); + } + + public void setHODTemporaryGraphics(Graphics g) { + // No-op or temporary override + } + + public void restoreHODGraphics() { + // Restore + } + + public void setHODGraphColor(int colorIndex) { + this.currentColorIndex = colorIndex; + int argb = GocaConstants.getGocaColorArgb(colorIndex); + this.currentColor = new Color(argb, true); + delegate.setColor(argb); + } + + public void setHODGraphColor(Color color) { + if (color != null) { + this.currentColor = color; + delegate.setColor(color.getRGB()); + } + } + + public void setHODLineWidth(int width) { + this.currentLineWidth = Math.max(1, width); + delegate.setLineWidth(this.currentLineWidth); + } + + public void setHODLineType(int type) { + this.currentLineType = type; + delegate.setLineType(type); + } + + public void setHODPatternSym(int sym, int set) { + this.patternSymbol = sym; + this.symbolSet = set; + } + + public void setHODForegroundMix(int mix) { + this.currentForegroundMix = mix; + delegate.setMixMode(mix); + } + + public void drawHODLine(int x1, int y1, int x2, int y2) { + delegate.drawLine(x1, y1, x2, y2); + updateHODBounds(x1, y1); + updateHODBounds(x2, y2); + } + + public void drawHODLines(int[] xCoords, int[] yCoords, int numPoints) { + if (xCoords == null || yCoords == null || numPoints < 2) return; + for (int i = 1; i < numPoints && i < xCoords.length && i < yCoords.length; i++) { + drawHODLine(xCoords[i - 1], yCoords[i - 1], xCoords[i], yCoords[i]); + } + } + + public void drawHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + Graphics g = delegate.getGraphics(); + if (g != null) { + g.setColor(currentColor); + g.drawArc(x, y, width, height, startAngle, arcAngle); + updateHODBounds(x, y); + updateHODBounds(x + width, y + height); + } + } + + public void fillHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + Graphics g = delegate.getGraphics(); + if (g != null) { + g.setColor(currentColor); + g.fillArc(x, y, width, height, startAngle, arcAngle); + updateHODBounds(x, y); + updateHODBounds(x + width, y + height); + } + } + + public void drawHODImage(HODBitImage bitImage, int x, int y, int w, int h) { + if (bitImage != null) { + Image img = bitImage.getHODImage(w, h, currentColorIndex); + if (img != null) { + Graphics g = delegate.getGraphics(); + if (g != null) { + g.drawImage(img, x, y, null); + updateHODBounds(x, y); + updateHODBounds(x + w, y + h); + } + } + } + } + + public void fillHODArea(FillArea area) { + if (area != null) { + area.fill(delegate, currentColor.getRGB(), 0, GocaConstants.PT_SOLID, false, 0, 0, 1, 0, 0, null); + Rectangle b = area.getBounds(); + updateHODBounds(b.x, b.y); + updateHODBounds(b.x + b.width, b.y + b.height); + } + } + + public void updateHODBounds(int x, int y) { + if (x < bounds.getHODUpperLeftX()) bounds.setHODUpperLeftX(x); + if (y < bounds.getHODUpperLeftY()) bounds.setHODUpperLeftY(y); + if (x > bounds.getHODLowerRightX()) bounds.setHODLowerRightX(x); + if (y > bounds.getHODLowerRightY()) bounds.setHODLowerRightY(y); + } + + public HODBounds getHODBounds() { + return bounds; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODPart.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODPart.java new file mode 100644 index 0000000..d170e74 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODPart.java @@ -0,0 +1,99 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Rectangle; +import java.io.Serializable; + +/** + * Visual part container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODPart). + */ +public class HODPart extends Rectangle implements Serializable { + protected Component hodParent; + protected Font _hodFont; + protected Color foregroundColor; + protected Color backgroundColor; + protected Boolean isTransparent; + protected Boolean _visible = true; + + protected HODPart() {} + + public HODPart(Component component) { + this(); + this.setHODParent(component); + } + + public HODPart(Component component, Dimension dimension) { + super(dimension); + this.setHODParent(component); + } + + public HODPart(Component component, Rectangle rectangle) { + super(rectangle); + this.setHODParent(component); + } + + public HODPart(HODPart hODPart) { + this(hODPart.getHODParent(), hODPart.getSize()); + this.setHODBackground(hODPart.getHODBackground()); + this.setHODForeground(hODPart.getHODForeground()); + this.setHODFont(hODPart.getHODFont()); + } + + public Component getHODParent() { + return this.hodParent; + } + + public void setHODParent(Component component) { + if (component != null && !component.equals(this.hodParent)) { + this.hodParent = component; + } + } + + public void repaint() { + if (this.hodParent != null) { + this.hodParent.repaint(this.x, this.y, this.width, this.height); + } + } + + public void paint(Component component, Graphics graphics, int x, int y, int w, int h) { + this.setBounds(x, y, w, h); + if (Boolean.TRUE.equals(this._visible)) { + this.paintHODView(graphics); + } + } + + protected void paintHODView(Graphics graphics) {} + + public Color getHODBackground() { + return this.backgroundColor; + } + + public void setHODBackground(Color color) { + this.backgroundColor = color; + } + + public Color getHODForeground() { + return this.foregroundColor; + } + + public void setHODForeground(Color color) { + this.foregroundColor = color; + } + + public Font getHODFont() { + return this._hodFont; + } + + public void setHODFont(Font font) { + this._hodFont = font; + } + + public Insets getInsets() { + return new Insets(0, 0, 0, 0); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODProgramSymbolManager.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODProgramSymbolManager.java new file mode 100644 index 0000000..c7a4cea --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODProgramSymbolManager.java @@ -0,0 +1,73 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.image.BufferedImage; + +/** + * Programmed Symbol Set manager facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODProgramSymbolManager). + */ +public class HODProgramSymbolManager { + public static final int MAX_HOD_SLOT = 254; + public static final int MIN_HOD_SLOT = 64; + public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; + public static final int NUMBER_SINGLE_PLANE_PS_SETS = 2; + public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; + public static final int NUM_SLOTS = 191; + public static final int NUMBER_TEXT_SYMBOL_SETS = 6; + public static final int NUMBER_SYMBOL_SETS = 10; + + private final ProgramSymbolManager delegate; + + public HODProgramSymbolManager() { + this(new ProgramSymbolManager()); + } + + public HODProgramSymbolManager(ProgramSymbolManager delegate) { + this.delegate = (delegate != null) ? delegate : new ProgramSymbolManager(); + } + + public ProgramSymbolManager getDelegate() { + return delegate; + } + + public HODBitImage getHODBitImage(int lcid, int codepoint) { + ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint); + if (slot == null) return null; + + int w = slot.getWidth(); + int h = slot.getHeight(); + byte[] raw = slot.getRawData(); + return new HODBitImage(null, w, h, raw, 7, slot.isTriplePlane() ? 8 : 1, false); + } + + public void loadps(char[] chars) { + if (chars == null || chars.length == 0) return; + byte[] bytes = new byte[chars.length]; + for (int i = 0; i < chars.length; i++) { + bytes[i] = (byte) (chars[i] & 0xFF); + } + delegate.loadProgrammedSymbolSet(bytes, 0, bytes.length); + } + + public void drawHODImageCharacter(Graphics g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) { + if (g == null || pt == null) return; + ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint); + if (slot != null) { + int fg = GocaConstants.getGocaColorArgb(colorIdx); + BufferedImage img = slot.getScaledImage(cellW, cellH, fg, 0); + if (img != null) { + g.drawImage(img, pt.x, pt.y, null); + } + } + } + + public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codepoint) { + return delegate.getSymbol(lcid, codepoint); + } + + public void clear() { + delegate.clearAll(); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODTransform.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODTransform.java new file mode 100644 index 0000000..b0beead --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODTransform.java @@ -0,0 +1,58 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Point; +import java.awt.Rectangle; + +/** + * Coordinate transform adapter matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODTransform). + */ +public class HODTransform { + private double transformX; + private double transformY; + private int defaultCharWidth; + private int defaultCharHeight; + + public HODTransform(int charW, int charH, int defaultCharW, int defaultCharH) { + this.defaultCharWidth = defaultCharW; + this.defaultCharHeight = defaultCharH; + this.resize(charW, charH); + } + + public void resize(int charW, int charH) { + this.transformX = (this.defaultCharWidth > 0) ? (double) charW / (double) this.defaultCharWidth : 1.0; + this.transformY = (this.defaultCharHeight > 0) ? (double) charH / (double) this.defaultCharHeight : 1.0; + } + + public HODBounds calculate(HODBounds hODBounds) { + if (hODBounds == null) return null; + hODBounds.setHODUpperLeftX((int) ((double) hODBounds.getHODUpperLeftX() * this.transformX + 0.5)); + hODBounds.setHODUpperLeftY((int) ((double) hODBounds.getHODUpperLeftY() * this.transformY + 0.5)); + hODBounds.setHODLowerRightX((int) ((double) hODBounds.getHODLowerRightX() * this.transformX + 0.5)); + hODBounds.setHODLowerRightY((int) ((double) hODBounds.getHODLowerRightY() * this.transformY + 0.5)); + return hODBounds; + } + + public Point calculate(Point point) { + if (point == null) return null; + point.x = (int) ((double) point.x * this.transformX + 0.5); + point.y = (int) ((double) point.y * this.transformY + 0.5); + return point; + } + + public Rectangle calculate(Rectangle rectangle) { + if (rectangle == null) return null; + rectangle.x = (int) ((double) rectangle.x * this.transformX + 0.5); + rectangle.y = (int) ((double) rectangle.y * this.transformY + 0.5); + rectangle.width = (int) ((double) rectangle.width * this.transformX + 0.5); + rectangle.height = (int) ((double) rectangle.height * this.transformY + 0.5); + return rectangle; + } + + public double getXTrans() { + return this.transformX; + } + + public double getYTrans() { + return this.transformY; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODTransparentColorFilter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODTransparentColorFilter.java new file mode 100644 index 0000000..e26b11d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODTransparentColorFilter.java @@ -0,0 +1,38 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Color; +import java.awt.image.RGBImageFilter; + +/** + * Image filter that keys out a specific background color by setting its alpha to 0x00. + * Conforms to IBM Host On-Demand HODTransparentColorFilter. + */ +public class HODTransparentColorFilter extends RGBImageFilter { + + private int transparentRgb; + + public HODTransparentColorFilter(int rgb) { + this.canFilterIndexColorModel = true; + this.transparentRgb = rgb & 0x00FFFFFF; + } + + public HODTransparentColorFilter(Color color) { + this(color != null ? color.getRGB() : 0); + } + + public int getTransparentRgb() { + return transparentRgb; + } + + public void setTransparentRgb(int rgb) { + this.transparentRgb = rgb & 0x00FFFFFF; + } + + @Override + public int filterRGB(int x, int y, int rgb) { + if ((rgb & 0x00FFFFFF) == transparentRgb) { + return 0x00000000; + } + return rgb; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODWallpaper.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODWallpaper.java new file mode 100644 index 0000000..7194aa4 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/HODWallpaper.java @@ -0,0 +1,139 @@ +package haus.nightmare.lib3270j.graphics; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Insets; +import java.awt.image.BufferedImage; + +/** + * Wallpaper background manager matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODWallpaper). + */ +public class HODWallpaper extends HODPart { + public static final int HOD_TILE = 0; + public static final int HOD_CENTER = 1; + public static final int HOD_STRETCH = 2; + + private int _display = HOD_CENTER; + private Image rawImage; + private Image backgroundImage; + + public HODWallpaper() { + this(HOD_CENTER); + } + + public HODWallpaper(int displayMode) { + this.setDisplay(displayMode); + } + + public HODWallpaper(Image image, int displayMode) { + this(displayMode); + this.setImage(image); + } + + public void setDisplay(int displayMode) { + if (displayMode >= 0 && displayMode <= 2) { + this._display = displayMode; + } else { + this._display = HOD_CENTER; + } + this.backgroundImage = null; + } + + public int getDisplay() { + return this._display; + } + + public void setImage(Image image) { + this.rawImage = image; + this.backgroundImage = null; + this.repaint(); + } + + public Image getHODImage() { + return this.rawImage; + } + + @Override + public void paint(Component component, Graphics graphics, int x, int y, int w, int h) { + this.setBounds(x, y, w, h); + this.setHODParent(component); + super.paint(component, graphics, x, y, w, h); + } + + @Override + protected void paintHODView(Graphics graphics) { + Image image = this.getHODImage(); + int display = this.getDisplay(); + Component component = this.getHODParent(); + + if (image == null || component == null) { + return; + } + + if (display == HOD_CENTER) { + this.centerHODImage(graphics, image); + } else if (display == HOD_TILE) { + this.hodtileImage(graphics, image); + } else if (display == HOD_STRETCH) { + this.stretchHODImage(graphics, image); + } + } + + protected void hodtileImage(Graphics graphics, Image image) { + Dimension imgSize = getImageSize(image); + if (imgSize.width <= 0 || imgSize.height <= 0) return; + + Insets insets = this.getInsets(); + int startX = this.x + insets.left; + int startY = this.y + insets.top; + int availW = this.width - (insets.left + insets.right); + int availH = this.height - (insets.top + insets.bottom); + + int cols = (availW / imgSize.width) + 1; + int rows = (availH / imgSize.height) + 1; + + int curX = startX; + for (int i = 0; i < cols; i++) { + int curY = startY; + for (int j = 0; j < rows; j++) { + graphics.drawImage(image, curX, curY, this.getHODParent()); + curY += imgSize.height; + } + curX += imgSize.width; + } + } + + protected void centerHODImage(Graphics graphics, Image image) { + Dimension imgSize = getImageSize(image); + if (imgSize.width <= 0 || imgSize.height <= 0) return; + + Insets insets = this.getInsets(); + int cx = this.x + insets.left + (this.width - imgSize.width) / 2; + int cy = this.y + insets.top + (this.height - imgSize.height) / 2; + graphics.drawImage(image, cx, cy, this.getHODParent()); + } + + protected void stretchHODImage(Graphics graphics, Image image) { + Insets insets = this.getInsets(); + int sx = this.x + insets.left; + int sy = this.y + insets.top; + int sw = this.width - (insets.left + insets.right); + int sh = this.height - (insets.top + insets.bottom); + graphics.drawImage(image, sx, sy, sw, sh, this.getHODParent()); + } + + private Dimension getImageSize(Image image) { + if (image == null) return new Dimension(0, 0); + Component c = this.getHODParent(); + int w = image.getWidth(c); + int h = image.getHeight(c); + if (w <= 0 && image instanceof BufferedImage) { + w = ((BufferedImage) image).getWidth(); + h = ((BufferedImage) image).getHeight(); + } + return new Dimension(Math.max(0, w), Math.max(0, h)); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java index 48b3b86..17df9c9 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java @@ -148,6 +148,17 @@ public class ProgramSymbolManager { /** * Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload. */ + public synchronized void loadps(byte[] data, int offset, int length) { + if (data == null || length <= 0 || offset < 0 || offset + length > data.length) return; + byte[] payload = new byte[length]; + System.arraycopy(data, offset, payload, 0, length); + loadps(payload); + } + + public synchronized void loadProgrammedSymbolSet(byte[] data, int offset, int length) { + loadps(data, offset, length); + } + public synchronized void loadps(byte[] data) { if (data == null || data.length < 4) { logger.warning("LOADPS: Payload too short (" + (data == null ? 0 : data.length) + " bytes)"); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java index 8d43184..0100b43 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java @@ -90,6 +90,10 @@ public class ProgramSymbolSet { return pixelData; } + public byte[] getRawData() { + return pixelData; + } + public boolean isTriplePlane() { return isTriplePlane; } @@ -174,12 +178,21 @@ public class ProgramSymbolSet { } else if (!isTriplePlane) { rgbArray[idx] = fgArgb; } else { - // Triple-Plane RGB composite: + // Triple-Plane RGB composite aligned with IBM Host On-Demand 16-color GOCA palette: // val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue - int r = (val & 0x01) != 0 ? 255 : 0; - int g = (val & 0x02) != 0 ? 255 : 0; - int b = (val & 0x04) != 0 ? 255 : 0; - rgbArray[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b; + int colorMask = val & 0x07; + int colorArgb; + switch (colorMask) { + case 1: colorArgb = GocaConstants.GOCA_COLORS[2]; break; // Red + case 2: colorArgb = GocaConstants.GOCA_COLORS[4]; break; // Green + case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow + case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue (CUSTOMBLUE) + case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink + case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise + case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White + default: colorArgb = bgArgb; break; + } + rgbArray[idx] = colorArgb; } } } 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 77a685b..86ab4d9 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -7,6 +7,7 @@ import haus.nightmare.lib3270j.telnet.TelnetFSM; import haus.nightmare.lib3270j.protocol.TelnetConstants; import haus.nightmare.lib3270j.ecl.ECLOIA; import haus.nightmare.lib3270j.ecl.ECLConstants; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import java.io.ByteArrayOutputStream; @@ -66,6 +67,16 @@ public class InputProcessor { return gocaDecoder; } + private DataStreamProcessor.OutputSender outputSender; + + public void setOutputSender(DataStreamProcessor.OutputSender outputSender) { + this.outputSender = outputSender; + } + + public DataStreamProcessor.OutputSender getOutputSender() { + return outputSender; + } + public enum OiaStatus { NOT_CONNECTED("OFFLINE"), X_SYSTEM("X SYSTEM"), @@ -493,6 +504,16 @@ public class InputProcessor { * Send an AID key (Enter, PF1-24, PA1-3, Clear). */ public void sendAid(int aidCode) { + sendAid(aidCode, -1); + } + + /** + * Send an AID key with an explicitly specified cursor address. + */ + public void sendAid(int aidCode, int cursorAddress) { + if (cursorAddress >= 0 && screen != null) { + screen.setCursorAddress(cursorAddress); + } log.fine("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked); if (aidCode == AID_SYSREQ) { if (fsm != null && fsm.isTn3270eNegotiated()) { @@ -596,6 +617,8 @@ public class InputProcessor { fsm.sendSscpLuData(data); } else if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) { fsm.send3270Data(data); + } else if (outputSender != null) { + outputSender.send3270Data(data); } } @@ -1760,5 +1783,90 @@ public class InputProcessor { public TelnetFSM getTelnetFSM() { return fsm; } + + // ========== Phase 3: HoD PS3270 Keystroke Methods ========== + + /** + * Handles key-down event matching HoD PS3270.keyDown(int key, boolean shift). + */ + public boolean keyDown(int key, boolean shift) { + if (keyboardLocked) { + if (key == 27) { // Shift-Escape or Escape unlocks keyboard + setKeyboardLocked(false); + return true; + } + return false; + } + + // Process standard ASCII printable characters + if (key >= 32 && key < 127) { + processChar((char) key); + return true; + } + + switch (key) { + case '\n': + case '\r': + processEnter(); + return true; + case '\t': + if (shift) { + processBackTab(); + } else { + processTab(); + } + return true; + case 8: // Backspace + processBackspace(); + return true; + case 127: // Delete + processDelete(); + return true; + case 1004: // Arrow Up + processCursorUp(); + return true; + case 1005: // Arrow Down + processCursorDown(); + return true; + case 1006: // Arrow Left + processCursorLeft(); + return true; + case 1007: // Arrow Right + processCursorRight(); + return true; + default: + if (key > 0 && Character.isDefined((char) key)) { + processChar((char) key); + return true; + } + return false; + } + } + + /** + * Handles key-up event matching HoD PS3270.keyUp(int key, boolean shift). + */ + public boolean keyUp(int key, boolean shift) { + return true; + } + + /** + * Direct character keystroke insertion matching HoD PS3270.CharKeyStrokes(int code). + */ + public boolean CharKeyStrokes(int code) { + if (keyboardLocked) return false; + if (code >= 0 && Character.isDefined((char) code)) { + processChar((char) code); + return true; + } + return false; + } + + /** + * Processes any pending keystrokes matching HoD PS3270.ProcessKeyStrokes(). + */ + public void ProcessKeyStrokes() { + // Synchronous processing + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java index 8beedbb..b874f34 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/nvt/NvtProcessor.java @@ -206,6 +206,10 @@ public class NvtProcessor { /** * Process incoming ASCII NVT data bytes. */ + public synchronized void processBytes(byte[] data, int offset, int length) { + processNVTData(data, offset, length); + } + public synchronized void processNVTData(byte[] data, int offset, int length) { if (length <= 0) return; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/DS3270P.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/DS3270P.java new file mode 100644 index 0000000..cbcb145 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/DS3270P.java @@ -0,0 +1,353 @@ +package haus.nightmare.lib3270j.printer; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.protocol.DS3270Constants; +import haus.nightmare.lib3270j.protocol.TN3270EConstants; + +import java.io.ByteArrayOutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Host 3270 Printer Data Stream Processor (DS3270P). + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.DS3270P. + * + * Coordinates data stream parsing and demultiplexing for both: + * - LU Type 1: SCS (SNA Character String) and LU1 WSF (Write Structured Field) + * - LU Type 3: 3270 Printer Data Stream (WCC, SBA, RA, etc.) + */ +public class DS3270P { + + private static final Logger log = Logger.getLogger(DS3270P.class.getName()); + + private Telnet3270EP telnet; + private PrinterConfig config; + private PD3270 pd; + private PrintSCS3270 scs; + private PrintPS3270 printPs; + private EbcdicTranslator translator; + private short activeLuType = PrinterConstants.LU_TYPE_UNKNOWN; + + // In-memory buffer for no-arg receiveDataLU1() + private final ByteArrayOutputStream pendingBufferLU1 = new ByteArrayOutputStream(); + + public DS3270P() { + this(new PrinterConfig()); + } + + public DS3270P(PrinterConfig config) { + this(null, config, null, null, null, null); + } + + public DS3270P(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + this(null, config, pd, null, null, translator); + } + + public DS3270P(Telnet3270EP telnet, PrinterConfig config, PD3270 pd, + PrintSCS3270 scs, PrintPS3270 printPs, EbcdicTranslator translator) { + this.telnet = telnet; + this.config = config != null ? config : new PrinterConfig(); + this.pd = pd != null ? pd : new PD3270(this.config); + this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage()); + this.scs = scs != null ? scs : new PrintSCS3270(this.config, this.pd, this.translator); + this.printPs = printPs != null ? printPs : new PrintPS3270(this.config, this.pd, this.translator); + if (this.telnet != null) { + this.activeLuType = this.telnet.getActiveLuType(); + } + } + + // ========== Primary Data Ingestion (Fn #1 - #6) ========== + + /** + * Primary entry point for incoming host printer stream data in short array format. + */ + public synchronized int receiveData(short[] sArray, int offset, int length) { + if (sArray == null || length <= 0 || offset < 0 || offset + length > sArray.length) { + return 0; + } + byte[] bArray = toByteArray(sArray, offset, length); + return receiveData(bArray, 0, bArray.length); + } + + /** + * Overload for byte array data ingestion. + */ + public synchronized int receiveData(byte[] data, int offset, int length) { + if (data == null || length <= 0 || offset < 0 || offset + length > data.length) { + return 0; + } + + short lu = activeLuType; + if (lu == PrinterConstants.LU_TYPE_UNKNOWN && telnet != null) { + lu = telnet.getActiveLuType(); + } + + if (lu == PrinterConstants.LU_TYPE_3_DS) { + receiveDataLU3(data, offset, length); + } else { + // Default to LU1 SCS + receiveDataLU1(data, offset, length); + } + return length; + } + + /** + * Process LU1 (SCS) data from short array. + */ + public synchronized void receiveDataLU1(short[] sArray, int offset, int length) { + if (sArray == null || length <= 0) return; + byte[] b = toByteArray(sArray, offset, length); + receiveDataLU1(b, 0, b.length); + } + + /** + * Process LU1 (SCS) data from byte array. + */ + public synchronized void receiveDataLU1(byte[] data, int offset, int length) { + if (data == null || length <= 0) return; + + // Check if data is an LU1 Write Structured Field (WSF) + // WSF command: 0xF3 (SNA WSF) or 0x11 + if (length >= 3 && ((data[offset] & 0xFF) == DS3270Constants.CMD_WSF || (data[offset] & 0xFF) == 0xF3)) { + processWSFLU1(data, offset + 1, length - 1); + return; + } + + processSCSData(data, offset, length); + } + + /** + * Process pending buffered LU1 data. + */ + public synchronized void receiveDataLU1() { + if (pendingBufferLU1.size() > 0) { + byte[] buf = pendingBufferLU1.toByteArray(); + pendingBufferLU1.reset(); + receiveDataLU1(buf, 0, buf.length); + } + } + + /** + * Append to internal pending buffer for later receiveDataLU1() execution. + */ + public synchronized void bufferDataLU1(byte[] data, int offset, int length) { + if (data != null && length > 0) { + pendingBufferLU1.write(data, offset, length); + } + } + + /** + * Process LU3 (3270 Printer Data Stream) data from short array. + */ + public synchronized void receiveDataLU3(short[] sArray, int offset, int length) { + if (sArray == null || length <= 0) return; + byte[] b = toByteArray(sArray, offset, length); + receiveDataLU3(b, 0, b.length); + } + + /** + * Process LU3 (3270 Printer Data Stream) data from byte array. + */ + public synchronized void receiveDataLU3(byte[] data, int offset, int length) { + if (data == null || length <= 0) return; + process3270DS(data, offset, length); + } + + // ========== Subsystem Processors (Fn #7 - #12) ========== + + /** + * Deliver SCS records to PrintSCS3270 engine. + */ + public synchronized void processSCSData(short[] sArray, int offset, int length) { + if (sArray == null || length <= 0) return; + byte[] b = toByteArray(sArray, offset, length); + processSCSData(b, 0, b.length); + } + + public synchronized void processSCSData(byte[] data, int offset, int length) { + if (data == null || length <= 0) return; + if (scs != null) { + scs.processHostData(data, offset, length); + } + } + + /** + * Process Write Structured Field (WSF) for LU1 printer sessions. + */ + public synchronized void processWSFLU1(short[] sArray, int offset, int length) { + if (sArray == null || length <= 0) return; + byte[] b = toByteArray(sArray, offset, length); + processWSFLU1(b, 0, b.length); + } + + public synchronized void processWSFLU1(byte[] data, int offset, int length) { + if (data == null || length <= 0) return; + + int pos = offset; + int end = offset + length; + + while (pos + 2 <= end) { + int sfLen = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF); + if (sfLen < 3 || pos + sfLen > end) { + // If length is 0 or malformed, fallback to remainder + sfLen = end - pos; + } + + int sfId = (pos + 2 < end) ? (data[pos + 2] & 0xFF) : 0; + log.fine(String.format("DS3270P WSF LU1 sfId=0x%02X len=%d", sfId, sfLen)); + + // Structured field dispatch: + // 0x40 = Outbound 3270DS + // 0x65 = SCS Data Unit + // 0x01 = Read Partition / Query + switch (sfId) { + case 0x65: // SCS Data + if (sfLen > 3) { + processSCSData(data, pos + 3, sfLen - 3); + } + break; + case 0x40: // 3270 Data Stream + if (sfLen > 3) { + process3270DS(data, pos + 3, sfLen - 3); + } + break; + default: + // Treat payload as SCS + if (sfLen > 3) { + processSCSData(data, pos + 3, sfLen - 3); + } + break; + } + pos += sfLen; + } + } + + /** + * Process 3270 Printer Data Stream in short array format. + */ + public synchronized void process3270DS(short[] sArray, int offset, int length) { + if (sArray == null || length <= 0) return; + byte[] b = toByteArray(sArray, offset, length); + process3270DS(b, 0, b.length); + } + + /** + * Process 3270 Printer Data Stream in byte array format. + */ + public synchronized void process3270DS(byte[] data, int offset, int length) { + if (data == null || length <= 0) return; + if (printPs != null) { + printPs.process3270PrintDS(data, offset, length); + } + } + + /** + * Process Write Control Character (WCC). + */ + public synchronized void processWCC(short wcc) { + processWCC((int) wcc & 0xFF); + } + + public synchronized void processWCC(int wcc) { + if (printPs != null) { + if (printPs.isStartPrint(wcc)) { + printPs.cancelAutoFlush(); + printPs.flushPrintBuffer(); + } else { + printPs.scheduleAutoFlush(); + } + } + } + + // ========== Session State & Lifecycle (Fn #13 - #17) ========== + + /** + * Handle End of Record (EOR) indicator. + */ + public synchronized void endOfRecord() { + if (activeLuType == PrinterConstants.LU_TYPE_1_SCS && scs != null) { + scs.flushLineBuffer(); + } else if (printPs != null) { + printPs.flushPrintBuffer(); + } + if (pd != null) { + pd.flush(); + } + } + + /** + * Handle End of File / End of Job (EOJ). + */ + public synchronized void endOfFile() { + processPrintComplete(); + } + + public synchronized void processPrintComplete() { + if (activeLuType == PrinterConstants.LU_TYPE_1_SCS && scs != null) { + scs.flushLineBuffer(); + } + if (printPs != null) { + printPs.processPrintComplete(); + } else if (pd != null) { + if (config.isFormFeedAtEoj()) { + pd.formFeed(); + } + if (config.isAutoFlushOnEoj()) { + pd.flush(); + } + pd.endJob(); + } + } + + /** + * Reset processor state to initial conditions. + */ + public synchronized void reset() { + setToInitState(); + } + + public synchronized void setToInitState() { + pendingBufferLU1.reset(); + if (scs != null) { + scs.resetSCSFormatDefaults(); + } + if (printPs != null) { + printPs.cancelAutoFlush(); + printPs.erasePrintBuffer(); + } + if (pd != null) { + pd.resetCapture(); + } + log.fine("DS3270P reset to initial state"); + } + + // ========== Accessors & Conversions ========== + + public PrintSCS3270 getPrintSCS() { return scs; } + public void setPrintSCS(PrintSCS3270 scs) { this.scs = scs; } + + public PrintPS3270 getPrintPS() { return printPs; } + public void setPrintPS(PrintPS3270 printPs) { this.printPs = printPs; } + + public PD3270 getPD() { return pd; } + public void setPD(PD3270 pd) { this.pd = pd; } + + public PrinterConfig getConfig() { return config; } + public void setConfig(PrinterConfig config) { this.config = config; } + + public EbcdicTranslator getTranslator() { return translator; } + + public short getActiveLuType() { return activeLuType; } + public void setActiveLuType(short luType) { this.activeLuType = luType; } + + public Telnet3270EP getTelnet() { return telnet; } + public void setTelnet(Telnet3270EP telnet) { this.telnet = telnet; } + + private static byte[] toByteArray(short[] sArray, int offset, int length) { + byte[] b = new byte[length]; + for (int i = 0; i < length; i++) { + b[i] = (byte) (sArray[offset + i] & 0xFF); + } + return b; + } +} 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 8626e3a..3a849d1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270.java @@ -18,26 +18,26 @@ public class PrintPS3270 { private static final Logger log = Logger.getLogger(PrintPS3270.class.getName()); - private final PrinterConfig config; - private final PD3270 pd; - private final EbcdicTranslator translator; + protected final PrinterConfig config; + protected final PD3270 pd; + protected final EbcdicTranslator translator; - private int rows = 24; - private int cols = 80; - private int bufferSize = 24 * 80; - private int bufferAddress = 0; + protected int rows = 24; + protected int cols = 80; + protected int bufferSize = 24 * 80; + protected int bufferAddress = 0; // Buffer planes - private byte[] textPlane; - private byte[] attrPlane; - private byte[] colorPlane; - private byte[] hilitePlane; + protected byte[] textPlane; + protected byte[] attrPlane; + protected byte[] colorPlane; + protected byte[] hilitePlane; - private int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL; + protected int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL; // Auto-Flush Timer (Phase 7) - private java.util.Timer autoFlushTimer; - private final Object timerLock = new Object(); + protected Timer autoFlushTimer; + protected final Object timerLock = new Object(); public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { this.config = config != null ? config : new PrinterConfig(); @@ -279,15 +279,15 @@ public class PrintPS3270 { if (timeout <= 0) return; synchronized (timerLock) { cancelAutoFlush(); - autoFlushTimer = new java.util.Timer("PrintPS3270-AutoFlush", true); - autoFlushTimer.schedule(new java.util.TimerTask() { + autoFlushTimer = new Timer(timeout, new TimerListener() { @Override - public void run() { + public void timerExpired(TimerEvent event) { synchronized (PrintPS3270.this) { flushPrintBuffer(); } } - }, timeout); + }, false, "PrintPS3270-AutoFlush"); + autoFlushTimer.start(); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270DB.java new file mode 100644 index 0000000..6e1cfe3 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintPS3270DB.java @@ -0,0 +1,167 @@ +package haus.nightmare.lib3270j.printer; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * DBCS (Double-Byte Character Set) LU3 3270 Printer Data Stream Engine (PrintPS3270DB). + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintPS3270DB. + * + * Provides support for double-byte character buffering (2-cell width spans) and + * cell-level grid/ruling line borders (Left, Right, Top, Bottom) in LU3 print streams. + */ +public class PrintPS3270DB extends PrintPS3270 { + + private static final Logger log = Logger.getLogger(PrintPS3270DB.class.getName()); + + // Grid flags + public static final int GRID_TOP = 0x01; + public static final int GRID_BOTTOM = 0x02; + public static final int GRID_LEFT = 0x04; + public static final int GRID_RIGHT = 0x08; + + // DBCS cell types + public static final byte DBCS_NONE = 0x00; + public static final byte DBCS_LEFT = 0x01; // Lead byte cell + public static final byte DBCS_RIGHT = 0x02; // Trail byte cell + + protected byte[] dbcsPlane; + protected byte[] gridPlane; + protected char[] dbcsCharPlane; + + public PrintPS3270DB(PrinterConfig config) { + this(config, null, null); + } + + public PrintPS3270DB(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + initDBCSPlanes(); + } + + private void initDBCSPlanes() { + this.dbcsPlane = new byte[bufferSize]; + this.gridPlane = new byte[bufferSize]; + this.dbcsCharPlane = new char[bufferSize]; + eraseDBCSPlanes(); + } + + public synchronized void eraseDBCSPlanes() { + if (dbcsPlane != null) Arrays.fill(dbcsPlane, DBCS_NONE); + if (gridPlane != null) Arrays.fill(gridPlane, (byte) 0); + if (dbcsCharPlane != null) Arrays.fill(dbcsCharPlane, ' '); + } + + @Override + public synchronized void erasePrintBuffer() { + super.erasePrintBuffer(); + eraseDBCSPlanes(); + } + + // ========== DBCS Character & Grid Accessors ========== + + public synchronized void setDBCSChar(int row, int col, char unicodeChar) { + if (row < 0 || row >= rows || col < 0 || col >= cols) return; + int addr = row * cols + col; + setDBCSCharAtAddress(addr, unicodeChar); + } + + public synchronized void setDBCSCharAtAddress(int addr, char unicodeChar) { + if (addr < 0 || addr >= bufferSize) return; + dbcsPlane[addr] = DBCS_LEFT; + dbcsCharPlane[addr] = unicodeChar; + if (addr + 1 < bufferSize) { + dbcsPlane[addr + 1] = DBCS_RIGHT; + dbcsCharPlane[addr + 1] = ' '; + } + } + + public synchronized boolean isDBCSCell(int addr) { + return addr >= 0 && addr < bufferSize && dbcsPlane[addr] != DBCS_NONE; + } + + public synchronized boolean isDBCSLeft(int addr) { + return addr >= 0 && addr < bufferSize && dbcsPlane[addr] == DBCS_LEFT; + } + + public synchronized boolean isDBCSRight(int addr) { + return addr >= 0 && addr < bufferSize && dbcsPlane[addr] == DBCS_RIGHT; + } + + public synchronized void setGridLine(int addr, int gridFlags) { + if (addr >= 0 && addr < bufferSize) { + gridPlane[addr] = (byte) gridFlags; + } + } + + public synchronized int getGridLine(int addr) { + if (addr >= 0 && addr < bufferSize) { + return gridPlane[addr] & 0xFF; + } + return 0; + } + + // ========== Output Formatting with DBCS & Grid Lines ========== + + @Override + public synchronized void printLine(int row, int lineLen) { + if (row < 0 || row >= rows) return; + int startAddr = row * cols; + int effLineLen = Math.min(lineLen, cols); + + // Check if row has grid line attributes + boolean hasRowTopGrid = false; + boolean hasRowBottomGrid = false; + for (int c = 0; c < effLineLen; c++) { + int g = gridPlane[startAddr + c] & 0xFF; + if ((g & GRID_TOP) != 0) hasRowTopGrid = true; + if ((g & GRID_BOTTOM) != 0) hasRowBottomGrid = true; + } + + if (hasRowTopGrid && pd != null) { + pd.writePrintString("┌" + "─".repeat(effLineLen) + "┐\n"); + } + + StringBuilder lineBuilder = new StringBuilder(); + for (int c = 0; c < effLineLen; c++) { + int addr = startAddr + c; + int g = gridPlane[addr] & 0xFF; + + if ((g & GRID_LEFT) != 0) { + lineBuilder.append('│'); + } + + if (dbcsPlane[addr] == DBCS_LEFT) { + char ch = dbcsCharPlane[addr]; + lineBuilder.append(ch != 0 ? ch : ' '); + } else if (dbcsPlane[addr] == DBCS_RIGHT) { + // Already rendered with left half; do not emit separate char + } else { + int ebc = textPlane[addr] & 0xFF; + char ch = (ebc == 0x00) ? ' ' : translator.ebcdicToUnicode(ebc); + lineBuilder.append(ch); + } + + if ((g & GRID_RIGHT) != 0) { + lineBuilder.append('│'); + } + } + + // Trim trailing blanks + String rendered = lineBuilder.toString(); + int end = rendered.length(); + while (end > 0 && rendered.charAt(end - 1) == ' ') { + end--; + } + + if (end > 0) { + pd.writePrintString(rendered.substring(0, end)); + } + pd.writePrintString("\n"); + + if (hasRowBottomGrid && pd != null) { + pd.writePrintString("└" + "─".repeat(effLineLen) + "┘\n"); + } + } +} 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 403eefd..d494764 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270.java @@ -13,36 +13,40 @@ public class PrintSCS3270 { private static final Logger log = Logger.getLogger(PrintSCS3270.class.getName()); - private final PrinterConfig config; - private final PD3270 pd; - private final EbcdicTranslator translator; + protected final PrinterConfig config; + protected final PD3270 pd; + protected final EbcdicTranslator translator; // Formatting Parameters - private int mpp = PrinterConstants.DEFAULT_MPP; // Maximum Presentation Position (Line Length) - private int mpl = PrinterConstants.DEFAULT_MPL; // Maximum Page Length - private int leftMargin = 1; - private int rightMargin = PrinterConstants.DEFAULT_MPP; - private int topMargin = 1; - private int bottomMargin = PrinterConstants.DEFAULT_MPL; - private int cpi = PrinterConstants.DEFAULT_CPI; - private int lpi = PrinterConstants.DEFAULT_LPI; + protected int mpp = PrinterConstants.DEFAULT_MPP; // Maximum Presentation Position (Line Length) + protected int mpl = PrinterConstants.DEFAULT_MPL; // Maximum Page Length + protected int leftMargin = 1; + protected int rightMargin = PrinterConstants.DEFAULT_MPP; + protected int topMargin = 1; + protected int bottomMargin = PrinterConstants.DEFAULT_MPL; + protected int cpi = PrinterConstants.DEFAULT_CPI; + protected int lpi = PrinterConstants.DEFAULT_LPI; // Tab Stops (1-based arrays) - private int[] horizontalTabs = new int[0]; - private int[] verticalTabs = new int[0]; + protected int[] horizontalTabs = new int[0]; + protected int[] verticalTabs = new int[0]; // Current State - private int currentRow = 1; - private int currentCol = 1; - private boolean doubleWidth = false; - private int activeColor = 0; - private int activeHighlight = PrinterConstants.SEAC_DEFAULT; - private int textOrientation = 0; - private boolean presentationEnabled = true; + protected int currentRow = 1; + protected int currentCol = 1; + protected boolean doubleWidth = false; + protected int activeColor = 0; + protected int activeHighlight = PrinterConstants.SEAC_DEFAULT; + protected int textOrientation = 0; + protected boolean presentationEnabled = true; // Line buffering for print composition - private char[] lineBuffer; - private boolean lineModified = false; + protected char[] lineBuffer; + protected boolean lineModified = false; + + // Auto-Flush Timer (Phase 7) + protected Timer autoFlushTimer; + protected final Object timerLock = new Object(); public PrintSCS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { this.config = config != null ? config : new PrinterConfig(); @@ -855,4 +859,34 @@ public class PrintSCS3270 { public PD3270 getPD() { return pd; } public EbcdicTranslator getTranslator() { return translator; } + + public void scheduleAutoFlush() { + long timeout = config.getAutoFlushTimeoutMs(); + if (timeout <= 0) return; + synchronized (timerLock) { + cancelAutoFlush(); + autoFlushTimer = new Timer(timeout, new TimerListener() { + @Override + public void timerExpired(TimerEvent event) { + synchronized (PrintSCS3270.this) { + flushLineBuffer(); + if (pd != null) { + pd.flush(); + } + } + } + }, false, "PrintSCS3270-AutoFlush"); + autoFlushTimer.start(); + } + } + + public void cancelAutoFlush() { + synchronized (timerLock) { + if (autoFlushTimer != null) { + autoFlushTimer.cancel(); + autoFlushTimer = null; + } + } + } } + diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270DB.java new file mode 100644 index 0000000..0325536 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/PrintSCS3270DB.java @@ -0,0 +1,292 @@ +package haus.nightmare.lib3270j.printer; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * DBCS (Double-Byte Character Set) SCS Printer Extension (PrintSCS3270DB). + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintSCS3270DB. + * + * Implements double-byte character decoding (SO 0x0E / SI 0x0F), 2-cell column spacing, + * DBCS pitch scaling (e.g. 5 CPI / 6 CPI), and ruling / grid line support. + */ +public class PrintSCS3270DB extends PrintSCS3270 { + + private static final Logger log = Logger.getLogger(PrintSCS3270DB.class.getName()); + + // Grid / Ruling Line bit flags + public static final int GRID_TOP = 0x01; + public static final int GRID_BOTTOM = 0x02; + public static final int GRID_LEFT = 0x04; + public static final int GRID_RIGHT = 0x08; + + private boolean dbcsMode = false; + private boolean continuousDbcs = false; + private int dbcsCpi = 5; // Default DBCS pitch (5 CPI for 10 CPI base) + + // Active Grid lines + private int activeGridFlags = 0; + private byte[] lineGridFlags; + + public PrintSCS3270DB(PrinterConfig config) { + this(config, null, null); + } + + public PrintSCS3270DB(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + this.dbcsCpi = Math.max(1, this.cpi / 2); + this.lineGridFlags = new byte[Math.max(256, mpp + 2)]; + } + + @Override + public synchronized void resetSCSFormatDefaults() { + super.resetSCSFormatDefaults(); + this.dbcsMode = false; + this.continuousDbcs = false; + this.activeGridFlags = 0; + this.dbcsCpi = Math.max(1, this.cpi / 2); + this.lineGridFlags = new byte[Math.max(256, mpp + 2)]; + } + + // ========== Shift-Out / Shift-In Controls ========== + + @Override + public synchronized void processSO() { + super.processSO(); + this.dbcsMode = true; + log.fine("PrintSCS3270DB entered DBCS mode (SO)"); + } + + @Override + public synchronized void processSI() { + super.processSI(); + this.dbcsMode = false; + log.fine("PrintSCS3270DB exited DBCS mode (SI)"); + } + + public boolean isDBCSMode() { + return dbcsMode || continuousDbcs; + } + + public void setDBCSMode(boolean mode) { + this.dbcsMode = mode; + } + + public boolean isContinuousDBCS() { + return continuousDbcs; + } + + public void setContinuousDBCS(boolean continuous) { + this.continuousDbcs = continuous; + } + + public int getDBCSCPI() { + return dbcsCpi; + } + + public void setDBCSCPI(int cpi) { + this.dbcsCpi = cpi; + } + + // ========== Grid & Ruling Line Controls ========== + + public synchronized void setGridLines(boolean left, boolean right, boolean top, boolean bottom) { + int flags = 0; + if (top) flags |= GRID_TOP; + if (bottom) flags |= GRID_BOTTOM; + if (left) flags |= GRID_LEFT; + if (right) flags |= GRID_RIGHT; + this.activeGridFlags = flags; + } + + public synchronized void setGridFlags(int flags) { + this.activeGridFlags = flags; + } + + public int getGridFlags() { + return activeGridFlags; + } + + public boolean hasLeftGrid() { + return (activeGridFlags & GRID_LEFT) != 0; + } + + public boolean hasRightGrid() { + return (activeGridFlags & GRID_RIGHT) != 0; + } + + public boolean hasTopGrid() { + return (activeGridFlags & GRID_TOP) != 0; + } + + public boolean hasBottomGrid() { + return (activeGridFlags & GRID_BOTTOM) != 0; + } + + public synchronized void clearGridLines() { + this.activeGridFlags = 0; + } + + // ========== Data Stream Decoding with DBCS Support ========== + + @Override + public synchronized void processHostData(byte[] data, int offset, int length) { + if (data == null || length <= 0 || offset < 0 || offset + length > data.length) { + return; + } + + int idx = offset; + int end = offset + length; + + while (idx < end) { + int b1 = data[idx] & 0xFF; + + if (isDBCSMode()) { + // In DBCS mode: check for Shift-In (SI = 0x0F) + if (b1 == PrinterConstants.SCS_SI) { + processSI(); + idx++; + continue; + } + + // Check for single-byte control orders that can interrupt DBCS mode + if (b1 == PrinterConstants.SCS_NL) { + processNL(); + idx++; + continue; + } else if (b1 == PrinterConstants.SCS_CR) { + processCR(); + idx++; + continue; + } else if (b1 == PrinterConstants.SCS_LF) { + processLF(); + idx++; + continue; + } else if (b1 == PrinterConstants.SCS_FF) { + processFF(); + idx++; + continue; + } + + // Need 2 bytes for DBCS character + if (idx + 1 < end) { + int b2 = data[idx + 1] & 0xFF; + + // If b2 is SI (0x0F), b1 might be stray or padding + if (b2 == PrinterConstants.SCS_SI) { + processSI(); + idx += 2; + continue; + } + + processDBCSCharacter(b1, b2); + idx += 2; + } else { + // Incomplete trailing byte + idx++; + } + } else { + // In SBCS mode: check for Shift-Out (SO = 0x0E) + if (b1 == PrinterConstants.SCS_SO) { + processSO(); + idx++; + continue; + } + + // Process through single-byte SCS engine + super.processHostData(data, idx, 1); + idx++; + } + } + } + + /** + * Decode and buffer a double-byte character pair. + */ + protected synchronized void processDBCSCharacter(int b1, int b2) { + if (!presentationEnabled) return; + + char unicodeChar; + // Check for DBCS ideographic blank (0x4040 in EBCDIC) + if (b1 == 0x40 && b2 == 0x40) { + unicodeChar = '\u3000'; // Full-width ideographic space + } else { + unicodeChar = translator.dbcsToUnicode(b1, b2); + if (unicodeChar == '\0' || unicodeChar == 0xFFFF) { + unicodeChar = '?'; + } + } + + ensureLineBufferCapacity(currentCol + 2); + + // Record grid line attributes for this cell + if (lineGridFlags != null && currentCol < lineGridFlags.length) { + lineGridFlags[currentCol] = (byte) activeGridFlags; + } + + lineBuffer[currentCol] = unicodeChar; + lineModified = true; + + // Advance column by 2 (DBCS glyph takes 2 character columns) + currentCol += 2; + + if (currentCol > rightMargin) { + newLine(); + } + } + + private void ensureLineBufferCapacity(int reqCols) { + if (lineBuffer == null || reqCols >= lineBuffer.length) { + int newSize = Math.max(reqCols + 64, lineBuffer != null ? lineBuffer.length * 2 : 256); + char[] newBuf = new char[newSize]; + Arrays.fill(newBuf, ' '); + if (lineBuffer != null) { + System.arraycopy(lineBuffer, 0, newBuf, 0, lineBuffer.length); + } + lineBuffer = newBuf; + + byte[] newGrid = new byte[newSize]; + if (lineGridFlags != null) { + System.arraycopy(lineGridFlags, 0, newGrid, 0, lineGridFlags.length); + } + lineGridFlags = newGrid; + } + } + + @Override + public synchronized void flushLineBuffer() { + if (!lineModified && activeGridFlags == 0) { + return; + } + + // Render top grid border if active + if (hasTopGrid() && pd != null) { + pd.writePrintString("┌" + "─".repeat(Math.max(1, rightMargin - leftMargin)) + "┐\n"); + } + + // Check if any cells on this line have vertical grid flags + boolean lineHasLeftGrid = hasLeftGrid(); + boolean lineHasRightGrid = hasRightGrid(); + + if (lineHasLeftGrid && pd != null) { + pd.writePrintString("│"); + } + + super.flushLineBuffer(); + + if (lineHasRightGrid && pd != null) { + pd.writePrintString("│\n"); + } + + // Render bottom grid border if active + if (hasBottomGrid() && pd != null) { + pd.writePrintString("└" + "─".repeat(Math.max(1, rightMargin - leftMargin)) + "┘\n"); + } + + if (lineGridFlags != null) { + Arrays.fill(lineGridFlags, (byte) 0); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java index c8afbd1..2b92dd9 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Telnet3270EP.java @@ -33,6 +33,7 @@ public class Telnet3270EP implements Runnable { private final EbcdicTranslator translator; private final PrintSCS3270 scs; private final PrintPS3270 printPs; + private final DS3270P ds3270p; private final List listeners = new CopyOnWriteArrayList<>(); @@ -67,6 +68,7 @@ public class Telnet3270EP implements Runnable { this.translator = new EbcdicTranslator(this.config.getCodePage()); this.scs = new PrintSCS3270(this.config, this.pd, this.translator); this.printPs = new PrintPS3270(this.config, this.pd, this.translator); + this.ds3270p = new DS3270P(this, this.config, this.pd, this.scs, this.printPs, this.translator); } public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { @@ -75,6 +77,7 @@ public class Telnet3270EP implements Runnable { this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage()); this.scs = new PrintSCS3270(this.config, this.pd, this.translator); this.printPs = new PrintPS3270(this.config, this.pd, this.translator); + this.ds3270p = new DS3270P(this, this.config, this.pd, this.scs, this.printPs, this.translator); } // ========== Connection Lifecycle (Fn #1) ========== @@ -225,6 +228,9 @@ public class Telnet3270EP implements Runnable { */ public synchronized void process_bind(short bindType) { this.activeLuType = bindType; + if (ds3270p != null) { + ds3270p.setActiveLuType(bindType); + } log.info("Printer session BIND accepted, LU type = " + (activeLuType == PrinterConstants.LU_TYPE_1_SCS ? "LU-1 (SCS)" : "LU-3 (3270 DS)")); @@ -631,7 +637,7 @@ public class Telnet3270EP implements Runnable { switch (dataType) { case TN3270EConstants.DT_SCS_DATA: updateStatus(PrinterConstants.STATUS_PRINTING, "Processing SCS print stream"); - scs.processHostData(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE); + ds3270p.receiveDataLU1(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE); if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) { sendTN3270EPositiveResponse(seqNum); } @@ -639,7 +645,7 @@ public class Telnet3270EP implements Runnable { case TN3270EConstants.DT_3270_DATA: updateStatus(PrinterConstants.STATUS_PRINTING, "Processing 3270 printer data stream"); - printPs.process3270PrintDS(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE); + ds3270p.receiveDataLU3(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE); if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) { sendTN3270EPositiveResponse(seqNum); } @@ -671,21 +677,13 @@ public class Telnet3270EP implements Runnable { default: // Fallback to active LU type - if (activeLuType == PrinterConstants.LU_TYPE_3_DS) { - printPs.process3270PrintDS(record, 0, record.length); - } else { - scs.processHostData(record, 0, record.length); - } + ds3270p.receiveData(record, 0, record.length); break; } } else { // Non-TN3270E mode firePrintJobData(record, 0, record.length); - if (activeLuType == PrinterConstants.LU_TYPE_3_DS) { - printPs.process3270PrintDS(record, 0, record.length); - } else { - scs.processHostData(record, 0, record.length); - } + ds3270p.receiveData(record, 0, record.length); } } @@ -772,4 +770,6 @@ public class Telnet3270EP implements Runnable { public EbcdicTranslator getTranslator() { return translator; } public PrintSCS3270 getSCS() { return scs; } public PrintPS3270 getPrintPS() { return printPs; } + public DS3270P getDS3270P() { return ds3270p; } + public DS3270P getDS() { return ds3270p; } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Timer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Timer.java new file mode 100644 index 0000000..16caefa --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/Timer.java @@ -0,0 +1,144 @@ +package haus.nightmare.lib3270j.printer; + +import java.util.List; +import java.util.TimerTask; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Timer implementation for printer session timeouts and auto-flush mechanisms. + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.Timer. + */ +public class Timer { + + private static final Logger log = Logger.getLogger(Timer.class.getName()); + + private final List listeners = new CopyOnWriteArrayList<>(); + private final String timerId; + private long intervalMs; + private final boolean repeating; + private final AtomicBoolean running = new AtomicBoolean(false); + + private java.util.Timer internalTimer; + private final Object lock = new Object(); + + public Timer() { + this(2000, null, false, "PrinterTimer"); + } + + public Timer(long intervalMs) { + this(intervalMs, null, false, "PrinterTimer"); + } + + public Timer(long intervalMs, TimerListener listener) { + this(intervalMs, listener, false, "PrinterTimer"); + } + + public Timer(long intervalMs, TimerListener listener, boolean repeating) { + this(intervalMs, listener, repeating, "PrinterTimer"); + } + + public Timer(long intervalMs, TimerListener listener, boolean repeating, String timerId) { + this.intervalMs = intervalMs; + this.repeating = repeating; + this.timerId = timerId != null ? timerId : "PrinterTimer"; + if (listener != null) { + this.listeners.add(listener); + } + } + + public void addTimerListener(TimerListener listener) { + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + } + } + + public void removeTimerListener(TimerListener listener) { + listeners.remove(listener); + } + + public synchronized void start() { + synchronized (lock) { + stop(); + if (intervalMs <= 0) { + return; + } + running.set(true); + internalTimer = new java.util.Timer(timerId + "-Worker", true); + TimerTask task = new TimerTask() { + @Override + public void run() { + if (!repeating) { + running.set(false); + } + fireTimerExpired(); + } + }; + if (repeating) { + internalTimer.scheduleAtFixedRate(task, intervalMs, intervalMs); + } else { + internalTimer.schedule(task, intervalMs); + } + } + } + + public synchronized void stop() { + synchronized (lock) { + running.set(false); + if (internalTimer != null) { + internalTimer.cancel(); + internalTimer.purge(); + internalTimer = null; + } + } + } + + public synchronized void cancel() { + stop(); + } + + public synchronized void restart() { + stop(); + start(); + } + + public synchronized void reset() { + restart(); + } + + public boolean isRunning() { + return running.get(); + } + + public long getInterval() { + return intervalMs; + } + + public synchronized void setInterval(long intervalMs) { + this.intervalMs = intervalMs; + if (running.get()) { + restart(); + } + } + + public String getTimerId() { + return timerId; + } + + public boolean isRepeating() { + return repeating; + } + + public void fireTimerExpired() { + TimerEvent event = new TimerEvent(this, timerId, System.currentTimeMillis()); + for (TimerListener listener : listeners) { + try { + listener.timerExpired(event); + } catch (Exception e) { + log.log(Level.WARNING, "Error dispatching TimerEvent to " + listener, e); + } + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/TimerEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/TimerEvent.java new file mode 100644 index 0000000..dab5981 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/TimerEvent.java @@ -0,0 +1,46 @@ +package haus.nightmare.lib3270j.printer; + +import java.util.EventObject; + +/** + * Event delivered when a printer timer expires. + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.TimerEvent. + */ +public class TimerEvent extends EventObject { + + private static final long serialVersionUID = 1L; + + private final String timerId; + private final long timestamp; + + public TimerEvent(Object source) { + this(source, "PrinterTimer", System.currentTimeMillis()); + } + + public TimerEvent(Object source, String timerId) { + this(source, timerId, System.currentTimeMillis()); + } + + public TimerEvent(Object source, String timerId, long timestamp) { + super(source); + this.timerId = timerId != null ? timerId : "PrinterTimer"; + this.timestamp = timestamp; + } + + public Object getTimer() { + return getSource(); + } + + public String getTimerId() { + return timerId; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return "TimerEvent[timerId=" + timerId + ", timestamp=" + timestamp + ", source=" + source + "]"; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/TimerListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/TimerListener.java new file mode 100644 index 0000000..d41de18 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/printer/TimerListener.java @@ -0,0 +1,16 @@ +package haus.nightmare.lib3270j.printer; + +import java.util.EventListener; + +/** + * Listener interface for printer timeout and flush timer events. + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.TimerListener. + */ +public interface TimerListener extends EventListener { + + /** + * Invoked when a scheduled printer timer expires. + * @param event TimerEvent containing event metadata + */ + void timerExpired(TimerEvent event); +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java index 7caa2fd..1bbf892 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java @@ -18,6 +18,30 @@ public class ExtendedAttribute { /** Background color (0x00 for default, or 0xf0-0xff for explicit). */ public byte bg; + // Base 4-Color constants per IBM Host On-Demand ColorRemapModel3270 (baseCategoryAttributes) + public static final byte COLOR_BASE_NORMAL_UNPROTECT = (byte) 0xF4; // Green (bNU) + public static final byte COLOR_BASE_INTENSIFY_UNPROTECT = (byte) 0xF2; // Red (bIU) + public static final byte COLOR_BASE_NORMAL_PROTECT = (byte) 0xF5; // Turquoise / Cyan (bNP) + public static final byte COLOR_BASE_INTENSIFY_PROTECT = (byte) 0xF7; // White (bIP) + + /** + * Computes the standard base 4-color for a given 3270 Field Attribute byte. + * Conforms 1:1 to IBM Host On-Demand ColorRemapModel3270 defaults: + * - Normal Unprotected: Green (0xF4) + * - Intensified Unprotected: Red (0xF2) + * - Normal Protected: Turquoise (0xF5) + * - Intensified Protected: White (0xF7) + */ + public static byte getBase3270Color(int fa) { + boolean isProt = (fa & haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT) != 0; + boolean isHigh = (fa & haus.nightmare.lib3270j.protocol.DS3270Constants.FA_INTENSITY) == haus.nightmare.lib3270j.protocol.DS3270Constants.FA_INT_HIGH_SEL; + if (isProt) { + return isHigh ? COLOR_BASE_INTENSIFY_PROTECT : COLOR_BASE_NORMAL_PROTECT; + } else { + return isHigh ? COLOR_BASE_INTENSIFY_UNPROTECT : COLOR_BASE_NORMAL_UNPROTECT; + } + } + // Character set constants public static final byte CS_BASE = 0; public static final byte CS_APL = 1; @@ -32,6 +56,12 @@ public class ExtendedAttribute { public static final byte DB_SI = 3; // Shift-In control char public static final byte DB_SO = 4; // Shift-Out control char + // Graphics rendition bits (DS3270Constants compatible) + public static final byte GR_BLINK = 0x01; + public static final byte GR_REVERSE = 0x02; + public static final byte GR_UNDERLINE = 0x04; + public static final byte GR_INTENSIFY = 0x08; + /** * Graphics rendition bits. * GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08 diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java index 0bdc903..843fc12 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java @@ -18,6 +18,13 @@ public class ScreenBuffer { private ExtendedAttribute[] altBuffer; // Alternate screen buffer private final ExtendedAttribute defaultFA; // Default field attribute (ea_buf[-1]) + // ECL Plane type constants + public static final int PLANE_TEXT = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_TEXT; + public static final int PLANE_COLOR = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_COLOR; + public static final int PLANE_HILITE = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_HILITE; + public static final int PLANE_EXTENDED = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_EXTENDED; + public static final int PLANE_FIELD = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_FIELD; + private int maxRows, maxCols; // Maximum (alternate) dimensions private int defRows, defCols; // Default dimensions (24x80) private int altRows, altCols; // Alternate dimensions @@ -61,16 +68,24 @@ public class ScreenBuffer { return translator; } + public ScreenBuffer() { + this(TerminalModel.IBM_3279_2, new EbcdicTranslator()); + } + public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) { + this(MODEL_2_ROWS, MODEL_2_COLS, model.getAlternateRows(), model.getAlternateCols(), translator); + } + + public ScreenBuffer(int defRows, int defCols, int altRows, int altCols, EbcdicTranslator translator) { this.translator = translator; - this.defRows = MODEL_2_ROWS; - this.defCols = MODEL_2_COLS; - this.altRows = model.getAlternateRows(); - this.altCols = model.getAlternateCols(); - this.maxRows = altRows; - this.maxCols = altCols; - this.rows = defRows; - this.cols = defCols; + this.defRows = Math.max(1, defRows); + this.defCols = Math.max(1, defCols); + this.altRows = Math.max(1, altRows); + this.altCols = Math.max(1, altCols); + this.maxRows = Math.max(this.defRows, this.altRows); + this.maxCols = Math.max(this.defCols, this.altCols); + this.rows = this.defRows; + this.cols = this.defCols; // Default field attribute (like ea_buf[-1]) defaultFA = new ExtendedAttribute(); @@ -83,14 +98,26 @@ public class ScreenBuffer { private void allocateBuffers() { int size = maxRows * maxCols; + ExtendedAttribute[] oldBuf = buffer; + ExtendedAttribute[] oldAlt = altBuffer; buffer = new ExtendedAttribute[size]; altBuffer = new ExtendedAttribute[size]; for (int i = 0; i < size; i++) { buffer[i] = new ExtendedAttribute(); altBuffer[i] = new ExtendedAttribute(); } - cursorAddress = 0; - bufferAddress = 0; + if (oldBuf != null) { + int copyLen = Math.min(oldBuf.length, size); + for (int i = 0; i < copyLen; i++) { + buffer[i].copyFrom(oldBuf[i]); + } + } + if (oldAlt != null) { + int copyLen = Math.min(oldAlt.length, size); + for (int i = 0; i < copyLen; i++) { + altBuffer[i].copyFrom(oldAlt[i]); + } + } } /** Get the current screen buffer. */ @@ -159,6 +186,7 @@ public class ScreenBuffer { /** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */ public synchronized void setAlternateDimensions(int newAltRows, int newAltCols) { + if (newAltRows <= 0 || newAltCols <= 0) return; if (newAltRows == altRows && newAltCols == altCols) return; this.altRows = newAltRows; this.altCols = newAltCols; @@ -168,6 +196,10 @@ public class ScreenBuffer { this.maxCols = Math.max(maxCols, newAltCols); allocateBuffers(); } + if (screenAlt) { + this.rows = this.altRows; + this.cols = this.altCols; + } updateDisplaySnapshot(); } @@ -382,6 +414,108 @@ public class ScreenBuffer { return buffer[fa_addr].fa; } + /** + * Returns the effective foreground color byte for the given buffer address. + * If the cell has an explicit foreground color attribute (0xF0-0xFF), it is returned. + * Otherwise, if the field attribute has an explicit color, it is returned. + * Otherwise, returns the standard base 4-color calculated from the governing field attribute. + */ + public synchronized byte getEffectiveForegroundColor(int baddr) { + int size = rows * cols; + if (size <= 0) return 0; + baddr = ((baddr % size) + size) % size; + ExtendedAttribute ea = buffer[baddr]; + if (ea.fg != 0) { + return ea.fg; + } + int faAddr = findFieldAttribute(baddr); + if (faAddr >= 0) { + ExtendedAttribute faEa = buffer[faAddr]; + if (faEa.fg != 0) { + return faEa.fg; + } + return ExtendedAttribute.getBase3270Color(faEa.fa & 0xFF); + } + return ExtendedAttribute.COLOR_BASE_NORMAL_UNPROTECT; + } + + /** + * Returns the effective background color byte for the given buffer address. + * If the cell or field has an explicit background attribute, it is returned; + * otherwise 0x00 (default / transparent) is returned. + */ + public synchronized byte getEffectiveBackgroundColor(int baddr) { + int size = rows * cols; + if (size <= 0) return 0; + baddr = ((baddr % size) + size) % size; + ExtendedAttribute ea = buffer[baddr]; + if (ea.bg != 0) return ea.bg; + int faAddr = findFieldAttribute(baddr); + if (faAddr >= 0 && buffer[faAddr].bg != 0) { + return buffer[faAddr].bg; + } + return 0x00; + } + + /** + * Returns the effective highlighting rendition byte (GR_BLINK, GR_REVERSE, etc.) + * for the given buffer address. + */ + public synchronized byte getEffectiveHighlighting(int baddr) { + int size = rows * cols; + if (size <= 0) return 0; + baddr = ((baddr % size) + size) % size; + ExtendedAttribute ea = buffer[baddr]; + if (ea.gr != 0) return ea.gr; + int faAddr = findFieldAttribute(baddr); + if (faAddr >= 0) { + return buffer[faAddr].gr; + } + return 0; + } + + /** + * Copy a specific plane (Character, Color, Highlighting, Field, etc.) into destBuffer. + */ + public synchronized int copyPlanes(int planeType, char[] destBuffer, int start, int length) { + int size = rows * cols; + if (size <= 0 || destBuffer == null || length <= 0) return 0; + int copyLen = Math.min(length, destBuffer.length); + for (int i = 0; i < copyLen; i++) { + int addr = ((start + i) % size + size) % size; + ExtendedAttribute ea = buffer[addr]; + switch (planeType) { + case haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_TEXT: + if (ea.isFieldAttribute()) { + destBuffer[i] = ' '; + } else if (ea.ucs4 != 0) { + destBuffer[i] = (char) ea.ucs4; + } else if (ea.ec != 0 && translator != null) { + destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF); + } else { + destBuffer[i] = ' '; + } + break; + case haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_COLOR: + destBuffer[i] = (char) (getEffectiveForegroundColor(addr) & 0xFF); + break; + case haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_HILITE: + destBuffer[i] = (char) (getEffectiveHighlighting(addr) & 0xFF); + break; + case haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_EXTENDED: + destBuffer[i] = (char) (ea.cs & 0xFF); + break; + case haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_FIELD: + destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0; + break; + default: + destBuffer[i] = ' '; + break; + } + } + return copyLen; + } + /** * Find the next unprotected field after the given address. * Returns 0 if none found. @@ -455,6 +589,10 @@ public class ScreenBuffer { // ========== Setters for model reconfiguration ========== + public synchronized void setDimensions(int rows, int cols) { + setDimensions(rows, cols, rows, cols, rows, cols); + } + public synchronized void setDimensions(int maxRows, int maxCols, int defRows, int defCols, int altRows, int altCols) { this.maxRows = maxRows; @@ -1157,4 +1295,48 @@ public class ScreenBuffer { cleanAdjacentSISO(0); processSOSI(); } + + // ========== Phase 3: HoD Special EAB and DBCS Character Input ========== + + /** + * Sets the special Extended Attribute Buffer (EAB) / character set byte at pos. + * Maps 1:1 to IBM Host On-Demand PS3270.SetSpecialEAB. + */ + public synchronized void setSpecialEAB(int pos, byte val) { + int size = rows * cols; + if (size <= 0) return; + pos = ((pos % size) + size) % size; + ExtendedAttribute ea = buffer[pos]; + ea.ic = val; + ea.cs = val; + screenChanged = true; + } + + /** + * Retrieves the special Extended Attribute Buffer byte at pos. + */ + public synchronized byte getSpecialEAB(int pos) { + int size = rows * cols; + if (size <= 0) return 0; + pos = ((pos % size) + size) % size; + ExtendedAttribute ea = buffer[pos]; + return ea.ic != 0 ? ea.ic : ea.cs; + } + + /** + * Inserts a DBCS or SBCS character at pos, respecting protected field boundaries + * and double-byte alignment. Returns the character cell width (1 or 2), or 0 on failure. + */ + public synchronized int DBCSinputChar(char c, int pos) { + int size = rows * cols; + if (size <= 0) return 0; + pos = ((pos % size) + size) % size; + if (formatted) { + byte faVal = getFieldAttributeAt(pos); + if (faIsProtected(faVal & 0xFF)) return 0; + } + boolean ok = insertChar(pos, c); + if (!ok) return 0; + return (translator != null && translator.isDBCS() && translator.unicodeToDbcs(c) >= 0) ? 2 : 1; + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/security/ssl/HODSSLECLSessionImpl.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/security/ssl/HODSSLECLSessionImpl.java new file mode 100644 index 0000000..2a8e560 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/security/ssl/HODSSLECLSessionImpl.java @@ -0,0 +1,721 @@ +package haus.nightmare.lib3270j.security.ssl; + +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.ecl.ECLConnection; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.tls.TlsCertificateVerifier; +import haus.nightmare.lib3270j.tls.TlsTrustManager; + +import javax.net.ssl.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.net.URL; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * IBM Host On-Demand (HoD) drop-in compatibility class for HODSSLECLSessionImpl. + * Wraps standard Java JSSE SSLContext, KeyManager, TrustManager, and SSLSocketFactory + * providing full TLS 1.2 / 1.3 protocol and cipher negotiation, client certificate + * keystores (.p12 / JKS), and CustomizedCAs truststores. + */ +public class HODSSLECLSessionImpl { + + private static final Logger log = Logger.getLogger(HODSSLECLSessionImpl.class.getName()); + + // HoD SSL Session Property Keys + public static final String SESSION_SSL = "SESSION_SSL"; + public static final String SESSION_SSL_USE_JSSE = "useJSSE"; + public static final String SESSION_CERT_NAME = "certificateName"; + public static final String SESSION_CERT_URL = "certificateURL"; + public static final String SESSION_CERT_PASSWORD = "certificatePassword"; + public static final String SESSION_TRUSTSTORE = "jsseTrustStore"; + public static final String SESSION_TRUSTSTORE_PASSWORD = "jsseTrustStorePassword"; + public static final String SESSION_TRUSTSTORE_TYPE = "jsseTrustStoreType"; + public static final String SESSION_TLS_VERSION = "tlsProtocolVersion"; + + private Properties properties = new Properties(); + private ConnectionConfig config; + private ECLSession session; + private ECLConnection connection; + + private boolean useJSSE = true; + private boolean tlsVerifyCert = true; + private TlsCertificateVerifier certificateVerifier; + + // Client Keystore configuration + private String keyStorePath; + private String keyStorePassword; + private String keyStoreType = "PKCS12"; + private String certificateAlias; + private KeyStore keyStore; + private KeyManager[] keyManagers; + + // Truststore configuration + private String trustStorePath; + private String trustStorePassword; + private String trustStoreType; + private KeyStore trustStore; + private TrustManager[] trustManagers; + private ClassLoader customizedCAsClassLoader; + + // Protocols and Ciphers + private String securityProtocol = "TLS"; + private String tlsProtocolVersion = "TLSv1.2"; + private final List enabledProtocols = new ArrayList<>(Arrays.asList("TLSv1.3", "TLSv1.2")); + private final List enabledCipherSuites = new ArrayList<>(); + + // Runtime SSL state + private SSLContext sslContext; + private SSLSocketFactory sslSocketFactory; + private SSLSession sslSession; + private Socket activeSocket; + + // ========================================================================= + // Constructors + // ========================================================================= + + public HODSSLECLSessionImpl() { + this.config = new ConnectionConfig(); + } + + public HODSSLECLSessionImpl(ConnectionConfig config) { + this.config = (config != null) ? config : new ConnectionConfig(); + syncFromConfig(); + } + + public HODSSLECLSessionImpl(ECLSession session) { + this(); + setSession(session); + } + + public HODSSLECLSessionImpl(ECLConnection connection) { + this(); + setConnection(connection); + } + + public HODSSLECLSessionImpl(Properties props) { + this(); + setProperties(props); + } + + // ========================================================================= + // Properties & Session / Connection Association + // ========================================================================= + + public Properties getProperties() { return properties; } + public Properties GetProperties() { return getProperties(); } + + public void setProperties(Properties props) { + if (props != null) { + this.properties.putAll(props); + applyProperties(props); + } + } + public void SetProperties(Properties props) { setProperties(props); } + + public String getProperty(String key) { return properties.getProperty(key); } + public String GetProperty(String key) { return getProperty(key); } + + public String getProperty(String key, String defVal) { return properties.getProperty(key, defVal); } + public String GetProperty(String key, String defVal) { return getProperty(key, defVal); } + + public void setProperty(String key, String value) { + if (key != null) { + if (value != null) { + properties.setProperty(key, value); + Properties p = new Properties(); + p.setProperty(key, value); + applyProperties(p); + } else { + properties.remove(key); + } + } + } + public void SetProperty(String key, String value) { setProperty(key, value); } + + public ECLSession getSession() { return session; } + public ECLSession GetSession() { return getSession(); } + + public void setSession(ECLSession session) { + this.session = session; + if (session != null) { + if (session.getProperties() != null) { + applyProperties(session.getProperties()); + } + if (session.GetCustomizedCAsClassLoader() != null) { + setCustomizedCAsClassLoader(session.GetCustomizedCAsClassLoader()); + } + if (session.getConnection() != null) { + setConnection(session.getConnection()); + } + } + } + public void SetSession(ECLSession session) { setSession(session); } + + public ECLConnection getConnection() { return connection; } + public ECLConnection GetConnection() { return getConnection(); } + + public void setConnection(ECLConnection connection) { + this.connection = connection; + if (connection != null) { + if (connection.getCertificateName() != null && !connection.getCertificateName().isEmpty()) { + setCertificateAlias(connection.getCertificateName()); + } + if (connection.getCertificateURL() != null && !connection.getCertificateURL().isEmpty()) { + setKeyStorePath(connection.getCertificateURL()); + } + if (connection.getCertificatePassword() != null && !connection.getCertificatePassword().isEmpty()) { + setKeyStorePassword(connection.getCertificatePassword()); + } + if (connection.getJSSETrustStore() != null && !connection.getJSSETrustStore().isEmpty()) { + setTrustStorePath(connection.getJSSETrustStore()); + } + if (connection.getJSSETrustStoreType() != null && !connection.getJSSETrustStoreType().isEmpty()) { + setTrustStoreType(connection.getJSSETrustStoreType()); + } + if (connection.getJSSETrustStorePassword() != null && !connection.getJSSETrustStorePassword().isEmpty()) { + setTrustStorePassword(connection.getJSSETrustStorePassword()); + } + if (connection.getTLSProtocolVersion() != null && !connection.getTLSProtocolVersion().isEmpty()) { + setTLSProtocolVersion(connection.getTLSProtocolVersion()); + } + this.useJSSE = connection.isUseJSSE(); + } + } + public void SetConnection(ECLConnection connection) { setConnection(connection); } + + public ConnectionConfig getConfig() { return config; } + public ConnectionConfig GetConfig() { return getConfig(); } + + public void setConfig(ConnectionConfig config) { + this.config = (config != null) ? config : new ConnectionConfig(); + syncFromConfig(); + } + public void SetConfig(ConnectionConfig config) { setConfig(config); } + + private void syncFromConfig() { + if (config == null) return; + this.tlsVerifyCert = config.isTlsVerifyCert(); + this.certificateVerifier = config.getCertificateVerifier(); + if (config.getSslProtocol() != null) this.securityProtocol = config.getSslProtocol(); + if (config.getKeyStorePath() != null) this.keyStorePath = config.getKeyStorePath(); + if (config.getKeyStorePassword() != null) this.keyStorePassword = config.getKeyStorePassword(); + if (config.getKeyStoreType() != null) this.keyStoreType = config.getKeyStoreType(); + if (config.getKeyStoreAlias() != null) this.certificateAlias = config.getKeyStoreAlias(); + if (config.getTrustStorePath() != null) this.trustStorePath = config.getTrustStorePath(); + if (config.getTrustStorePassword() != null) this.trustStorePassword = config.getTrustStorePassword(); + if (config.getTrustStoreType() != null) this.trustStoreType = config.getTrustStoreType(); + if (config.getCustomizedCAsClassLoader() != null) this.customizedCAsClassLoader = config.getCustomizedCAsClassLoader(); + if (config.getEnabledProtocols() != null && !config.getEnabledProtocols().isEmpty()) { + this.enabledProtocols.clear(); + this.enabledProtocols.addAll(config.getEnabledProtocols()); + } + if (config.getEnabledCipherSuites() != null && !config.getEnabledCipherSuites().isEmpty()) { + this.enabledCipherSuites.clear(); + this.enabledCipherSuites.addAll(config.getEnabledCipherSuites()); + } + } + + private void applyProperties(Properties props) { + if (props == null) return; + String jsse = props.getProperty(SESSION_SSL_USE_JSSE); + if (jsse != null) this.useJSSE = "true".equalsIgnoreCase(jsse) || "1".equals(jsse); + + String certUrl = props.getProperty(SESSION_CERT_URL); + if (certUrl != null && !certUrl.trim().isEmpty()) setKeyStorePath(certUrl.trim()); + + String certPwd = props.getProperty(SESSION_CERT_PASSWORD); + if (certPwd != null) setKeyStorePassword(certPwd); + + String certName = props.getProperty(SESSION_CERT_NAME); + if (certName != null && !certName.trim().isEmpty()) setCertificateAlias(certName.trim()); + + String ts = props.getProperty(SESSION_TRUSTSTORE); + if (ts != null && !ts.trim().isEmpty()) setTrustStorePath(ts.trim()); + + String tsPwd = props.getProperty(SESSION_TRUSTSTORE_PASSWORD); + if (tsPwd != null) setTrustStorePassword(tsPwd); + + String tsType = props.getProperty(SESSION_TRUSTSTORE_TYPE); + if (tsType != null && !tsType.trim().isEmpty()) setTrustStoreType(tsType.trim()); + + String tlsVer = props.getProperty(SESSION_TLS_VERSION); + if (tlsVer != null && !tlsVer.trim().isEmpty()) setTLSProtocolVersion(tlsVer.trim()); + } + + // ========================================================================= + // Client Keystore & Certificate Methods + // ========================================================================= + + public String getKeyStorePath() { return keyStorePath; } + public String GetKeyStorePath() { return getKeyStorePath(); } + public void setKeyStorePath(String path) { + this.keyStorePath = path; + if (config != null) config.setKeyStorePath(path); + } + public void SetKeyStorePath(String path) { setKeyStorePath(path); } + + public String getKeyStorePassword() { return keyStorePassword; } + public String GetKeyStorePassword() { return getKeyStorePassword(); } + public void setKeyStorePassword(String pwd) { + this.keyStorePassword = pwd; + if (config != null) config.setKeyStorePassword(pwd); + } + public void setKeyStorePassword(char[] pwd) { + setKeyStorePassword(pwd != null ? new String(pwd) : null); + } + public void SetKeyStorePassword(String pwd) { setKeyStorePassword(pwd); } + public void SetKeyStorePassword(char[] pwd) { setKeyStorePassword(pwd); } + + public String getKeyStoreType() { return keyStoreType; } + public String GetKeyStoreType() { return getKeyStoreType(); } + public void setKeyStoreType(String type) { + this.keyStoreType = type; + if (config != null) config.setKeyStoreType(type); + } + public void SetKeyStoreType(String type) { setKeyStoreType(type); } + + public KeyStore getKeyStore() { return keyStore; } + public KeyStore GetKeyStore() { return getKeyStore(); } + public void setKeyStore(KeyStore ks) { this.keyStore = ks; } + public void SetKeyStore(KeyStore ks) { setKeyStore(ks); } + + public String getCertificateAlias() { return certificateAlias; } + public String GetCertificateAlias() { return getCertificateAlias(); } + public void setCertificateAlias(String alias) { + this.certificateAlias = alias; + if (config != null) config.setKeyStoreAlias(alias); + } + public void SetCertificateAlias(String alias) { setCertificateAlias(alias); } + + public String getCertificateName() { return getCertificateAlias(); } + public String GetCertificateName() { return getCertificateAlias(); } + public void setCertificateName(String name) { setCertificateAlias(name); } + public void SetCertificateName(String name) { setCertificateAlias(name); } + + public KeyStore loadKeyStore(InputStream in, char[] password, String type) throws Exception { + this.keyStore = TlsTrustManager.loadKeyStore(in, password, type); + return this.keyStore; + } + + public KeyStore loadKeyStore(String path, char[] password, String type) throws Exception { + this.keyStore = TlsTrustManager.loadKeyStore(path, password, type); + this.keyStorePath = path; + return this.keyStore; + } + + public KeyStore loadKeyStore(URL url, char[] password, String type) throws Exception { + if (url == null) throw new IllegalArgumentException("URL cannot be null"); + try (InputStream in = url.openStream()) { + return loadKeyStore(in, password, type); + } + } + + public KeyManager[] createKeyManagers() throws Exception { + if (this.keyStore == null && this.keyStorePath != null) { + char[] pwd = keyStorePassword != null ? keyStorePassword.toCharArray() : null; + this.keyStore = loadKeyStore(this.keyStorePath, pwd, this.keyStoreType); + } + if (this.keyStore == null) return null; + char[] pwd = keyStorePassword != null ? keyStorePassword.toCharArray() : null; + this.keyManagers = TlsTrustManager.createKeyManagers(this.keyStore, pwd, this.certificateAlias); + return this.keyManagers; + } + public KeyManager[] CreateKeyManagers() throws Exception { return createKeyManagers(); } + + public KeyManager[] getKeyManagers() { return keyManagers; } + public KeyManager[] GetKeyManagers() { return getKeyManagers(); } + public void setKeyManagers(KeyManager[] kms) { this.keyManagers = kms; } + public void SetKeyManagers(KeyManager[] kms) { setKeyManagers(kms); } + + // ========================================================================= + // Truststore & CustomizedCAs Methods + // ========================================================================= + + public String getTrustStorePath() { return trustStorePath; } + public String GetTrustStorePath() { return getTrustStorePath(); } + public void setTrustStorePath(String path) { + this.trustStorePath = path; + if (config != null) config.setTrustStorePath(path); + } + public void SetTrustStorePath(String path) { setTrustStorePath(path); } + + public String getTrustStorePassword() { return trustStorePassword; } + public String GetTrustStorePassword() { return getTrustStorePassword(); } + public void setTrustStorePassword(String pwd) { + this.trustStorePassword = pwd; + if (config != null) config.setTrustStorePassword(pwd); + } + public void setTrustStorePassword(char[] pwd) { + setTrustStorePassword(pwd != null ? new String(pwd) : null); + } + public void SetTrustStorePassword(String pwd) { setTrustStorePassword(pwd); } + public void SetTrustStorePassword(char[] pwd) { setTrustStorePassword(pwd); } + + public String getTrustStoreType() { return trustStoreType; } + public String GetTrustStoreType() { return getTrustStoreType(); } + public void setTrustStoreType(String type) { + this.trustStoreType = type; + if (config != null) config.setTrustStoreType(type); + } + public void SetTrustStoreType(String type) { setTrustStoreType(type); } + + public KeyStore getTrustStore() { return trustStore; } + public KeyStore GetTrustStore() { return getTrustStore(); } + public void setTrustStore(KeyStore ts) { this.trustStore = ts; } + public void SetTrustStore(KeyStore ts) { setTrustStore(ts); } + + public ClassLoader getCustomizedCAsClassLoader() { return customizedCAsClassLoader; } + public ClassLoader GetCustomizedCAsClassLoader() { return getCustomizedCAsClassLoader(); } + public void setCustomizedCAsClassLoader(ClassLoader cl) { + this.customizedCAsClassLoader = cl; + if (config != null) config.setCustomizedCAsClassLoader(cl); + } + public void SetCustomizedCAsClassLoader(ClassLoader cl) { setCustomizedCAsClassLoader(cl); } + + public KeyStore loadTrustStore(InputStream in, char[] password, String type) throws Exception { + this.trustStore = TlsTrustManager.loadKeyStore(in, password, type); + return this.trustStore; + } + + public KeyStore loadTrustStore(String path, char[] password, String type) throws Exception { + this.trustStore = TlsTrustManager.loadKeyStore(path, password, type); + this.trustStorePath = path; + return this.trustStore; + } + + public KeyStore loadTrustStore(URL url, char[] password, String type) throws Exception { + if (url == null) throw new IllegalArgumentException("URL cannot be null"); + try (InputStream in = url.openStream()) { + return loadTrustStore(in, password, type); + } + } + + public KeyStore loadCustomizedCAs() { + char[] pwd = trustStorePassword != null ? trustStorePassword.toCharArray() : null; + this.trustStore = TlsTrustManager.loadCustomizedCAs(customizedCAsClassLoader, trustStorePath, pwd); + return this.trustStore; + } + public KeyStore LoadCustomizedCAs() { return loadCustomizedCAs(); } + + public TrustManager[] createTrustManagers() throws Exception { + if (this.trustStore == null) { + if (this.trustStorePath != null && !this.trustStorePath.isEmpty()) { + char[] pwd = trustStorePassword != null ? trustStorePassword.toCharArray() : null; + this.trustStore = loadTrustStore(this.trustStorePath, pwd, this.trustStoreType); + } else if (customizedCAsClassLoader != null) { + this.trustStore = loadCustomizedCAs(); + } + } + TlsTrustManager tm = new TlsTrustManager(config != null ? config : new ConnectionConfig(), this.trustStore); + if (this.certificateVerifier != null) { + if (config != null) config.setCertificateVerifier(this.certificateVerifier); + } + this.trustManagers = new TrustManager[] { tm }; + return this.trustManagers; + } + public TrustManager[] CreateTrustManagers() throws Exception { return createTrustManagers(); } + + public TrustManager[] getTrustManagers() { return trustManagers; } + public TrustManager[] GetTrustManagers() { return getTrustManagers(); } + public void setTrustManagers(TrustManager[] tms) { this.trustManagers = tms; } + public void SetTrustManagers(TrustManager[] tms) { setTrustManagers(tms); } + + public boolean isTlsVerifyCert() { return tlsVerifyCert; } + public boolean IsTlsVerifyCert() { return isTlsVerifyCert(); } + public void setTlsVerifyCert(boolean verify) { + this.tlsVerifyCert = verify; + if (config != null) config.setTlsVerifyCert(verify); + } + public void SetTlsVerifyCert(boolean verify) { setTlsVerifyCert(verify); } + + public TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; } + public TlsCertificateVerifier GetCertificateVerifier() { return getCertificateVerifier(); } + public void setCertificateVerifier(TlsCertificateVerifier verifier) { + this.certificateVerifier = verifier; + if (config != null) config.setCertificateVerifier(verifier); + } + public void SetCertificateVerifier(TlsCertificateVerifier verifier) { setCertificateVerifier(verifier); } + + // ========================================================================= + // Protocols & Cipher Suites + // ========================================================================= + + public String getSecurityProtocol() { return securityProtocol; } + public String GetSecurityProtocol() { return getSecurityProtocol(); } + public void setSecurityProtocol(String protocol) { + this.securityProtocol = protocol; + if (config != null) config.setSslProtocol(protocol); + } + public void SetSecurityProtocol(String protocol) { setSecurityProtocol(protocol); } + + public String getTLSProtocolVersion() { return tlsProtocolVersion; } + public String GetTLSProtocolVersion() { return getTLSProtocolVersion(); } + public void setTLSProtocolVersion(String version) { + this.tlsProtocolVersion = version; + if (version != null && !version.trim().isEmpty()) { + this.securityProtocol = version.trim(); + this.enabledProtocols.clear(); + this.enabledProtocols.add(version.trim()); + if (config != null) { + config.setSslProtocol(version.trim()); + config.setEnabledProtocols(version.trim()); + } + } + } + public void SetTLSProtocolVersion(String version) { setTLSProtocolVersion(version); } + + public String[] getEnabledProtocols() { + return enabledProtocols.toArray(new String[0]); + } + public String[] GetEnabledProtocols() { return getEnabledProtocols(); } + + public void setEnabledProtocols(String[] protocols) { + this.enabledProtocols.clear(); + if (protocols != null) { + this.enabledProtocols.addAll(Arrays.asList(protocols)); + } + if (config != null) config.setEnabledProtocols(protocols); + } + public void SetEnabledProtocols(String[] protocols) { setEnabledProtocols(protocols); } + + public String[] getSupportedProtocols() { + try { + SSLContext ctx = getSSLContext(); + if (ctx == null) ctx = createSSLContext(); + SSLSocketFactory factory = ctx.getSocketFactory(); + try (SSLSocket dummy = (SSLSocket) factory.createSocket()) { + return dummy.getSupportedProtocols(); + } + } catch (Exception e) { + return new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1" }; + } + } + public String[] GetSupportedProtocols() { return getSupportedProtocols(); } + + public String[] getEnabledCipherSuites() { + return enabledCipherSuites.toArray(new String[0]); + } + public String[] GetEnabledCipherSuites() { return getEnabledCipherSuites(); } + + public void setEnabledCipherSuites(String[] ciphers) { + this.enabledCipherSuites.clear(); + if (ciphers != null) { + this.enabledCipherSuites.addAll(Arrays.asList(ciphers)); + } + if (config != null) config.setEnabledCipherSuites(ciphers); + } + public void SetEnabledCipherSuites(String[] ciphers) { setEnabledCipherSuites(ciphers); } + + public String[] getSupportedCipherSuites() { + try { + SSLContext ctx = getSSLContext(); + if (ctx == null) ctx = createSSLContext(); + SSLSocketFactory factory = ctx.getSocketFactory(); + try (SSLSocket dummy = (SSLSocket) factory.createSocket()) { + return dummy.getSupportedCipherSuites(); + } + } catch (Exception e) { + return new String[0]; + } + } + public String[] GetSupportedCipherSuites() { return getSupportedCipherSuites(); } + + public void applySocketSettings(SSLSocket socket) { + if (socket == null) return; + if (!enabledProtocols.isEmpty()) { + try { + socket.setEnabledProtocols(enabledProtocols.toArray(new String[0])); + } catch (Exception e) { + log.log(Level.WARNING, "Failed setting socket enabled protocols", e); + } + } + if (!enabledCipherSuites.isEmpty()) { + try { + socket.setEnabledCipherSuites(enabledCipherSuites.toArray(new String[0])); + } catch (Exception e) { + log.log(Level.WARNING, "Failed setting socket enabled cipher suites", e); + } + } + } + + public void negotiateProtocols(SSLSocket socket) { applySocketSettings(socket); } + public void negotiateCipherSuites(SSLSocket socket) { applySocketSettings(socket); } + + // ========================================================================= + // SSLContext & Socket Factory + // ========================================================================= + + public SSLContext createSSLContext() throws Exception { + if (this.keyManagers == null) { + createKeyManagers(); + } + if (this.trustManagers == null) { + createTrustManagers(); + } + String proto = (securityProtocol != null && !securityProtocol.trim().isEmpty()) + ? securityProtocol.trim() + : "TLS"; + this.sslContext = SSLContext.getInstance(proto); + this.sslContext.init(this.keyManagers, this.trustManagers, new SecureRandom()); + this.sslSocketFactory = this.sslContext.getSocketFactory(); + return this.sslContext; + } + public SSLContext CreateSSLContext() throws Exception { return createSSLContext(); } + + public SSLContext initSSLContext() throws Exception { return createSSLContext(); } + public SSLContext InitSSLContext() throws Exception { return createSSLContext(); } + + public SSLContext getSSLContext() { return sslContext; } + public SSLContext GetSSLContext() { return getSSLContext(); } + public void setSSLContext(SSLContext ctx) { + this.sslContext = ctx; + this.sslSocketFactory = (ctx != null) ? ctx.getSocketFactory() : null; + } + public void SetSSLContext(SSLContext ctx) { setSSLContext(ctx); } + + public SSLSocketFactory getSSLSocketFactory() throws Exception { + if (this.sslContext == null) { + createSSLContext(); + } + if (this.sslSocketFactory == null && this.sslContext != null) { + this.sslSocketFactory = this.sslContext.getSocketFactory(); + } + return this.sslSocketFactory; + } + public SSLSocketFactory GetSSLSocketFactory() throws Exception { return getSSLSocketFactory(); } + + public Socket createSocket(String host, int port) throws IOException { + try { + SSLSocketFactory factory = getSSLSocketFactory(); + SSLSocket socket = (SSLSocket) factory.createSocket(host, port); + applySocketSettings(socket); + socket.startHandshake(); + this.activeSocket = socket; + this.sslSession = socket.getSession(); + if (this.session != null) { + this.session.setKeyStrength(getKeyStrength()); + } + return socket; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed creating TLS socket to " + host + ":" + port + ": " + e.getMessage(), e); + } + } + public Socket CreateSocket(String host, int port) throws IOException { return createSocket(host, port); } + + public Socket createSocket(Socket existingSocket, String host, int port, boolean autoClose) throws IOException { + try { + SSLSocketFactory factory = getSSLSocketFactory(); + SSLSocket socket = (SSLSocket) factory.createSocket(existingSocket, host, port, autoClose); + applySocketSettings(socket); + socket.startHandshake(); + this.activeSocket = socket; + this.sslSession = socket.getSession(); + if (this.session != null) { + this.session.setKeyStrength(getKeyStrength()); + } + return socket; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed upgrading socket to TLS for " + host + ":" + port + ": " + e.getMessage(), e); + } + } + public Socket CreateSocket(Socket existingSocket, String host, int port, boolean autoClose) throws IOException { + return createSocket(existingSocket, host, port, autoClose); + } + + // ========================================================================= + // Security Status & Session Inspection + // ========================================================================= + + public SSLSession getSSLSession() { return sslSession; } + public SSLSession GetSSLSession() { return getSSLSession(); } + public void setSSLSession(SSLSession session) { this.sslSession = session; } + public void SetSSLSession(SSLSession session) { setSSLSession(session); } + + public String getProtocol() { + return sslSession != null ? sslSession.getProtocol() : (enabledProtocols.isEmpty() ? "TLS" : enabledProtocols.get(0)); + } + public String GetProtocol() { return getProtocol(); } + + public String getCipherSuite() { + return sslSession != null ? sslSession.getCipherSuite() : "NONE"; + } + public String GetCipherSuite() { return getCipherSuite(); } + + public int getKeyStrength() { + if (sslSession == null) return 0; + String cipher = sslSession.getCipherSuite(); + if (cipher == null) return 0; + if (cipher.contains("256")) return 256; + if (cipher.contains("128")) return 128; + if (cipher.contains("3DES") || cipher.contains("168") || cipher.contains("192")) return 168; + if (cipher.contains("DES")) return 56; + if (cipher.contains("NULL")) return 0; + return 128; + } + public int GetKeyStrength() { return getKeyStrength(); } + + public Certificate[] getPeerCertificates() { + if (sslSession != null) { + try { + return sslSession.getPeerCertificates(); + } catch (SSLPeerUnverifiedException ignored) {} + } + return new Certificate[0]; + } + public Certificate[] GetPeerCertificates() { return getPeerCertificates(); } + + public Certificate[] getLocalCertificates() { + if (sslSession != null) { + Certificate[] certs = sslSession.getLocalCertificates(); + if (certs != null) return certs; + } + return new Certificate[0]; + } + public Certificate[] GetLocalCertificates() { return getLocalCertificates(); } + + public boolean isAuthenticated() { + return sslSession != null && sslSession.isValid(); + } + public boolean IsAuthenticated() { return isAuthenticated(); } + + public boolean isConnected() { + return activeSocket != null && activeSocket.isConnected() && !activeSocket.isClosed(); + } + public boolean IsConnected() { return isConnected(); } + + public boolean isUseJSSE() { return useJSSE; } + public boolean IsUseJSSE() { return isUseJSSE(); } + public void setUseJSSE(boolean jsse) { this.useJSSE = jsse; } + public void SetUseJSSE(boolean jsse) { setUseJSSE(jsse); } + + public void close() { + if (activeSocket != null && !activeSocket.isClosed()) { + try { + activeSocket.close(); + } catch (IOException ignored) {} + } + } + public void Close() { close(); } + + public void reset() { + close(); + this.sslSession = null; + this.activeSocket = null; + this.sslContext = null; + this.sslSocketFactory = null; + } + public void Reset() { reset(); } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java index 8bd9d28..66b2ac9 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java @@ -123,6 +123,7 @@ public class TelnetConnection { if (config.getSoTimeoutMs() > 0) { sslSocket.setSoTimeout(config.getSoTimeoutMs()); } + applyTlsSocketSettings(sslSocket); sslSocket.startHandshake(); socket = sslSocket; sslSession = sslSocket.getSession(); @@ -166,6 +167,7 @@ public class TelnetConnection { if (config.getSoTimeoutMs() > 0) { sslSocket.setSoTimeout(config.getSoTimeoutMs()); } + applyTlsSocketSettings(sslSocket); sslSocket.startHandshake(); this.socket = sslSocket; this.sslSession = sslSocket.getSession(); @@ -180,6 +182,24 @@ public class TelnetConnection { } } + private void applyTlsSocketSettings(javax.net.ssl.SSLSocket sslSocket) { + if (config == null || sslSocket == null) return; + if (config.getEnabledProtocols() != null && !config.getEnabledProtocols().isEmpty()) { + try { + sslSocket.setEnabledProtocols(config.getEnabledProtocols().toArray(new String[0])); + } catch (Exception e) { + log.log(java.util.logging.Level.WARNING, "Failed to apply enabled TLS protocols", e); + } + } + if (config.getEnabledCipherSuites() != null && !config.getEnabledCipherSuites().isEmpty()) { + try { + sslSocket.setEnabledCipherSuites(config.getEnabledCipherSuites().toArray(new String[0])); + } catch (Exception e) { + log.log(java.util.logging.Level.WARNING, "Failed to apply enabled TLS cipher suites", e); + } + } + } + private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException { OutputStream out = s.getOutputStream(); InputStream in = s.getInputStream(); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java index 13823dd..3615c1e 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java @@ -75,7 +75,17 @@ public class TelnetFSM { return list; } if (config.isDynamicModel()) { - list.add(config.isExtendedDataStream() ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC"); + if (config.isExtendedDataStream()) { + list.add("IBM-DYNAMIC-E"); + } + list.add("IBM-DYNAMIC"); + list.add("IBM-3279-4-E"); + list.add("IBM-3279-4"); + list.add("IBM-3279-2-E"); + list.add("IBM-3279-2"); + list.add("IBM-3278-2"); + list.add("UNKNOWN"); + return list; } TerminalModel model = config.getModel(); list.add(model.getTerminalType()); @@ -1396,6 +1406,7 @@ public class TelnetFSM { } private void sendBytes(byte[] data) { + if (connection == null) return; try { connection.sendRaw(data); } catch (IOException e) { @@ -1466,8 +1477,8 @@ public class TelnetFSM { break; case 0x03: bindRd = 24; bindCd = 80; - bindRa = screenBuffer.getMaxRows(); - bindCa = screenBuffer.getMaxCols(); + bindRa = config.isDynamicModel() ? config.getDynamicRows() : screenBuffer.getMaxRows(); + bindCa = config.isDynamicModel() ? config.getDynamicCols() : screenBuffer.getMaxCols(); break; case 0x7E: bindRa = bindRd; bindCa = bindCd; @@ -1477,17 +1488,15 @@ public class TelnetFSM { bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF; break; default: - bindRa = screenBuffer.getMaxRows(); - bindCa = screenBuffer.getMaxCols(); + bindRa = config.isDynamicModel() ? config.getDynamicRows() : screenBuffer.getMaxRows(); + bindCa = config.isDynamicModel() ? config.getDynamicCols() : screenBuffer.getMaxCols(); break; } log.info("BIND SSIZE=0x" + String.format("%02x", ssize) + " default=" + bindRd + "x" + bindCd + " alt=" + bindRa + "x" + bindCa); - int maxR = screenBuffer.getMaxRows(); - int maxC = screenBuffer.getMaxCols(); - if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) { + if (bindRa > 0 && bindCa > 0) { screenBuffer.setAlternateDimensions(bindRa, bindCa); } } @@ -1739,4 +1748,69 @@ public class TelnetFSM { default: return "OP-" + op; } } + + // ========== Phase 3: HoD Telnet3270E Methods ========== + + /** + * Sends a 5-byte TN3270E header matching HoD Telnet3270E.send_TN3270E_header. + */ + public void send_TN3270E_header(byte type, byte req, byte resp) { + if (!tn3270eNegotiated) return; + byte[] header = new byte[EH_SIZE]; + header[0] = type; + header[1] = req; + header[2] = resp; + header[3] = (byte) ((eXmitSeq >> 8) & 0xFF); + header[4] = (byte) (eXmitSeq & 0xFF); + eXmitSeq = (eXmitSeq + 1) & 0xFFFF; + sendBytes(header); + } + + /** + * Processes outbound data from short buffer matching HoD Telnet3270E.process_outbound. + */ + public void process_outbound(short[] buf, int off, int len) { + if (buf == null || len <= 0) return; + byte[] bdata = new byte[len]; + for (int i = 0; i < len; i++) { + bdata[i] = (byte) (buf[off + i] & 0xFF); + } + process_outbound(bdata, 0, len); + } + + /** + * Processes outbound data from byte buffer. + */ + public void process_outbound(byte[] buf, int off, int len) { + if (buf == null || len <= 0) return; + byte[] data = new byte[len]; + System.arraycopy(buf, off, data, 0, len); + send3270Data(data); + } + + /** + * Processes End of Record (EOR) transmission matching HoD Telnet3270E.process_EOR. + */ + public void process_EOR(boolean endOfRecord) { + if (endOfRecord) { + byte[] eor = new byte[] { (byte) IAC, (byte) EOR }; + sendBytes(eor); + } + } + + /** + * Requests negotiation of TN3270E functions matching HoD Telnet3270E.negotiate_functions. + */ + public void negotiate_functions() { + if (tn3270eNegotiated) { + sendTN3270EFunctionsRequest(); + } + } + + /** + * Checks if a TN3270E function has been negotiated. + */ + public boolean isFunctionNegotiated(int func) { + return func >= 0 && func < eFuncs.length && eFuncs[func]; + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsCertificateVerifier.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsCertificateVerifier.java index 2c670a7..bf1cb47 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsCertificateVerifier.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsCertificateVerifier.java @@ -10,6 +10,12 @@ import java.security.cert.X509Certificate; @FunctionalInterface public interface TlsCertificateVerifier { + /** Trust all certificates unconditionally. */ + TlsCertificateVerifier TRUST_ALL = (chain, authType, exception) -> true; + + /** Reject unverified certificates. */ + TlsCertificateVerifier REJECT_ALL = (chain, authType, exception) -> false; + /** * Determine whether to trust an unverified server certificate chain. * diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsTrustManager.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsTrustManager.java index bdd68f4..03816eb 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsTrustManager.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tls/TlsTrustManager.java @@ -22,18 +22,36 @@ public class TlsTrustManager implements X509TrustManager { private final ConnectionConfig config; private X509TrustManager defaultTrustManager; + private X509TrustManager customTrustManager; + private KeyStore customTrustStore; public TlsTrustManager(ConnectionConfig config) { + this(config, null); + } + + public TlsTrustManager(ConnectionConfig config, KeyStore customTrustStore) { this.config = config; + this.customTrustStore = customTrustStore; initDefaultTrustManager(); + if (customTrustStore != null) { + initCustomTrustManager(customTrustStore); + } } public TlsTrustManager(boolean tlsVerifyCert, TlsCertificateVerifier verifier) { + this(null, tlsVerifyCert, verifier); + } + + public TlsTrustManager(KeyStore customTrustStore, boolean tlsVerifyCert, TlsCertificateVerifier verifier) { ConnectionConfig cfg = new ConnectionConfig(); cfg.setTlsVerifyCert(tlsVerifyCert); cfg.setCertificateVerifier(verifier); this.config = cfg; + this.customTrustStore = customTrustStore; initDefaultTrustManager(); + if (customTrustStore != null) { + initCustomTrustManager(customTrustStore); + } } private void initDefaultTrustManager() { @@ -51,8 +69,44 @@ public class TlsTrustManager implements X509TrustManager { } } + private void initCustomTrustManager(KeyStore trustStore) { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + this.customTrustManager = (X509TrustManager) tm; + break; + } + } + } catch (Exception e) { + log.log(Level.WARNING, "Failed to initialize custom TrustManagerFactory", e); + } + } + + public KeyStore getCustomTrustStore() { + return customTrustStore; + } + + public void setCustomTrustStore(KeyStore trustStore) { + this.customTrustStore = trustStore; + if (trustStore != null) { + initCustomTrustManager(trustStore); + } else { + this.customTrustManager = null; + } + } + @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (customTrustManager != null) { + try { + customTrustManager.checkClientTrusted(chain, authType); + return; + } catch (CertificateException e) { + // Try default trust manager if custom fails + } + } if (defaultTrustManager != null) { defaultTrustManager.checkClientTrusted(chain, authType); } @@ -76,39 +130,246 @@ public class TlsTrustManager implements X509TrustManager { throw ex; } - try { - if (defaultTrustManager != null) { - defaultTrustManager.checkServerTrusted(chain, authType); - } else { - throw new CertificateException("No default X509TrustManager available"); - } - } catch (Exception ex) { - CertificateException certEx = (ex instanceof CertificateException) - ? (CertificateException) ex - : new CertificateException("Certificate validation failed: " + ex.getMessage(), ex); + CertificateException lastException = null; - log.log(Level.FINE, "Standard certificate validation failed: " + certEx.getMessage(), certEx); - - if (config != null && config.getCertificateVerifier() != null) { - boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, certEx); - if (accepted) { - log.info("Server certificate accepted via TlsCertificateVerifier callback"); - return; - } else { - log.warning("Server certificate rejected by TlsCertificateVerifier callback"); - throw new CertificateException("Certificate rejected by user/verifier: " + certEx.getMessage(), certEx); - } + // 1. Try custom truststore first if available (e.g. CustomizedCAs.p12 / CustomizedCAs.jks) + if (customTrustManager != null) { + try { + customTrustManager.checkServerTrusted(chain, authType); + log.fine("Server certificate validated via custom truststore"); + return; + } catch (CertificateException ex) { + lastException = ex; + log.log(Level.FINE, "Custom truststore validation failed, falling back to JVM defaults: " + ex.getMessage()); } - throw certEx; } + + // 2. Try default JVM trust manager + if (defaultTrustManager != null) { + try { + defaultTrustManager.checkServerTrusted(chain, authType); + log.fine("Server certificate validated via default JVM truststore"); + return; + } catch (CertificateException ex) { + lastException = ex; + log.log(Level.FINE, "Standard certificate validation failed: " + ex.getMessage()); + } + } + + // 3. Fallback to interactive/custom verifier callback + if (config != null && config.getCertificateVerifier() != null) { + boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, lastException); + if (accepted) { + log.info("Server certificate accepted via TlsCertificateVerifier callback"); + return; + } else { + log.warning("Server certificate rejected by TlsCertificateVerifier callback"); + throw new CertificateException("Certificate rejected by user/verifier: " + + (lastException != null ? lastException.getMessage() : "untrusted chain"), lastException); + } + } + + if (lastException != null) { + throw lastException; + } + throw new CertificateException("No X509TrustManager available to validate server certificate"); } @Override public X509Certificate[] getAcceptedIssuers() { - if (defaultTrustManager != null) { - return defaultTrustManager.getAcceptedIssuers(); + java.util.List issuers = new java.util.ArrayList<>(); + if (customTrustManager != null) { + X509Certificate[] customIssuers = customTrustManager.getAcceptedIssuers(); + if (customIssuers != null) { + for (X509Certificate c : customIssuers) issuers.add(c); + } } - return new X509Certificate[0]; + if (defaultTrustManager != null) { + X509Certificate[] defIssuers = defaultTrustManager.getAcceptedIssuers(); + if (defIssuers != null) { + for (X509Certificate c : defIssuers) issuers.add(c); + } + } + return issuers.toArray(new X509Certificate[0]); + } + + // ========================================================================= + // KeyStore & SSLContext Utility Methods + // ========================================================================= + + /** + * Load a KeyStore from an InputStream with optional format auto-detection. + * Tries PKCS12 first, then JKS if type is null or unspecified. + */ + public static KeyStore loadKeyStore(java.io.InputStream in, char[] password, String type) throws Exception { + if (in == null) throw new IllegalArgumentException("InputStream cannot be null"); + byte[] data = in.readAllBytes(); + + String[] typesToTry; + if (type != null && !type.trim().isEmpty()) { + typesToTry = new String[] { type.trim() }; + } else { + typesToTry = new String[] { "PKCS12", "JKS" }; + } + + Exception lastEx = null; + for (String t : typesToTry) { + try { + KeyStore ks = KeyStore.getInstance(t); + ks.load(new java.io.ByteArrayInputStream(data), password); + return ks; + } catch (Exception ex) { + lastEx = ex; + } + } + throw (lastEx != null) ? lastEx : new java.io.IOException("Failed to load KeyStore"); + } + + /** + * Load a KeyStore from a file path or URL string. + */ + public static KeyStore loadKeyStore(String path, char[] password, String type) throws Exception { + if (path == null || path.trim().isEmpty()) throw new IllegalArgumentException("Path cannot be empty"); + String p = path.trim(); + + // Check if URL + if (p.startsWith("file:") || p.startsWith("http:") || p.startsWith("https:") || p.startsWith("jar:")) { + try (java.io.InputStream in = new java.net.URL(p).openStream()) { + return loadKeyStore(in, password, type); + } + } + + // Check filesystem file + java.io.File file = new java.io.File(p); + if (file.exists() && file.isFile()) { + try (java.io.InputStream in = new java.io.FileInputStream(file)) { + return loadKeyStore(in, password, type); + } + } + + // Try ClassLoader resource + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl != null) { + java.io.InputStream in = cl.getResourceAsStream(p); + if (in != null) { + try (in) { + return loadKeyStore(in, password, type); + } + } + } + + throw new java.io.FileNotFoundException("KeyStore file not found: " + path); + } + + /** + * Load IBM CustomizedCAs keystore from classloader or standard files. + * Checks CustomizedCAs.p12 (default password "hod") and CustomizedCAs.jks (default password "hodpwd"). + */ + public static KeyStore loadCustomizedCAs(ClassLoader cl, String explicitPath, char[] explicitPassword) { + ClassLoader loader = (cl != null) ? cl : Thread.currentThread().getContextClassLoader(); + if (loader == null) loader = TlsTrustManager.class.getClassLoader(); + + // 1. If explicit path provided + if (explicitPath != null && !explicitPath.trim().isEmpty()) { + char[] pwd = explicitPassword != null ? explicitPassword : "hod".toCharArray(); + try { + return loadKeyStore(explicitPath, pwd, null); + } catch (Exception e) { + log.log(Level.FINE, "Failed loading explicit CustomizedCAs: " + explicitPath, e); + } + } + + // 2. Check resource CustomizedCAs.p12 (password "hod") + try { + java.io.InputStream in = loader.getResourceAsStream("CustomizedCAs.p12"); + if (in != null) { + try (in) { + char[] pwd = (explicitPassword != null) ? explicitPassword : "hod".toCharArray(); + return loadKeyStore(in, pwd, "PKCS12"); + } + } + } catch (Exception ignored) {} + + // 3. Check resource CustomizedCAs.jks (password "hodpwd") + try { + java.io.InputStream in = loader.getResourceAsStream("CustomizedCAs.jks"); + if (in != null) { + try (in) { + char[] pwd = (explicitPassword != null) ? explicitPassword : "hodpwd".toCharArray(); + return loadKeyStore(in, pwd, "JKS"); + } + } + } catch (Exception ignored) {} + + // 4. Check filesystem working directory for CustomizedCAs.p12 or CustomizedCAs.jks + java.io.File p12File = new java.io.File("CustomizedCAs.p12"); + if (p12File.exists() && p12File.isFile()) { + try { + char[] pwd = (explicitPassword != null) ? explicitPassword : "hod".toCharArray(); + return loadKeyStore(p12File.getAbsolutePath(), pwd, "PKCS12"); + } catch (Exception ignored) {} + } + java.io.File jksFile = new java.io.File("CustomizedCAs.jks"); + if (jksFile.exists() && jksFile.isFile()) { + try { + char[] pwd = (explicitPassword != null) ? explicitPassword : "hodpwd".toCharArray(); + return loadKeyStore(jksFile.getAbsolutePath(), pwd, "JKS"); + } catch (Exception ignored) {} + } + + return null; + } + + /** + * Create KeyManagers for client certificate authentication. + */ + public static KeyManager[] createKeyManagers(KeyStore keyStore, char[] password, String alias) throws Exception { + if (keyStore == null) return null; + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, password); + KeyManager[] kms = kmf.getKeyManagers(); + + if (alias == null || alias.trim().isEmpty() || kms == null) { + return kms; + } + + final String selectAlias = alias.trim(); + for (int i = 0; i < kms.length; i++) { + if (kms[i] instanceof X509KeyManager) { + final X509KeyManager origKm = (X509KeyManager) kms[i]; + kms[i] = new X509ExtendedKeyManager() { + @Override + public String chooseClientAlias(String[] keyType, java.security.Principal[] issuers, java.net.Socket socket) { + return selectAlias; + } + @Override + public String chooseServerAlias(String keyType, java.security.Principal[] issuers, java.net.Socket socket) { + return origKm.chooseServerAlias(keyType, issuers, socket); + } + @Override + public X509Certificate[] getCertificateChain(String a) { + return origKm.getCertificateChain(a); + } + @Override + public String[] getClientAliases(String keyType, java.security.Principal[] issuers) { + return origKm.getClientAliases(keyType, issuers); + } + @Override + public String[] getServerAliases(String keyType, java.security.Principal[] issuers) { + return origKm.getServerAliases(keyType, issuers); + } + @Override + public java.security.PrivateKey getPrivateKey(String a) { + return origKm.getPrivateKey(a); + } + @Override + public String chooseEngineClientAlias(String[] keyType, java.security.Principal[] issuers, SSLEngine engine) { + return selectAlias; + } + }; + } + } + return kms; } /** @@ -119,8 +380,37 @@ public class TlsTrustManager implements X509TrustManager { ? config.getSslProtocol() : "TLS"; SSLContext sslContext = SSLContext.getInstance(protocol); - TlsTrustManager trustManager = new TlsTrustManager(config); - sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom()); + + KeyStore customTrustStore = null; + if (config != null) { + if (config.getTrustStorePath() != null && !config.getTrustStorePath().trim().isEmpty()) { + char[] pwd = config.getTrustStorePassword() != null ? config.getTrustStorePassword().toCharArray() : null; + customTrustStore = loadKeyStore(config.getTrustStorePath(), pwd, config.getTrustStoreType()); + } else if (config.getCustomizedCAsClassLoader() != null) { + customTrustStore = loadCustomizedCAs(config.getCustomizedCAsClassLoader(), null, null); + } + } + + TlsTrustManager trustManager = new TlsTrustManager(config, customTrustStore); + + KeyManager[] keyManagers = null; + if (config != null && config.getKeyStorePath() != null && !config.getKeyStorePath().trim().isEmpty()) { + char[] pwd = config.getKeyStorePassword() != null ? config.getKeyStorePassword().toCharArray() : null; + KeyStore ks = loadKeyStore(config.getKeyStorePath(), pwd, config.getKeyStoreType()); + keyManagers = createKeyManagers(ks, pwd, config.getKeyStoreAlias()); + } + + sslContext.init(keyManagers, new TrustManager[] { trustManager }, new SecureRandom()); return sslContext; } + + /** + * Create an SSLContext with explicit KeyManagers, TrustManagers, and protocol. + */ + public static SSLContext createSSLContext(String protocol, KeyManager[] keyManagers, TrustManager[] trustManagers, SecureRandom random) throws Exception { + String prot = (protocol != null && !protocol.trim().isEmpty()) ? protocol.trim() : "TLS"; + SSLContext ctx = SSLContext.getInstance(prot); + ctx.init(keyManagers, trustManagers, random != null ? random : new SecureRandom()); + return ctx; + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/DS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/DS3270.java new file mode 100644 index 0000000..97484d1 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/DS3270.java @@ -0,0 +1,392 @@ +package haus.nightmare.lib3270j.tn3270; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.datastream.QueryReplyBuilder; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +import java.util.logging.Logger; + +/** + * 3270 Data Stream processor matching IBM Host On-Demand (com.ibm.eNetwork.ECL.tn3270.DS3270). + * Handles inbound 3270 orders, control characters, structured fields, and query replies. + */ +public class DS3270 { + + private static final Logger log = Logger.getLogger(DS3270.class.getName()); + + // 3270 Command Constants + public static final short CMD_W = 1; + public static final short CMD_EW = 5; + public static final short CMD_EW2 = 3; + public static final short CMD_EWA = 13; + public static final short CMD_RB = 2; + public static final short CMD_RM = 6; + public static final short CMD_RMA = 14; + public static final short CMD_EAU = 15; + public static final short CMD_WSF = 17; + + // SNA 3270 Command Constants + public static final short SNA_CMD_W = 241; + public static final short SNA_CMD_EW = 245; + public static final short SNA_CMD_EWA = 126; + public static final short SNA_CMD_RB = 242; + public static final short SNA_CMD_RM = 246; + public static final short SNA_CMD_RMA = 110; + public static final short SNA_CMD_EAU = 111; + public static final short SNA_CMD_WSF = 243; + + // ASCII equivalents + public static final short ASCII_SNA_CMD_W = 49; + public static final short ASCII_SNA_CMD_EW = 53; + public static final short ASCII_SNA_CMD_EWA = 61; + public static final short ASCII_SNA_CMD_RB = 50; + public static final short ASCII_SNA_CMD_RM = 54; + public static final short ASCII_SNA_CMD_RMA = 62; + public static final short ASCII_SNA_CMD_EAU = 63; + public static final short ASCII_SNA_CMD_WSF = 51; + + // Diagnostic Commands + public static final short DCMD_W = 241; + public static final short DCMD_EW = 245; + public static final short DCMD_EWA = 126; + public static final short DCMD_EAU = 111; + public static final short DCMD_BSC = 247; + + // Write Control Character (WCC) bits + public static final short WCC_RESET = 64; // 0x40 + public static final short WCC_RESERVED = 48; // 0x30 + public static final short WCC_START = 8; // 0x08 + public static final short WCC_ALARM = 4; // 0x04 + public static final short WCC_RESTORE = 2; // 0x02 + public static final short WCC_RMDT = 1; // 0x01 + + public static final short request_bit_SDI = 1; + public static final short request_bit_KRI = 2; + + // Order Constants + public static final short ORD_SF = 29; // 0x1D + public static final short ORD_SFE = 41; // 0x29 + public static final short ORD_SBA = 17; // 0x11 + public static final short ORD_SA = 40; // 0x28 + public static final short ORD_MF = 44; // 0x2C + public static final short ORD_IC = 19; // 0x13 + public static final short ORD_PT = 5; // 0x05 + public static final short ORD_RA = 60; // 0x3C + public static final short ORD_EUA = 18; // 0x12 + public static final short ORD_GE = 8; // 0x08 + public static final short ORD_XANT = 43; // 0x2B + public static final short ORD_SNA_EOM = 25; // 0x19 + + // Structured Field Types + public static final short SF_RESET = 0; + public static final short SF_READ = 1; + public static final short SF_ERASE = 3; + public static final short SF_LOADPS = 6; + public static final short SF_SETREPLY = 9; + public static final short SF_SETORIG = 11; + public static final short SF_CREATE = 12; + public static final short SF_DESTORY = 13; + public static final short SF_ACTIVATE = 14; + public static final short SF_TWO = 15; + public static final short SF_TWO2 = 16; + public static final short SF_O3270DS = 64; + public static final short SF_SCS = 65; + public static final short SF_SELECT = 74; + public static final short SF_PRABS = 75; + public static final short SF_PRREL = 76; + public static final short SF_HFTP = 208; + + // Read Partition types + public static final short RP_QUERY = 2; + public static final short RP_QUERYLIST = 3; + public static final short RP_RMA = 110; + public static final short RP_RB = 242; + public static final short RP_RM = 246; + public static final short RPQL_MASK = 192; + public static final short RPQL_LIST_ONLY= 0; + public static final short RPQL_QE_LIST = 64; + public static final short RPQL_QUERY_ALL= 128; + + // Attributes + public static final short ATTR_GRAPHIC = 192; + public static final short ATTR_PROT = 32; + public static final short ATTR_NUMERIC = 16; + public static final short ATTR_DISPLAY = 12; + public static final short ATTR_NORM = 0; + public static final short ATTR_SEL = 4; + public static final short ATTR_HIGH = 8; + public static final short ATTR_INVIS = 12; + public static final short ATTR_MDT = 1; + + // Default Query Reply List matching HoD DS3270.java + public haus.nightmare.lib3270j.tn3270.qr_elem[] qreplylist = new haus.nightmare.lib3270j.tn3270.qr_elem[] { + new haus.nightmare.lib3270j.tn3270.qr_elem(129, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(132, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(133, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(134, 0), + new haus.nightmare.lib3270j.tn3270.qr_elem(135, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(136, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(140, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(145, 0), + new haus.nightmare.lib3270j.tn3270.qr_elem(149, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(153, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(159, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(160, 0), + new haus.nightmare.lib3270j.tn3270.qr_elem(166, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(168, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(176, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(177, 0), + new haus.nightmare.lib3270j.tn3270.qr_elem(178, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(179, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(180, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(182, 0), + new haus.nightmare.lib3270j.tn3270.qr_elem(128, 0) + }; + + // Query Equivalents matching HoD DS3270.java + public final haus.nightmare.lib3270j.tn3270.qr_elem[] queryEquiv = new haus.nightmare.lib3270j.tn3270.qr_elem[] { + new haus.nightmare.lib3270j.tn3270.qr_elem(129, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(132, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(133, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(134, 1), + new haus.nightmare.lib3270j.tn3270.qr_elem(135, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(136, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(140, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(145, 1), + new haus.nightmare.lib3270j.tn3270.qr_elem(149, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(153, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(159, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(160, 1), + new haus.nightmare.lib3270j.tn3270.qr_elem(166, 1), new haus.nightmare.lib3270j.tn3270.qr_elem(168, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(176, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(177, 0), + new haus.nightmare.lib3270j.tn3270.qr_elem(178, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(179, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(180, 0), new haus.nightmare.lib3270j.tn3270.qr_elem(182, 0), + new haus.nightmare.lib3270j.tn3270.qr_elem(128, 1) + }; + + public final byte[] attr_conversion_table = new byte[] { + 64, -63, -62, -61, -60, -59, -58, -57, -56, -55, 74, 75, 76, 77, 78, 79, + 80, -47, -46, -45, -44, -43, -42, -41, -40, -39, 90, 91, 92, 93, 94, 95, + 96, 97, -30, -29, -28, -27, -26, -25, -24, -23, 106, 107, 108, 109, 110, 111, + -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, 122, 123, 124, 125, 126, 127 + }; + + public boolean suppressClearEvent; + public boolean wsfvalid = true; + + // Underlying lib3270j data stream processor + private final DataStreamProcessor delegate; + private ECLSession session; + private ECLPS ps; + + public DS3270() { + this(new ScreenBuffer(), new EbcdicTranslator()); + } + + public DS3270(ScreenBuffer screen, EbcdicTranslator translator) { + this(new DataStreamProcessor(screen, translator)); + } + + public DS3270(DataStreamProcessor delegate) { + this.delegate = delegate; + } + + public DS3270(ECLSession session, ECLPS ps) { + this.session = session; + this.ps = ps; + ScreenBuffer sb = (ps != null) ? ps.getScreenBuffer() : new ScreenBuffer(); + EbcdicTranslator trans = (ps != null) ? ps.getTranslator() : new EbcdicTranslator(); + this.delegate = new DataStreamProcessor(sb, trans); + if (ps != null && ps.getInputProcessor() != null) { + this.delegate.setInputProcessor(ps.getInputProcessor()); + } + } + + public DataStreamProcessor getDataStreamProcessor() { + return delegate; + } + + public ECLSession getSession() { + return session; + } + + public ECLPS getPS() { + return ps; + } + + public void setToInitState() { + log.fine("DS3270 setToInitState called"); + } + + public void connectionReset() { + log.fine("DS3270 connectionReset called"); + } + + public void setScrSizetoDefault(boolean bl) { + if (delegate != null && delegate.getScreen() != null) { + delegate.getScreen().erase(false); + } + } + + public boolean setScreenToBindSize(int rows, int cols, int altRows, int altCols, int type) { + if (delegate != null && delegate.getScreen() != null) { + delegate.getScreen().setDimensions(rows, cols); + return true; + } + return false; + } + + public void endOfRecord() { + log.fine("DS3270 endOfRecord"); + } + + public int receiveData(short[] sArray, int off, int len) { + processData(sArray, off, len); + return len; + } + + public void processData(short[] data, int off, int len) { + delegate.processData(data, off, len); + } + + public void processData(byte[] data, int off, int len) { + delegate.processData(data, off, len); + } + + public void processWCC(short wcc) { + delegate.processWCC(wcc); + } + + public void processSBA(int baddr) { + delegate.processSBA(baddr); + } + + public void processSBA(int b1, int b2) { + delegate.processSBA(b1, b2); + } + + public void processSBA() { + delegate.processSBA(); + } + + public void processSF(byte fa) { + delegate.processSF(fa); + } + + public void processSF() { + delegate.processSF(); + } + + public void processSFE(byte[] pairs) { + delegate.processSFE(pairs); + } + + public void processSFE() { + delegate.processSFE(); + } + + public void processSA(int attrType, int attrValue) { + delegate.processSA(attrType, attrValue); + } + + public void processSA() { + delegate.processSA(); + } + + public void processMF(byte[] pairs) { + delegate.processMF(pairs); + } + + public void processMF() { + delegate.processMF(); + } + + public void processIC() { + delegate.processIC(); + } + + public void processPT() { + delegate.processPT(); + } + + public void processRA(int targetBaddr, int fillChar) { + delegate.processRA(targetBaddr, fillChar); + } + + public void processRA() { + delegate.processRA(); + } + + public void processEUA(int targetBaddr) { + delegate.processEUA(targetBaddr); + } + + public void processEUA() { + delegate.processEUA(); + } + + public void processGE(int geChar) { + delegate.processGE(geChar); + } + + public void processGE() { + delegate.processGE(); + } + + public void processWSF(short[] data, int off, int len) { + delegate.processWSF(data, off, len); + } + + public void processWSF(byte[] data, int off, int len) { + delegate.processWSF(data, off, len); + } + + public void sendAid(short aid, int cursorAddress) { + delegate.sendAid(aid, cursorAddress); + } + + public void sendMouseAid(int aid, char[] data, int len) { + log.fine("sendMouseAid aid=0x" + Integer.toHexString(aid) + " len=" + len); + } + + public boolean qr_update(haus.nightmare.lib3270j.tn3270.qr_elem[] qrElemArray, char c, short s, int n) { + if (qrElemArray == null) return false; + for (int i = 0; i < n && i < qrElemArray.length; i++) { + if (qrElemArray[i].qr_type == (byte) c) { + qrElemArray[i].qr_send_flag = (byte) s; + return true; + } + } + return false; + } + + public void qr_set(haus.nightmare.lib3270j.tn3270.qr_elem[] qrElemArray, short s, int n) { + if (qrElemArray == null) return; + for (int i = 0; i < n && i < qrElemArray.length; i++) { + qrElemArray[i].qr_send_flag = (byte) s; + } + } + + /** + * Nested Query Reply Element type matching HoD DS3270.qr_elem. + */ + public static class qr_elem extends haus.nightmare.lib3270j.tn3270.qr_elem { + public qr_elem(int n, int n2) { + super(n, n2); + } + + public qr_elem(byte b1, byte b2) { + super(b1, b2); + } + } + + /** + * Query Reply Object matching HoD DS3270.QueryReplyObject. + */ + public class QueryReplyObject { + private final StringBuilder buf = new StringBuilder(); + private final StringBuilder qr_summary = new StringBuilder("\u0000\u0005\u0081\u0080\u0080"); + + public QueryReplyObject() { + } + + public void addReply(String reply) { + if (reply != null) { + int count = qr_summary.charAt(1); + qr_summary.setCharAt(1, (char) (++count)); + if (reply.length() > 3) { + qr_summary.append(reply.charAt(3)); + } + buf.append(reply); + } + } + + public byte[] getBytes() { + StringBuilder full = new StringBuilder(buf); + full.append(qr_summary); + byte[] bytes = new byte[full.length()]; + for (int i = 0; i < full.length(); i++) { + bytes[i] = (byte) (full.charAt(i) & 0xFF); + } + return bytes; + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/NVT3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/NVT3270.java new file mode 100644 index 0000000..03ca7fa --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/NVT3270.java @@ -0,0 +1,150 @@ +package haus.nightmare.lib3270j.tn3270; + +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.nvt.NvtProcessor; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +import java.util.logging.Logger; + +/** + * Network Virtual Terminal (NVT) transport wrapper matching IBM Host On-Demand (com.ibm.eNetwork.ECL.tn3270.NVT3270). + */ +public class NVT3270 { + + private static final Logger log = Logger.getLogger(NVT3270.class.getName()); + + public static final short FUNCTION_TRACE = 1; + public static final short DEBUG_TRACE = 2; + + protected String host; + protected ECLSession session; + protected ECLPS aPS; + protected DS3270 aDS; + protected int CursorPos = 0; + protected int base_rows = 24; + protected int base_cols = 80; + + private NvtProcessor nvtProcessor; + + public NVT3270() { + this(null, null, null, null); + } + + public NVT3270(NvtProcessor nvtProcessor) { + this.nvtProcessor = nvtProcessor; + } + + public NVT3270(String host, ECLSession session, ECLPS ps, DS3270 ds) { + this.host = host; + this.session = session; + this.aPS = ps; + this.aDS = ds; + if (ps != null && ps.getScreenBuffer() != null) { + this.base_rows = ps.getScreenBuffer().getRows(); + this.base_cols = ps.getScreenBuffer().getCols(); + this.CursorPos = ps.getScreenBuffer().getCursorAddress(); + } + } + + public NvtProcessor getNvtProcessor() { + return nvtProcessor; + } + + public void setNvtProcessor(NvtProcessor nvtProcessor) { + this.nvtProcessor = nvtProcessor; + } + + public ECLSession getSession() { + return session; + } + + public ECLPS getPS() { + return aPS; + } + + public DS3270 getDS() { + return aDS; + } + + public void process_outbound(short[] sArray, int n, int n2) { + if (sArray == null || n2 <= 0) return; + byte[] bdata = new byte[n2]; + for (int i = 0; i < n2; i++) { + bdata[i] = (byte) (sArray[n + i] & 0xFF); + } + process_outbound(bdata, 0, n2); + } + + public void process_outbound(byte[] data, int n, int n2) { + if (data == null || n2 <= 0) return; + if (nvtProcessor != null) { + nvtProcessor.processBytes(data, n, n2); + } + if (aPS != null && !aPS.isNVTmode()) { + aPS.setNVTmode(true); + } + NVT_initialize_outbound(); + NVT_process_outbound(data, n, n2); + NVT_terminate_outbound(); + process_EOR(true); + } + + protected void NVT_initialize_outbound() { + StatusDisplay(1, "NVT_initialize_outbound Entry"); + short[] sArray = new short[5]; + sArray[0] = 241; // Write command + sArray[1] = 2; // WCC + sArray[2] = 17; // SBA + sArray[3] = address1st(); + sArray[4] = address2nd(); + SendToDS(sArray, 0, 5); + StatusDisplay(1, "NVT_initialize_outbound Exit"); + } + + protected void NVT_process_outbound(short[] sArray, int n, int n2) { + SendToDS(sArray, n, n2); + } + + protected void NVT_process_outbound(byte[] data, int n, int n2) { + if (aDS != null) { + aDS.processData(data, n, n2); + } + } + + protected void NVT_terminate_outbound() { + StatusDisplay(1, "NVT_terminate_outbound Entry"); + short[] sArray = new short[1]; + sArray[0] = 19; // IC (Insert Cursor) + SendToDS(sArray, 0, 1); + int total = base_rows * base_cols; + if (total > 0) { + CursorPos %= total; + } + StatusDisplay(1, "NVT_terminate_outbound Exit"); + } + + public void process_EOR(boolean endOfRecord) { + if (aDS != null && endOfRecord) { + aDS.endOfRecord(); + } + } + + protected short address1st() { + return (short) ((CursorPos >> 8) & 0xFF); + } + + protected short address2nd() { + return (short) (CursorPos & 0xFF); + } + + protected void SendToDS(short[] sArray, int n, int n2) { + if (aDS != null) { + aDS.receiveData(sArray, n, n2); + } + } + + public void StatusDisplay(int n, String string) { + log.finest("NVT3270 status (" + n + "): " + string); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/PS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/PS3270.java new file mode 100644 index 0000000..6e7ee21 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/PS3270.java @@ -0,0 +1,208 @@ +package haus.nightmare.lib3270j.tn3270; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +import java.util.logging.Logger; + +/** + * Presentation Space state engine matching IBM Host On-Demand (com.ibm.eNetwork.ECL.tn3270.PS3270). + * Handles keyboard input, character insertion, DBCS boundaries, special EAB planes, and LU-LU session tracking. + */ +public class PS3270 extends ECLPS { + + private static final Logger log = Logger.getLogger(PS3270.class.getName()); + + // Standard AID constants matching HoD PS3270 + public static final short AID_NONE = 96; // 0x60 + public static final short AID_RP = 97; // 0x61 + public static final short AID_CLEAR = 109; // 0x6D + public static final short AID_SYSREQ = 240; // 0xF0 + public static final short AID_ENTER = 125; // 0x7D + public static final short AID_PA1 = 108; // 0x6C + public static final short AID_PA2 = 110; // 0x6E + public static final short AID_PA3 = 107; // 0x6B + public static final short AID_PF1 = 241; // 0xF1 + public static final short AID_PF2 = 242; // 0xF2 + public static final short AID_PF3 = 243; // 0xF3 + public static final short AID_PF4 = 244; // 0xF4 + public static final short AID_PF5 = 245; // 0xF5 + public static final short AID_PF6 = 246; // 0xF6 + public static final short AID_PF7 = 247; // 0xF7 + public static final short AID_PF8 = 248; // 0xF8 + public static final short AID_PF9 = 249; // 0xF9 + public static final short AID_PF10 = 122; // 0x7A + public static final short AID_PF11 = 123; // 0x7B + public static final short AID_PF12 = 124; // 0x7C + public static final short AID_PF13 = 193; // 0xC1 + public static final short AID_PF14 = 194; // 0xC2 + public static final short AID_PF15 = 195; // 0xC3 + public static final short AID_PF16 = 196; // 0xC4 + public static final short AID_PF17 = 197; // 0xC5 + public static final short AID_PF18 = 198; // 0xC6 + public static final short AID_PF19 = 199; // 0xC7 + public static final short AID_PF20 = 200; // 0xC8 + public static final short AID_PF21 = 201; // 0xC9 + public static final short AID_PF22 = 74; // 0x4A + public static final short AID_PF23 = 75; // 0x4B + public static final short AID_PF24 = 76; // 0x4C + public static final short AID_CRSEL = 126; // 0x7E + public static final short AID_OICR = 230; // 0xE6 + public static final short AID_MSR = 231; // 0xE7 + + // Character plane types + public static final int CHAR_ALL = 0; + + // Lock flags + public boolean locked_SYSLOCK = false; + public boolean locked_TWAIT = false; + private boolean LULUSession = false; + + private ECLSession session; + + public PS3270() { + this(new ScreenBuffer(), null, new EbcdicTranslator()); + } + + public PS3270(ECLSession session) { + super( + (session != null && session.getPS() != null) ? session.getPS().getScreenBuffer() : new ScreenBuffer(), + (session != null && session.getPS() != null) ? session.getPS().getInputProcessor() : null, + (session != null && session.getPS() != null) ? session.getPS().getTranslator() : new EbcdicTranslator() + ); + this.session = session; + } + + public PS3270(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) { + super(screen, inputProcessor, translator); + } + + public ECLSession getSession() { + return session; + } + + public void setSession(ECLSession session) { + this.session = session; + } + + /** + * Handles key-down events matching HoD PS3270.keyDown(int key, boolean shift). + */ + public boolean keyDown(int key, boolean shift) { + InputProcessor ip = getInputProcessor(); + if (ip != null) { + return ip.keyDown(key, shift); + } + return false; + } + + /** + * Handles key-up events matching HoD PS3270.keyUp(int key, boolean shift). + */ + public boolean keyUp(int key, boolean shift) { + InputProcessor ip = getInputProcessor(); + if (ip != null) { + return ip.keyUp(key, shift); + } + return true; + } + + /** + * Direct keystroke insertion matching HoD PS3270.CharKeyStrokes(int code). + */ + public boolean CharKeyStrokes(int code) { + InputProcessor ip = getInputProcessor(); + if (ip != null) { + return ip.CharKeyStrokes(code); + } + return false; + } + + /** + * Dispatches or processes queued keystrokes matching HoD PS3270.ProcessKeyStrokes(). + */ + public void ProcessKeyStrokes() { + InputProcessor ip = getInputProcessor(); + if (ip != null) { + ip.ProcessKeyStrokes(); + } + } + + /** + * Inserts a double-byte character at pos matching HoD PS3270.DBCSinputChar(char c, int pos). + */ + public int DBCSinputChar(char c, int pos) { + ScreenBuffer sb = getScreenBuffer(); + if (sb != null) { + return sb.DBCSinputChar(c, pos); + } + return 0; + } + + /** + * Sets the special Extended Attribute Buffer value at pos matching HoD PS3270.SetSpecialEAB(int pos, byte val). + */ + public void SetSpecialEAB(int pos, byte val) { + ScreenBuffer sb = getScreenBuffer(); + if (sb != null) { + sb.setSpecialEAB(pos, val); + } + } + + /** + * Retrieves the special Extended Attribute Buffer value at pos. + */ + public byte GetSpecialEAB(int pos) { + ScreenBuffer sb = getScreenBuffer(); + if (sb != null) { + return sb.getSpecialEAB(pos); + } + return 0; + } + + /** + * Configures the LU-LU session state matching HoD PS3270.set_LULU_Session(boolean). + */ + public void set_LULU_Session(boolean lulu) { + this.LULUSession = lulu; + } + + /** + * Returns true if in an active LU-LU session matching HoD PS3270.is_LULU_Session(). + */ + public boolean is_LULU_Session() { + return this.LULUSession; + } + + public void lockKeyboard(int reason) { + if (reason == 7) locked_TWAIT = true; + if (reason == 8) locked_SYSLOCK = true; + InputProcessor ip = getInputProcessor(); + if (ip != null) { + ip.setKeyboardLocked(true); + } + } + + public void unlockKeyboard(int reason) { + if (reason == 7) locked_TWAIT = false; + if (reason == 8) locked_SYSLOCK = false; + if (!locked_TWAIT && !locked_SYSLOCK) { + InputProcessor ip = getInputProcessor(); + if (ip != null) { + ip.setKeyboardLocked(false); + } + } + } + + public boolean islocked_TWAIT() { + return locked_TWAIT; + } + + public boolean islocked_SYSLOCK() { + return locked_SYSLOCK; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/Telnet3270E.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/Telnet3270E.java new file mode 100644 index 0000000..fc94bed --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/Telnet3270E.java @@ -0,0 +1,173 @@ +package haus.nightmare.lib3270j.tn3270; + +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.telnet.TelnetConnection; +import haus.nightmare.lib3270j.telnet.TelnetFSM; +import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*; + +import java.util.logging.Logger; + +/** + * TN3270E protocol transport engine matching IBM Host On-Demand (com.ibm.eNetwork.ECL.tn3270.Telnet3270E). + * Extends NVT3270 with RFC 2355 TN3270E option negotiation, function negotiation, and record framing. + */ +public class Telnet3270E extends NVT3270 { + + private static final Logger log = Logger.getLogger(Telnet3270E.class.getName()); + + public static final short TELOPT_TN3270E = 40; + public static final int STARTTLS_SECURE = 2; + + // Reason codes + public static final short CONN_PARTNER = 0; + public static final short DEVICE_IN_USE = 1; + public static final short INV_ASSOCIATE = 2; + public static final short INV_DEVICE_NAME = 3; + public static final short INV_DEVICE_TYPE = 4; + public static final short TYPE_NAME_ERROR = 5; + public static final short UNKNOWN_ERROR = 6; + public static final short UNSUPPORTED_REQ = 7; + + // Sub-negotiation opcodes + public static final short TN3270E_ASSOCIATE = 0; + public static final short TN3270E_CONNECT = 1; + public static final short TN3270E_DEVICE_TYPE = 2; + public static final short TN3270E_FUNCTIONS = 3; + public static final short TN3270E_IS = 4; + public static final short TN3270E_REASON = 5; + public static final short TN3270E_REJECT = 6; + public static final short TN3270E_REQUEST = 7; + public static final short TN3270E_SEND = 8; + + // Functions + public static final short TN3270E_BIND_IMAGE = 0; + public static final short TN3270E_DATA_STREAM_CTL = 1; + public static final short TN3270E_RESPONSES = 2; + public static final short TN3270E_SCS_CTL_CODES = 3; + public static final short TN3270E_SYSREQ = 4; + public static final short TN3270E_CONT_RESOLUTION = 5; + + // Data types + public static final short DATA_TYPE_3270_DATA = 0; + public static final short DATA_TYPE_SCS_DATA = 1; + public static final short DATA_TYPE_RESPONSE = 2; + public static final short DATA_TYPE_BIND_IMAGE = 3; + public static final short DATA_TYPE_UNBIND = 4; + public static final short DATA_TYPE_NVT_DATA = 5; + public static final short DATA_TYPE_REQUEST = 6; + public static final short DATA_TYPE_SSCP_LU_DATA = 7; + public static final short DATA_TYPE_END_OF_JOB = 8; + public static final short DATA_TYPE_BID_REQUEST = 9; + + public static final short REQUEST_FLAG_ERR_COND_CLEARED = 0; + public static final short RESPONSE_FLAG_NO_RESPONSE = 0; + public static final short RESPONSE_FLAG_ERROR_RESPONSE = 1; + public static final short RESPONSE_FLAG_ALWAYS_RESPONSE = 2; + + protected boolean isNegotiateCR = true; + protected boolean tn3270e_enabled = false; + protected boolean TN3270E_partner = false; + + private TelnetFSM fsm; + private TelnetConnection connection; + + public Telnet3270E() { + this(null, null, null, null); + } + + public Telnet3270E(TelnetFSM fsm, TelnetConnection connection) { + super(); + this.fsm = fsm; + this.connection = connection; + } + + public Telnet3270E(String host, ECLSession session, ECLPS ps, DS3270 ds) { + super(host, session, ps, ds); + if (session != null && session.getClient() != null) { + this.fsm = session.getClient().getTelnetFSM(); + this.connection = session.getClient().getConnection(); + } + } + + public TelnetFSM getFSM() { + return fsm; + } + + public void setFSM(TelnetFSM fsm) { + this.fsm = fsm; + } + + public TelnetConnection getConnection() { + return connection; + } + + public void setConnection(TelnetConnection connection) { + this.connection = connection; + } + + @Override + public void process_outbound(short[] buf, int off, int len) { + if (fsm != null) { + fsm.process_outbound(buf, off, len); + } else { + super.process_outbound(buf, off, len); + } + } + + @Override + public void process_outbound(byte[] buf, int off, int len) { + if (fsm != null) { + fsm.process_outbound(buf, off, len); + } else { + super.process_outbound(buf, off, len); + } + } + + @Override + public void process_EOR(boolean endOfRecord) { + if (fsm != null) { + fsm.process_EOR(endOfRecord); + } else { + super.process_EOR(endOfRecord); + } + } + + public void send_TN3270E_header(byte type, byte req, byte resp) { + if (fsm != null) { + fsm.send_TN3270E_header(type, req, resp); + } + } + + public void negotiate_functions() { + if (fsm != null) { + fsm.negotiate_functions(); + } + } + + public boolean isFunctionNegotiated(int func) { + if (fsm != null) { + return fsm.isFunctionNegotiated(func); + } + return false; + } + + public void send_response(short status, short flag, int seq) { + byte[] resp = new byte[9]; + resp[0] = (byte) DATA_TYPE_RESPONSE; + resp[1] = 0; + resp[2] = (byte) flag; + resp[3] = (byte) ((seq >> 8) & 0xFF); + resp[4] = (byte) (seq & 0xFF); + resp[5] = (byte) status; + resp[6] = (byte) -1; // IAC + resp[7] = (byte) -17; // EOR + if (connection != null) { + try { + connection.sendRaw(resp); + } catch (Exception e) { + log.warning("Error sending response: " + e.getMessage()); + } + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/qr_elem.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/qr_elem.java new file mode 100644 index 0000000..83066a8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270/qr_elem.java @@ -0,0 +1,91 @@ +package haus.nightmare.lib3270j.tn3270; + +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +/** + * Query reply element structure matching IBM Host On-Demand (com.ibm.eNetwork.ECL.tn3270.qr_elem). + */ +public class qr_elem { + + public byte qr_type; + public byte qr_send_flag; + + public static final byte SEND_NO = 0; + public static final byte SEND_YES = 1; + + // Common query reply codes + public static final int TYPE_SUMMARY = QR_SUMMARY; // 0x80 (128) + public static final int TYPE_USABLE_AREA = QR_USABLE_AREA; // 0x81 (129) + public static final int TYPE_ALPHA_PART = QR_ALPHA_PART; // 0x84 (132) + public static final int TYPE_CHARSETS = QR_CHARSETS; // 0x85 (133) + public static final int TYPE_COLOR = QR_COLOR; // 0x86 (134) + public static final int TYPE_HIGHLIGHTING = QR_HIGHLIGHTING; // 0x87 (135) + public static final int TYPE_REPLY_MODES = QR_REPLY_MODES; // 0x88 (136) + public static final int TYPE_OUTLINING = QR_OUTLINING; // 0x8C (140) + public static final int TYPE_DDM = QR_DDM; // 0x95 (149) + public static final int TYPE_AUXDA = QR_AUXDA; // 0x99 (153) + public static final int TYPE_IMP_PART = QR_IMP_PART; // 0xA6 (166) + public static final int TYPE_TRANSPARENCY = QR_TRANSPARENCY; // 0xA8 (168) + public static final int TYPE_SEGMENT = QR_SEGMENT; // 0xB0 (176) + public static final int TYPE_PROCEDURE = QR_PROCEDURE; // 0xB1 (177) + public static final int TYPE_LINETYPE = QR_LINETYPE; // 0xB2 (178) + public static final int TYPE_PORT = QR_PORT; // 0xB3 (179) + public static final int TYPE_GRCOLOR = QR_GRCOLOR; // 0xB4 (180) + + public qr_elem() { + this(0, 0); + } + + public qr_elem(int type, int sendFlag) { + this.qr_type = (byte) (type & 0xFF); + this.qr_send_flag = (byte) (sendFlag & 0xFF); + } + + public qr_elem(byte type, byte sendFlag) { + this.qr_type = type; + this.qr_send_flag = sendFlag; + } + + public int getType() { + return qr_type & 0xFF; + } + + public void setType(int type) { + this.qr_type = (byte) (type & 0xFF); + } + + public int getSendFlag() { + return qr_send_flag & 0xFF; + } + + public void setSendFlag(int sendFlag) { + this.qr_send_flag = (byte) (sendFlag & 0xFF); + } + + public boolean isSendEnabled() { + return qr_send_flag != SEND_NO; + } + + public void setSendEnabled(boolean enabled) { + this.qr_send_flag = enabled ? SEND_YES : SEND_NO; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + qr_elem qrElem = (qr_elem) o; + return qr_type == qrElem.qr_type && qr_send_flag == qrElem.qr_send_flag; + } + + @Override + public int hashCode() { + return 31 * (qr_type & 0xFF) + (qr_send_flag & 0xFF); + } + + @Override + public String toString() { + return "qr_elem{type=0x" + Integer.toHexString(qr_type & 0xFF) + + ", sendFlag=" + (qr_send_flag & 0xFF) + "}"; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/DS3270P.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/DS3270P.java new file mode 100644 index 0000000..e5c8f49 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/DS3270P.java @@ -0,0 +1,31 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrintPS3270; +import haus.nightmare.lib3270j.printer.PrintSCS3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; +import haus.nightmare.lib3270j.printer.Telnet3270EP; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.DS3270P. + */ +public class DS3270P extends haus.nightmare.lib3270j.printer.DS3270P { + + public DS3270P() { + super(); + } + + public DS3270P(PrinterConfig config) { + super(config); + } + + public DS3270P(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } + + public DS3270P(Telnet3270EP telnet, PrinterConfig config, PD3270 pd, + PrintSCS3270 scs, PrintPS3270 printPs, EbcdicTranslator translator) { + super(telnet, config, pd, scs, printPs, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PD3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PD3270.java new file mode 100644 index 0000000..f0c014b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PD3270.java @@ -0,0 +1,17 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.PD3270. + */ +public class PD3270 extends haus.nightmare.lib3270j.printer.PD3270 { + + public PD3270() { + super(); + } + + public PD3270(PrinterConfig config) { + super(config); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PDT.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PDT.java new file mode 100644 index 0000000..b264f9f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PDT.java @@ -0,0 +1,11 @@ +package haus.nightmare.lib3270j.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.PDT. + */ +public class PDT extends PrinterDefinitionTable { + + public PDT(String name, String description) { + super(name, description); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintPS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintPS3270.java new file mode 100644 index 0000000..66eaac7 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintPS3270.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.PrintPS3270. + */ +public class PrintPS3270 extends haus.nightmare.lib3270j.printer.PrintPS3270 { + + public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintPS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintPS3270DB.java new file mode 100644 index 0000000..c0e480b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintPS3270DB.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.PrintPS3270DB. + */ +public class PrintPS3270DB extends haus.nightmare.lib3270j.printer.PrintPS3270DB { + + public PrintPS3270DB(PrinterConfig config) { + super(config); + } + + public PrintPS3270DB(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintSCS3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintSCS3270.java new file mode 100644 index 0000000..e3a3507 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintSCS3270.java @@ -0,0 +1,15 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.PrintSCS3270. + */ +public class PrintSCS3270 extends haus.nightmare.lib3270j.printer.PrintSCS3270 { + + public PrintSCS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintSCS3270DB.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintSCS3270DB.java new file mode 100644 index 0000000..960f87f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrintSCS3270DB.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.PrintSCS3270DB. + */ +public class PrintSCS3270DB extends haus.nightmare.lib3270j.printer.PrintSCS3270DB { + + public PrintSCS3270DB(PrinterConfig config) { + super(config); + } + + public PrintSCS3270DB(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrinterDefinitionTable.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrinterDefinitionTable.java new file mode 100644 index 0000000..c1de9ae --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/PrinterDefinitionTable.java @@ -0,0 +1,11 @@ +package haus.nightmare.lib3270j.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.PrinterDefinitionTable. + */ +public class PrinterDefinitionTable extends haus.nightmare.lib3270j.printer.PrinterDefinitionTable { + + public PrinterDefinitionTable(String name, String description) { + super(name, description); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/Telnet3270EP.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/Telnet3270EP.java new file mode 100644 index 0000000..b0ee76f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/Telnet3270EP.java @@ -0,0 +1,19 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.printer.PD3270; +import haus.nightmare.lib3270j.printer.PrinterConfig; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.Telnet3270EP. + */ +public class Telnet3270EP extends haus.nightmare.lib3270j.printer.Telnet3270EP { + + public Telnet3270EP(PrinterConfig config) { + super(config); + } + + public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { + super(config, pd, translator); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/Timer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/Timer.java new file mode 100644 index 0000000..9f992c5 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/Timer.java @@ -0,0 +1,29 @@ +package haus.nightmare.lib3270j.tn3270p; + +import haus.nightmare.lib3270j.printer.TimerListener; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.Timer. + */ +public class Timer extends haus.nightmare.lib3270j.printer.Timer { + + public Timer() { + super(); + } + + public Timer(long intervalMs) { + super(intervalMs); + } + + public Timer(long intervalMs, TimerListener listener) { + super(intervalMs, listener); + } + + public Timer(long intervalMs, TimerListener listener, boolean repeating) { + super(intervalMs, listener, repeating); + } + + public Timer(long intervalMs, TimerListener listener, boolean repeating, String timerId) { + super(intervalMs, listener, repeating, timerId); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/TimerEvent.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/TimerEvent.java new file mode 100644 index 0000000..6299aab --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/TimerEvent.java @@ -0,0 +1,21 @@ +package haus.nightmare.lib3270j.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.TimerEvent. + */ +public class TimerEvent extends haus.nightmare.lib3270j.printer.TimerEvent { + + private static final long serialVersionUID = 1L; + + public TimerEvent(Object source) { + super(source); + } + + public TimerEvent(Object source, String timerId) { + super(source, timerId); + } + + public TimerEvent(Object source, String timerId, long timestamp) { + super(source, timerId, timestamp); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/TimerListener.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/TimerListener.java new file mode 100644 index 0000000..62bfdc7 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/tn3270p/TimerListener.java @@ -0,0 +1,7 @@ +package haus.nightmare.lib3270j.tn3270p; + +/** + * Drop-in IBM Host On-Demand compatible facade for tn3270p.TimerListener. + */ +public interface TimerListener extends haus.nightmare.lib3270j.printer.TimerListener { +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferFileObject.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferFileObject.java new file mode 100644 index 0000000..0c325c0 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferFileObject.java @@ -0,0 +1,273 @@ +package haus.nightmare.lib3270j.xfer; + +import haus.nightmare.lib3270j.ft.FTConfig; +import haus.nightmare.lib3270j.ft.FTConfig.AllocationUnit; +import haus.nightmare.lib3270j.ft.FTConfig.CrAction; +import haus.nightmare.lib3270j.ft.FTConfig.ExistAction; +import haus.nightmare.lib3270j.ft.FTConfig.HostType; +import haus.nightmare.lib3270j.ft.FTConfig.RecordFormat; +import haus.nightmare.lib3270j.ft.FTConfig.TransferMode; + +import java.io.Serializable; + +/** + * Encapsulates file details, host dataset allocation, and transfer parameters. + * Conforms to IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.FileTransferFileObject) + * while providing comprehensive mainframe dataset attribute configuration. + */ +public class FileTransferFileObject implements Serializable { + + private static final long serialVersionUID = 1L; + + // Standard IBM HoD attributes + private String name = ""; + private long size = 0L; + private boolean isDirectory = false; + + // Extended mainframe dataset / transfer parameters + private String localFile = ""; + private String hostDatasetName = ""; + private HostType hostType = HostType.TSO; + private TransferMode transferMode = TransferMode.ASCII; + private CrAction crAction = CrAction.REMOVE; + private ExistAction existAction = ExistAction.KEEP; + private RecordFormat recfm = RecordFormat.DEFAULT; + private AllocationUnit spaceUnits = AllocationUnit.DEFAULT; + private int lrecl = 0; + private int blksize = 0; + private int primarySpace = 0; + private int secondarySpace = 0; + private int avblock = 0; + private String options = ""; + + public FileTransferFileObject() {} + + public FileTransferFileObject(String name) { + this(name, 0L, false); + } + + public FileTransferFileObject(String name, long size) { + this(name, size, false); + } + + public FileTransferFileObject(String name, long size, boolean isDirectory) { + this.name = name != null ? name : ""; + this.size = size; + this.isDirectory = isDirectory; + this.localFile = this.name; + this.hostDatasetName = this.name; + } + + public FileTransferFileObject(String localFile, String hostDatasetName) { + this(hostDatasetName != null ? hostDatasetName : localFile); + this.localFile = localFile != null ? localFile : ""; + this.hostDatasetName = hostDatasetName != null ? hostDatasetName : ""; + } + + public FileTransferFileObject(FTConfig config) { + fromFTConfig(config); + } + + // ========== Standard IBM HoD Getters / Setters ========== + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name != null ? name : ""; + if (localFile.isEmpty()) localFile = this.name; + if (hostDatasetName.isEmpty()) hostDatasetName = this.name; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public boolean isDirectory() { + return isDirectory; + } + + public void setIsDirectory(boolean isDirectory) { + this.isDirectory = isDirectory; + } + + public void setDirectory(boolean isDirectory) { + this.isDirectory = isDirectory; + } + + // ========== Mainframe Dataset & Transfer Parameters ========== + + public String getLocalFile() { + return localFile; + } + + public void setLocalFile(String localFile) { + this.localFile = localFile != null ? localFile : ""; + } + + public String getHostDatasetName() { + return hostDatasetName; + } + + public void setHostDatasetName(String hostDatasetName) { + this.hostDatasetName = hostDatasetName != null ? hostDatasetName : ""; + if (name == null || name.isEmpty()) { + this.name = this.hostDatasetName; + } + } + + public HostType getHostType() { + return hostType; + } + + public void setHostType(HostType hostType) { + if (hostType != null) this.hostType = hostType; + } + + public TransferMode getTransferMode() { + return transferMode; + } + + public void setTransferMode(TransferMode transferMode) { + if (transferMode != null) this.transferMode = transferMode; + } + + public CrAction getCrAction() { + return crAction; + } + + public void setCrAction(CrAction crAction) { + if (crAction != null) this.crAction = crAction; + } + + public ExistAction getExistAction() { + return existAction; + } + + public void setExistAction(ExistAction existAction) { + if (existAction != null) this.existAction = existAction; + } + + public RecordFormat getRecfm() { + return recfm; + } + + public void setRecfm(RecordFormat recfm) { + if (recfm != null) this.recfm = recfm; + } + + public AllocationUnit getSpaceUnits() { + return spaceUnits; + } + + public void setSpaceUnits(AllocationUnit spaceUnits) { + if (spaceUnits != null) this.spaceUnits = spaceUnits; + } + + public int getLrecl() { + return lrecl; + } + + public void setLrecl(int lrecl) { + this.lrecl = lrecl; + } + + public int getBlksize() { + return blksize; + } + + public void setBlksize(int blksize) { + this.blksize = blksize; + } + + public int getPrimarySpace() { + return primarySpace; + } + + public void setPrimarySpace(int primarySpace) { + this.primarySpace = primarySpace; + } + + public int getSecondarySpace() { + return secondarySpace; + } + + public void setSecondarySpace(int secondarySpace) { + this.secondarySpace = secondarySpace; + } + + public int getAvblock() { + return avblock; + } + + public void setAvblock(int avblock) { + this.avblock = avblock; + } + + public String getOptions() { + return options; + } + + public void setOptions(String options) { + this.options = options != null ? options : ""; + } + + // ========== Conversion to / from FTConfig ========== + + public FTConfig toFTConfig() { + FTConfig config = new FTConfig(); + if (hostDatasetName != null && !hostDatasetName.isEmpty()) { + config.setHostFilename(hostDatasetName); + } else { + config.setHostFilename(name); + } + if (localFile != null && !localFile.isEmpty()) { + config.setLocalFilename(localFile); + } else { + config.setLocalFilename(name); + } + config.setHostType(hostType); + config.setTransferMode(transferMode); + config.setCrAction(crAction); + config.setExistAction(existAction); + config.setRecfm(recfm); + config.setUnits(spaceUnits); + config.setLrecl(lrecl); + config.setBlksize(blksize); + config.setPrimarySpace(primarySpace); + config.setSecondarySpace(secondarySpace); + config.setAvblock(avblock); + if (options != null && !options.isEmpty()) { + config.parseOptions(options); + } + return config; + } + + public void fromFTConfig(FTConfig config) { + if (config == null) return; + this.hostDatasetName = config.getHostFilename() != null ? config.getHostFilename() : ""; + this.localFile = config.getLocalFilename() != null ? config.getLocalFilename() : ""; + this.name = !hostDatasetName.isEmpty() ? hostDatasetName : localFile; + this.hostType = config.getHostType(); + this.transferMode = config.getTransferMode(); + this.crAction = config.getCrAction(); + this.existAction = config.getExistAction(); + this.recfm = config.getRecfm(); + this.spaceUnits = config.getUnits(); + this.lrecl = config.getLrecl(); + this.blksize = config.getBlksize(); + this.primarySpace = config.getPrimarySpace(); + this.secondarySpace = config.getSecondarySpace(); + this.avblock = config.getAvblock(); + } + + @Override + public String toString() { + return this.name + ":" + this.size + ":" + this.isDirectory; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferHostDirectoryInterface.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferHostDirectoryInterface.java new file mode 100644 index 0000000..22cbe53 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferHostDirectoryInterface.java @@ -0,0 +1,35 @@ +package haus.nightmare.lib3270j.xfer; + +import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry; +import java.util.List; + +/** + * Interface for host directory listing operations and status indicators. + * Compatible with IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.FileTransferHostDirectoryInterface). + */ +public interface FileTransferHostDirectoryInterface { + + public static final int STATUS_OK = 0; + public static final int STATUS_EMPTY = 1; + public static final int STATUS_WAITING = 2; + public static final int STATUS_RECEIVING = 3; + public static final int STATUS_PROCESSING = 4; + + /** + * Set the current status of the directory retrieval operation. + * @param status One of the STATUS_* constants + */ + void setStatus(int status); + + /** + * Callback with parsed host directory entries when query succeeds. + * @param entries List of parsed directory entries + */ + default void onDirectoryLoaded(List entries) {} + + /** + * Callback when directory query fails. + * @param errorMessage Description of failure + */ + default void onDirectoryError(String errorMessage) {} +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferInterface.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferInterface.java new file mode 100644 index 0000000..5faa216 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferInterface.java @@ -0,0 +1,116 @@ +package haus.nightmare.lib3270j.xfer; + +import haus.nightmare.lib3270j.charset.CodePage; +import haus.nightmare.lib3270j.ecl.ECLErr; +import java.util.Vector; + +/** + * Standard File Transfer Interface conforming to IBM Host On-Demand specification + * (com.ibm.eNetwork.ECL.xfer.FileTransferInterface). + */ +public interface FileTransferInterface { + + // Host Operating System Types + public static final int VM_CMS = 0; + public static final int MVS_TSO = 1; + public static final int CICS = 2; + public static final int OS400 = 3; + + // Transfer Modes + public static final int ASCII = 0; + public static final int BINARY = 1; + + // Directions + public static final int GET = 0; + public static final int PUT = 1; + + /** + * Query files from host directory asynchronously. + * + * @param hostQuery Host query or filter pattern + * @param hostType Host system type (VM_CMS, MVS_TSO, CICS, OS400) + * @param timeout Timeout in seconds + * @param localFiles Vector populated with local target filenames + * @param hostFiles Vector populated with host filenames + * @param callback Callback receiving status and directory entries + * @return Vector containing parsed file entries or names + * @throws ECLErr on error + */ + @SuppressWarnings("rawtypes") + Vector getFiles(String hostQuery, int hostType, int timeout, + Vector localFiles, Vector hostFiles, + FileTransferHostDirectoryInterface callback) throws ECLErr; + + /** + * Format a host dataset or file name according to target host operating system conventions. + * + * @param filename Input file or dataset name + * @param hostType Target host system type (VM_CMS, MVS_TSO, CICS) + * @return Formatted host file name + */ + String getHostFileName(String filename, int hostType); + + /** + * Format a local PC filename from a host file or dataset name. + * + * @param filename Host file or dataset name + * @param hostType Host system type + * @return Formatted local file name + */ + String getLocalFileName(String filename, int hostType); + + /** + * Upload a local file to the host. + * + * @param localFile Path to local source file + * @param hostType Host system type + * @param hostFile Host target dataset/filename + * @param transferMode Transfer mode (ASCII or BINARY) + * @param status Status and progress listener + * @param options Additional options (CRLF, RECFM, LRECL, etc.) + * @throws ECLErr on transfer failure + */ + void putFile(String localFile, int hostType, String hostFile, int transferMode, + FileTransferStatusInterface status, String options) throws ECLErr; + + /** + * Download a file from the host. + * + * @param hostFile Host source dataset/filename + * @param hostType Host system type + * @param localFile Path to local destination file + * @param transferMode Transfer mode (ASCII or BINARY) + * @param status Status and progress listener + * @param options Additional options (CRLF, APPEND, REPLACE, etc.) + * @throws ECLErr on transfer failure + */ + void getFile(String hostFile, int hostType, String localFile, int transferMode, + FileTransferStatusInterface status, String options) throws ECLErr; + + /** + * Return human-readable error description for an exception. + * + * @param ex The encountered exception + * @return Error message string + */ + String getErrorMessage(Exception ex); + + /** + * Cancel currently active file transfer. + */ + void cancelTransfer(); + + /** + * Set the character code page used for host-PC translation. + * + * @param codePage CodePage mapping instance + */ + void setCodePage(CodePage codePage); + + /** + * Set the character code page by name or identifier. + * + * @param codePageName Name or CPGID (e.g. "037", "1047") + */ + default void setCodePage(String codePageName) {} +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferStatusInterface.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferStatusInterface.java new file mode 100644 index 0000000..8721c39 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferStatusInterface.java @@ -0,0 +1,49 @@ +package haus.nightmare.lib3270j.xfer; + +/** + * Interface for monitoring file transfer progress and lifecycle events. + * Compatible with IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.FileTransferStatusInterface). + */ +public interface FileTransferStatusInterface { + + /** + * Called at the beginning of a transfer to announce file details. + * @param fileName Name of the file being transferred + * @param fileSize Size of the file in bytes (or -1 if unknown) + */ + void setFileInfo(String fileName, long fileSize); + + /** + * Called when the transfer data phase starts. + */ + void startTransfer(); + + /** + * Called incrementally as bytes are transferred (IBM HoD naming). + * @param bytes Number of bytes transferred so far + */ + void bytesTransfered(long bytes); + + /** + * Convenience alias matching conventional spelling. + * @param bytes Number of bytes transferred so far + */ + default void bytesTransferred(long bytes) { + bytesTransfered(bytes); + } + + /** + * Called when the transfer completes successfully. + */ + void transferComplete(); + + /** + * Detailed progress callback providing bytes transferred, total bytes, and percentage. + * @param bytesTransferred Number of bytes transferred so far + * @param totalBytes Total expected bytes (or 0 if unknown) + * @param percent Progress percentage (0-100, or -1 if unknown) + */ + default void onProgress(long bytesTransferred, long totalBytes, int percent) { + bytesTransfered(bytesTransferred); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileInputStream.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileInputStream.java new file mode 100644 index 0000000..6f1a0b9 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileInputStream.java @@ -0,0 +1,153 @@ +package haus.nightmare.lib3270j.xfer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * File input stream with host record delimiter handling conforming to + * IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.XferFileInputStream). + */ +public class XferFileInputStream extends FileInputStream { + + protected int state = 0; + protected boolean bNonIbmText = false; + protected byte[] nonIbmTerminator = null; + protected boolean bASCIItypeTransfer = false; + protected boolean bDelimitCRLF = false; + protected boolean bFilterRead = false; + protected boolean bPreviousRecordFull = false; + protected int terminatorBytes = 0; + protected int bytesReturned = 0; + protected int bytesProcessed = 0; + + public XferFileInputStream(String name, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(name); + this.bNonIbmText = nonIbmText; + this.nonIbmTerminator = nonIbmTerminator; + this.bASCIItypeTransfer = asciiTransfer; + this.state = 0; + this.bDelimitCRLF = false; + this.bFilterRead = this.bNonIbmText && this.bASCIItypeTransfer; + } + + public XferFileInputStream(File file, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(file); + this.bNonIbmText = nonIbmText; + this.nonIbmTerminator = nonIbmTerminator; + this.bASCIItypeTransfer = asciiTransfer; + this.state = 0; + this.bDelimitCRLF = false; + this.bFilterRead = this.bNonIbmText && this.bASCIItypeTransfer; + } + + public XferFileInputStream(String name, byte[] terminators) throws FileNotFoundException { + super(name); + this.state = 0; + setTerminators(terminators); + } + + public XferFileInputStream(File file, byte[] terminators) throws FileNotFoundException { + super(file); + this.state = 0; + setTerminators(terminators); + } + + public void setTerminators(byte[] terminators) { + this.nonIbmTerminator = terminators; + if (terminators != null) { + this.bDelimitCRLF = true; + this.bFilterRead = true; + } else { + this.bDelimitCRLF = false; + this.bFilterRead = false; + } + } + + public int readData(byte[] b, int off, int len) throws IOException { + this.bytesReturned = 0; + if (!this.bDelimitCRLF && !this.bFilterRead) { + this.bytesReturned = super.read(b, off, len); + return this.bytesReturned; + } + + int maxLen = len - (this.nonIbmTerminator != null ? this.nonIbmTerminator.length : 0); + int destIndex = off; + boolean delim = false; + this.terminatorBytes = 0; + + while (this.bytesReturned < maxLen) { + int byteVal = this.read(); + if (byteVal == -1) { + break; + } + byte by = (byte) byteVal; + + switch (this.state) { + case 0: + if (this.nonIbmTerminator != null && by == this.nonIbmTerminator[0]) { + if (this.nonIbmTerminator.length == 1) { + if (this.bDelimitCRLF) { + this.terminatorBytes++; + if (!this.bPreviousRecordFull || this.bytesReturned != 0) { + delim = true; + } + this.bPreviousRecordFull = false; + break; + } + b[destIndex++] = 13; + b[destIndex++] = 10; + this.state = 0; + this.bytesReturned += 2; + break; + } + this.state = 1; + break; + } + b[destIndex++] = by; + this.bytesReturned++; + break; + + case 1: + this.state = 0; + if (this.bDelimitCRLF) { + this.terminatorBytes += 2; + if (!this.bPreviousRecordFull || this.bytesReturned != 0) { + delim = true; + } + this.bPreviousRecordFull = false; + break; + } + if (this.nonIbmTerminator != null && this.nonIbmTerminator.length > 1 && by == this.nonIbmTerminator[1]) { + b[destIndex++] = 13; + b[destIndex++] = 10; + this.bytesReturned += 2; + break; + } + if (this.nonIbmTerminator != null && this.nonIbmTerminator.length > 0) { + b[destIndex++] = this.nonIbmTerminator[0]; + } + b[destIndex++] = by; + this.bytesReturned += 2; + break; + } + + if (delim) { + return this.bytesReturned; + } + } + + if (this.bytesReturned == maxLen) { + this.bPreviousRecordFull = true; + } + return this.bytesReturned; + } + + public int getProcessedBytes() { + if (this.bDelimitCRLF) { + return this.bytesReturned + this.terminatorBytes; + } + return this.bytesReturned; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileInputUnicode.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileInputUnicode.java new file mode 100644 index 0000000..6d6bcc5 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileInputUnicode.java @@ -0,0 +1,134 @@ +package haus.nightmare.lib3270j.xfer; + +import haus.nightmare.lib3270j.charset.CodePage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +/** + * File input stream translating local Unicode files (UTF-8 or UCS-2) into host byte records + * conforming to IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.XferFileInputUnicode). + */ +public class XferFileInputUnicode extends XferFileInputStream { + + private static final char BOM_CHAR = '\ufeff'; + + private CodePage cp; + private InputStreamReader reader; + private int unicodeType = XferUnicodeConverter.UNICODE_UCS2; + private String encoding = "UTF-16LE"; + private boolean bDBCS = false; + private boolean bNOSO = false; + private boolean bFirstRead = true; + private boolean bEndOfFile = false; + + public XferFileInputUnicode(String filename, byte[] terminators, CodePage cp, + int unicodeType, int sessionType, boolean noso) + throws FileNotFoundException, UnsupportedEncodingException { + super(filename, terminators); + init(cp, unicodeType, noso); + } + + public XferFileInputUnicode(File file, byte[] terminators, CodePage cp, + int unicodeType, int sessionType, boolean noso) + throws FileNotFoundException, UnsupportedEncodingException { + super(file, terminators); + init(cp, unicodeType, noso); + } + + private void init(CodePage codePage, int type, boolean noso) throws UnsupportedEncodingException { + this.cp = codePage; + this.unicodeType = type; + this.bNOSO = noso; + if (this.cp != null) { + this.bDBCS = this.cp.isDBCS(); + } + if (this.unicodeType == XferUnicodeConverter.UNICODE_UTF8) { + this.encoding = "UTF-8"; + } else { + this.encoding = "UTF-16LE"; + } + this.reader = new InputStreamReader(this, this.encoding); + this.bFirstRead = true; + } + + @Override + public int readData(byte[] b, int off, int len) throws IOException { + this.bytesReturned = 0; + this.bytesProcessed = 0; + if (bEndOfFile) return -1; + + char[] charBuf = new char[len]; + int charCount = 0; + + while (charCount < len) { + int ch = reader.read(); + if (ch == -1) { + bEndOfFile = true; + break; + } + char c = (char) ch; + + // Strip initial Unicode BOM + if (bFirstRead && c == BOM_CHAR) { + bFirstRead = false; + continue; + } + bFirstRead = false; + + // Record delimiter handling + if (bDelimitCRLF && nonIbmTerminator != null) { + if (nonIbmTerminator.length == 1 && c == (char) nonIbmTerminator[0]) { + break; + } else if (nonIbmTerminator.length > 1 && c == (char) nonIbmTerminator[0]) { + int next = reader.read(); + if (next == -1 || next == (char) nonIbmTerminator[1]) { + break; + } + charBuf[charCount++] = c; + c = (char) next; + } + } + + charBuf[charCount++] = c; + } + + if (charCount == 0 && bEndOfFile) { + return -1; + } + + // Convert read chars to host bytes + byte[] converted; + if (cp != null) { + converted = cp.convBuffChar2Byte(charBuf, 0, charCount); + } else { + converted = new String(charBuf, 0, charCount).getBytes(StandardCharsets.ISO_8859_1); + } + + int toCopy = Math.min(converted.length, len); + System.arraycopy(converted, 0, b, off, toCopy); + this.bytesReturned = toCopy; + this.bytesProcessed = toCopy; + return toCopy; + } + + private boolean bFileClosed = false; + + @Override + public void close() throws IOException { + if (!bFileClosed) { + bFileClosed = true; + try { + if (reader != null) { + reader.close(); + } + } catch (IOException ignored) {} + } else { + super.close(); + } + } +} + diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileOutputStream.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileOutputStream.java new file mode 100644 index 0000000..20a0118 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileOutputStream.java @@ -0,0 +1,101 @@ +package haus.nightmare.lib3270j.xfer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * File output stream with host carriage control and line delimiter translation + * conforming to IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.XferFileOutputStream). + */ +public class XferFileOutputStream extends FileOutputStream { + + protected int state = 0; + protected boolean bNonIbmText = false; + protected byte[] nonIbmTerminator = null; + protected boolean bASCIItypeTransfer = false; + + public XferFileOutputStream(String name, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(name); + this.bNonIbmText = nonIbmText; + this.nonIbmTerminator = nonIbmTerminator; + this.bASCIItypeTransfer = asciiTransfer; + this.state = 0; + } + + public XferFileOutputStream(File file, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(file); + this.bNonIbmText = nonIbmText; + this.nonIbmTerminator = nonIbmTerminator; + this.bASCIItypeTransfer = asciiTransfer; + this.state = 0; + } + + public XferFileOutputStream(String name, boolean append, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(name, append); + this.bNonIbmText = nonIbmText; + this.nonIbmTerminator = nonIbmTerminator; + this.bASCIItypeTransfer = asciiTransfer; + this.state = 0; + } + + public XferFileOutputStream(File file, boolean append, boolean nonIbmText, byte[] nonIbmTerminator, boolean asciiTransfer) throws FileNotFoundException { + super(file, append); + this.bNonIbmText = nonIbmText; + this.nonIbmTerminator = nonIbmTerminator; + this.bASCIItypeTransfer = asciiTransfer; + this.state = 0; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (!this.bNonIbmText || !this.bASCIItypeTransfer) { + super.write(b, off, len); + return; + } + + int remaining = len; + int idx = off; + while (remaining > 0) { + byte by = b[idx]; + remaining--; + switch (this.state) { + case 0: + if (by == 13) { + this.state = 1; + break; + } + if (this.bASCIItypeTransfer && by == 26) { + // Strip Ctrl+Z EOF + break; + } + super.write(by); + break; + + case 1: + this.state = 0; + if (by == 10) { + if (this.nonIbmTerminator != null && this.nonIbmTerminator.length > 0) { + super.write(this.nonIbmTerminator, 0, this.nonIbmTerminator.length); + } else { + super.write(10); + } + break; + } + super.write(13); + super.write(by); + break; + } + idx++; + } + } + + public void writeCRLF(byte[] terminators) throws IOException { + if (terminators != null && terminators.length > 0) { + super.write(terminators); + } else { + super.write(new byte[]{13, 10}); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileOutputUnicode.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileOutputUnicode.java new file mode 100644 index 0000000..6075ede --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferFileOutputUnicode.java @@ -0,0 +1,70 @@ +package haus.nightmare.lib3270j.xfer; + +import haus.nightmare.lib3270j.charset.CodePage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * File output stream translating host byte streams into Unicode files (UTF-8 or UCS-2) + * conforming to IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.XferFileOutputUnicode). + */ +public class XferFileOutputUnicode extends XferFileOutputStream { + + private static final byte[] BOM_UTF8 = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; + private static final byte[] BOM_UCS2_LE = new byte[]{(byte) 0xFF, (byte) 0xFE}; + + private XferUnicodeConverter conv; + private int unicodeType = XferUnicodeConverter.UNICODE_UCS2; + private boolean bFirstWrite = true; + + public XferFileOutputUnicode(String filename, boolean append, byte[] terminators, + boolean asciiTransfer, CodePage cp, int unicodeType, + boolean soFlag, boolean soAlt) + throws FileNotFoundException, UnsupportedEncodingException { + super(filename, append, false, terminators, asciiTransfer); + this.unicodeType = unicodeType; + this.conv = new XferUnicodeConverter(false, terminators, asciiTransfer, cp, unicodeType, soFlag, soAlt); + } + + public XferFileOutputUnicode(File file, boolean append, byte[] terminators, + boolean asciiTransfer, CodePage cp, int unicodeType, + boolean soFlag, boolean soAlt) + throws FileNotFoundException, UnsupportedEncodingException { + super(file, append, false, terminators, asciiTransfer); + this.unicodeType = unicodeType; + this.conv = new XferUnicodeConverter(false, terminators, asciiTransfer, cp, unicodeType, soFlag, soAlt); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len <= 0) return; + + byte[] converted = conv.convert2Unicode(b, off, len); + if (bFirstWrite && converted.length > 0) { + writeBOM(); + bFirstWrite = false; + } + + super.write(converted, 0, converted.length); + } + + @Override + public void writeCRLF(byte[] terminators) throws IOException { + byte[] converted = conv.convertCRLF(terminators); + if (bFirstWrite && converted.length > 0) { + writeBOM(); + bFirstWrite = false; + } + super.write(converted, 0, converted.length); + } + + private void writeBOM() throws IOException { + if (unicodeType == XferUnicodeConverter.UNICODE_UTF8) { + super.write(BOM_UTF8, 0, BOM_UTF8.length); + } else { + super.write(BOM_UCS2_LE, 0, BOM_UCS2_LE.length); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferUnicodeConverter.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferUnicodeConverter.java new file mode 100644 index 0000000..84963b8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/XferUnicodeConverter.java @@ -0,0 +1,121 @@ +package haus.nightmare.lib3270j.xfer; + +import haus.nightmare.lib3270j.charset.CodePage; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; + +/** + * Converter between host EBCDIC data and Unicode (UTF-8 / UCS-2) streams + * conforming to IBM Host On-Demand (com.ibm.eNetwork.ECL.xfer.XferUnicodeConverter). + */ +public class XferUnicodeConverter { + + private static final Logger log = Logger.getLogger(XferUnicodeConverter.class.getName()); + + public static final int UNICODE_UCS2 = 0; + public static final int UNICODE_UTF8 = 1; + + private CodePage cp; + private int state = 0; + private boolean bNonIbmText = false; + private byte[] nonIbmTerminator = null; + private boolean bASCIItypeTransfer = false; + private boolean bSO = false; + private boolean bDBCS = false; + private String encoding = "UTF-16LE"; + private int unicodeType = 0; + private char so = (char) 30; + private char si = (char) 31; + private int charmode = 0; + private byte[] pendingDBCS = null; + + public XferUnicodeConverter(boolean nonIbmText, byte[] nonIbmTerminator, + boolean asciiTransfer, CodePage codePage, + int unicodeType, boolean soFlag, boolean soAlt) + throws UnsupportedEncodingException { + this.bNonIbmText = nonIbmText; + this.nonIbmTerminator = nonIbmTerminator; + this.bASCIItypeTransfer = asciiTransfer; + this.bSO = soFlag; + if (soAlt) { + this.so = (char) 14; + this.si = (char) 15; + } + this.state = 0; + this.cp = codePage; + this.unicodeType = unicodeType; + if (unicodeType == UNICODE_UTF8) { + this.encoding = "UTF-8"; + } else { + this.encoding = "UTF-16LE"; + } + this.bDBCS = (this.cp != null && this.cp.isDBCS()); + } + + public int getUnicodeType() { + return unicodeType; + } + + public String getEncoding() { + return encoding; + } + + /** + * Convert incoming host byte buffer to Unicode byte stream (UTF-8 or UTF-16LE). + */ + public byte[] convert2Unicode(byte[] hostData, int off, int len) { + if (hostData == null || len <= 0) return new byte[0]; + + char[] chars; + if (cp != null) { + chars = cp.convBuffByte2Char(hostData, off, len); + } else { + chars = new char[len]; + for (int i = 0; i < len; i++) { + chars[i] = (char) (hostData[off + i] & 0xFF); + } + } + + if (!this.bSO || !this.bDBCS) { + return new String(chars).getBytes(unicodeType == UNICODE_UTF8 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16LE); + } + + // Handle DBCS SO/SI insertion + StringBuilder sb = new StringBuilder(chars.length * 2); + for (char c : chars) { + boolean isDbcs = (cp != null && cp.unicodeToDbcs(c) != -1); + if (isDbcs) { + if (charmode == 0) { + sb.append(so); + charmode = 1; + } + sb.append(c); + } else { + if (charmode == 1) { + sb.append(si); + charmode = 0; + } + sb.append(c); + } + } + if (charmode == 1) { + sb.append(si); + charmode = 0; + } + + return sb.toString().getBytes(unicodeType == UNICODE_UTF8 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16LE); + } + + /** + * Convert CRLF bytes for output. + */ + public byte[] convertCRLF(byte[] crlfBytes) { + if (unicodeType == UNICODE_UTF8) { + return crlfBytes != null ? crlfBytes : new byte[]{13, 10}; + } + String s = new String(crlfBytes != null ? crlfBytes : new byte[]{13, 10}, StandardCharsets.ISO_8859_1); + return s.getBytes(StandardCharsets.UTF_16LE); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/CMSPrintXfer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/CMSPrintXfer.java new file mode 100644 index 0000000..19ff3cc --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/CMSPrintXfer.java @@ -0,0 +1,27 @@ +package haus.nightmare.lib3270j.xfer3270; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLXfer; + +/** + * VM/CMS Spool and Print File Transfer facility conforming to IBM Host On-Demand + * (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer). + */ +public class CMSPrintXfer extends haus.nightmare.lib3270j.ft.CMSPrintXfer { + + public CMSPrintXfer() { + super(); + } + + public CMSPrintXfer(ECLXfer xfer) { + super(xfer); + } + + public CMSPrintXfer(ECLXfer xfer, EbcdicTranslator translator) { + super(xfer, translator); + } + + public CMSPrintXfer(Xfer3270 xfer3270) { + super(xfer3270 != null ? xfer3270.getDelegateXfer() : null); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java new file mode 100644 index 0000000..215fa52 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java @@ -0,0 +1,594 @@ +package haus.nightmare.lib3270j.xfer3270; + +import haus.nightmare.lib3270j.charset.CodePage; +import haus.nightmare.lib3270j.charset.CodePageRegistry; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.ecl.*; +import haus.nightmare.lib3270j.ft.FTConfig; +import haus.nightmare.lib3270j.ft.FTConstants; +import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry; +import haus.nightmare.lib3270j.ft.dir.CMSDirectoryParser; +import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry; +import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.xfer.FileTransferHostDirectoryInterface; +import haus.nightmare.lib3270j.xfer.FileTransferInterface; +import haus.nightmare.lib3270j.xfer.FileTransferStatusInterface; + +import java.net.URL; +import java.util.List; +import java.util.StringTokenizer; +import java.util.Vector; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Core 3270 File Transfer Controller conforming to IBM Host On-Demand + * (com.ibm.eNetwork.ECL.xfer3270.Xfer3270). + * + * Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options, + * dynamic MTU buffering, host/local dataset name mappings, directory queries, + * and Unicode transfer modes. + */ +public class Xfer3270 implements FileTransferInterface { + + private static final Logger log = Logger.getLogger(Xfer3270.class.getName()); + + public static final String UNICODE_UCS2_STR = "UCS2"; + public static final String UNICODE_UTF8_STR = "UTF8"; + public static final String UNICODE_UTF_8_STR = "UTF-8"; + public static final int UNICODE_UCS2 = 0; + public static final int UNICODE_UTF8 = 1; + public static final String HOST_FILE_ERROR = "HOST_FILE_ERROR"; + public static final int MIN_MTU_SIZE = 256; + public static final int MAX_MTU_SIZE = 32767; + public static final int DefaultUploadBufferSize = 2048; + + // Session references + private ECLSession session; + private ScreenBuffer screen; + private InputProcessor input; + private DataStreamProcessor dsProcessor; + private EbcdicTranslator translator; + private CodePage currentCodePage; + private ECLXfer delegateXfer; + + // State and Configuration + private int bmtuSize = 2500; + public int TimeOutValue = 30; + private long bytesTransferred = 0L; + private String sReasonTerminated = ""; + private boolean bSendClear = true; + private boolean bCancelRequested = false; + + // HoD Option Flags + public boolean option_ASCII = false; + public boolean option_SO = false; + public boolean option_NOSO = false; + public boolean option_BLANK = false; + public boolean option_USER = false; + public boolean option_APPEND = false; + public boolean option_NEW = false; + public boolean option_CRLF = false; + public boolean option_UNICODE = false; + public int unicodeType = UNICODE_UCS2; + + // Mainframe dataset allocation parameters + private String recfm = "DEFAULT"; + private int lrecl = 0; + private int blksize = 0; + private int primarySpace = 0; + private int secondarySpace = 0; + private String spaceUnits = "DEFAULT"; + private int avblock = 0; + + // Constructors + public Xfer3270() { + this.translator = new EbcdicTranslator(); + this.currentCodePage = CodePageRegistry.getDefault(); + } + + public Xfer3270(ECLSession session) { + this(session, null); + } + + public Xfer3270(ECLSession session, URL url) { + this.session = session; + if (session != null) { + this.delegateXfer = session.GetXfer(); + } + this.translator = new EbcdicTranslator(); + this.currentCodePage = CodePageRegistry.getDefault(); + } + + public Xfer3270(ECLXfer xfer) { + this.delegateXfer = xfer; + this.translator = new EbcdicTranslator(); + this.currentCodePage = CodePageRegistry.getDefault(); + } + + public Xfer3270(ScreenBuffer screen, InputProcessor input, + DataStreamProcessor dsProcessor, CodePage codePage) { + this.screen = screen; + this.input = input; + this.dsProcessor = dsProcessor; + this.currentCodePage = codePage != null ? codePage : CodePageRegistry.getDefault(); + this.translator = new EbcdicTranslator(this.currentCodePage.getCodePageId()); + this.delegateXfer = new ECLXfer(screen, input, dsProcessor, translator); + } + + // ========== Option Flags Parser ========== + + /** + * Parses TSO and VM option flags into internal state, conforming to HoD syntax. + * @param options Option flags string + */ + public void setOptionFlags(String options) { + this.option_ASCII = false; + this.option_SO = false; + this.option_NOSO = false; + this.option_BLANK = false; + this.option_USER = false; + this.option_APPEND = false; + this.option_NEW = false; + this.option_CRLF = false; + this.option_UNICODE = false; + this.unicodeType = UNICODE_UCS2; + this.recfm = "DEFAULT"; + this.lrecl = 0; + this.blksize = 0; + this.primarySpace = 0; + this.secondarySpace = 0; + this.spaceUnits = "DEFAULT"; + this.avblock = 0; + + if (options == null || options.trim().isEmpty()) return; + String upper = options.toUpperCase(); + + if (upper.contains("ASCII")) this.option_ASCII = true; + if (upper.contains(" SO") || upper.contains("(SO")) this.option_SO = true; + if (upper.contains("NOSO")) this.option_NOSO = true; + if (upper.contains("BLANK")) this.option_BLANK = true; + if (upper.contains("USER")) this.option_USER = true; + if (upper.contains("APPEND")) this.option_APPEND = true; + if (upper.contains("NEW")) this.option_NEW = true; + if (upper.contains("CRLF") && !upper.contains("NOCRLF")) this.option_CRLF = true; + + // UNICODE options: UNICODE or UNICODE(UTF-8) / UNICODE(UCS2) + int uIdx = upper.indexOf("UNICODE"); + if (uIdx > -1) { + this.option_UNICODE = true; + int pStart = upper.indexOf('(', uIdx); + int pEnd = upper.indexOf(')', uIdx); + String uSub = ""; + if (pStart > uIdx && pEnd > pStart) { + uSub = upper.substring(pStart + 1, pEnd); + } + if (uSub.contains(UNICODE_UTF8_STR) || uSub.contains(UNICODE_UTF_8_STR)) { + this.unicodeType = UNICODE_UTF8; + } else { + this.unicodeType = UNICODE_UCS2; + } + } + + // Mainframe dataset parameters + Matcher recfmMatcher = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?").matcher(options); + if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase(); + + Matcher lreclMatcher = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?").matcher(options); + if (lreclMatcher.find()) { + try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {} + } + + Matcher blkMatcher = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?").matcher(options); + if (blkMatcher.find()) { + try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {} + } + + Matcher spaceMatcher = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?").matcher(options); + if (spaceMatcher.find()) { + try { + this.primarySpace = Integer.parseInt(spaceMatcher.group(1)); + if (spaceMatcher.group(2) != null) { + this.secondarySpace = Integer.parseInt(spaceMatcher.group(2)); + } + } catch (NumberFormatException ignored) {} + } + + Matcher avbMatcher = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?").matcher(options); + if (avbMatcher.find()) { + try { + this.avblock = Integer.parseInt(avbMatcher.group(1)); + this.spaceUnits = "AVBLOCK"; + } catch (NumberFormatException ignored) {} + } + + if (upper.contains("TRACKS") || upper.contains("TRK")) { + this.spaceUnits = "TRACKS"; + } else if (upper.contains("CYLINDERS") || upper.contains("CYL")) { + this.spaceUnits = "CYLINDERS"; + } + + Matcher mtuMatcher = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?").matcher(options); + if (mtuMatcher.find()) { + try { + SetMTUSize(Integer.parseInt(mtuMatcher.group(1))); + } catch (NumberFormatException ignored) {} + } + } + + // ========== File Name Formatting ========== + + @Override + public String getHostFileName(String string, int n) { + if (string == null) return n == VM_CMS ? "NONE NONE" : ""; + String trimmed = string.trim(); + if (trimmed.isEmpty()) { + return n == VM_CMS ? "NONE NONE" : ""; + } + String result = ""; + StringTokenizer st = new StringTokenizer(trimmed, ". \t"); + + switch (n) { + case VM_CMS: { // 0 + String fn = st.hasMoreTokens() ? st.nextToken() : ""; + String ft = st.hasMoreTokens() ? st.nextToken() : ""; + if (fn.length() > 8) fn = fn.substring(0, 8); + if (ft.length() > 8) ft = ft.substring(0, 8); + if (fn.isEmpty()) fn = "NONE"; + if (ft.isEmpty()) ft = "NONE"; + result = fn + " " + ft; + break; + } + case MVS_TSO: { // 1 + StringBuilder sb = new StringBuilder(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (token.length() > 8) token = token.substring(0, 8); + if (sb.length() > 0) sb.append("."); + sb.append(token); + } + result = sb.toString(); + break; + } + case CICS: { // 2 + String token = st.nextToken(); + result = token.length() > 8 ? token.substring(0, 8) : token; + break; + } + default: + result = string.trim(); + break; + } + return result; + } + + @Override + public String getLocalFileName(String string, int n) { + if (string == null || string.trim().isEmpty()) return ""; + String trimmed = string.trim(); + String result = ""; + + switch (n) { + case VM_CMS: { // 0 + StringTokenizer st = new StringTokenizer(trimmed, " "); + String fn = st.hasMoreTokens() ? st.nextToken() : "NONE"; + String ft = st.hasMoreTokens() ? st.nextToken() : "NONE"; + result = fn + "." + ft; + break; + } + case MVS_TSO: { // 1 + if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length() >= 2) { + result = trimmed.substring(1, trimmed.length() - 1); + } else { + result = trimmed; + } + break; + } + default: + result = trimmed; + break; + } + return result; + } + + // ========== State Inspection ========== + + public String stateIs(int n) { + switch (n) { + case 0: return "Idle"; + case 1: return "W4DirectoryList"; + case 2: return "W4GetFile"; + case 3: return "W4PutFile"; + case 12: return "W4Data"; + case 19: return "UploadComplete"; + case 20: return "DnloadComplete"; + case 21: return "Quiting"; + default: return "Unknown (" + n + ")"; + } + } + + public boolean isTransferActive() { + return delegateXfer != null && delegateXfer.isTransferActive(); + } + + public long getBytesTransferred() { + if (delegateXfer != null) return delegateXfer.getBytesTransferred(); + return bytesTransferred; + } + + // ========== Buffer / MTU Management ========== + + public void SetMTUSize(int n) { + if (n < MIN_MTU_SIZE) n = MIN_MTU_SIZE; + if (n > MAX_MTU_SIZE) n = MAX_MTU_SIZE; + this.bmtuSize = n; + if (delegateXfer != null) { + delegateXfer.setMTUSize(this.bmtuSize); + } + } + + public int GetMTUSize() { + if (delegateXfer != null) return delegateXfer.getMTUSize(); + return bmtuSize; + } + + public void setMTUSize(int n) { SetMTUSize(n); } + public int getMTUSize() { return GetMTUSize(); } + + public void setClear(boolean bl) { + this.bSendClear = bl; + } + + public void resendInboundDataBufferToHost() { + if (delegateXfer != null) { + delegateXfer.resendInboundDataBufferToHost(); + } + } + + // ========== File Transfer Operations ========== + + @Override + public void getFile(String hostFile, int hostType, String localFile, int transferMode, + FileTransferStatusInterface status, String options) throws ECLErr { + setOptionFlags(options); + if (transferMode == BINARY) { + this.option_ASCII = false; + } else { + this.option_ASCII = true; + } + + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.RECEIVE); + config.setLocalFilename(localFile); + config.setHostFilename(hostFile); + config.setHostType(hostType == VM_CMS ? FTConfig.HostType.CMS : + (hostType == CICS ? FTConfig.HostType.CICS : FTConfig.HostType.TSO)); + config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY); + config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP); + config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE); + config.setDftBufferSize(bmtuSize); + if (options != null && !options.isEmpty()) { + config.parseOptions(options); + } + + executeTransfer(config, status, localFile, hostFile); + } + + public void getFile(String command, String hostFile, String localFile, int mode, + String options, ECLXferListener listener) throws ECLErr { + FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile); + getFile(hostFile, MVS_TSO, localFile, mode, statusAdapter, options); + } + + @Override + public void putFile(String localFile, int hostType, String hostFile, int transferMode, + FileTransferStatusInterface status, String options) throws ECLErr { + setOptionFlags(options); + if (transferMode == BINARY) { + this.option_ASCII = false; + } else { + this.option_ASCII = true; + } + + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setLocalFilename(localFile); + config.setHostFilename(hostFile); + config.setHostType(hostType == VM_CMS ? FTConfig.HostType.CMS : + (hostType == CICS ? FTConfig.HostType.CICS : FTConfig.HostType.TSO)); + config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY); + config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP); + config.setRecfm(recfm); + config.setLrecl(String.valueOf(lrecl)); + config.setBlksize(String.valueOf(blksize)); + config.setDftBufferSize(bmtuSize); + if (primarySpace > 0) { + config.setSpace(primarySpace + (secondarySpace > 0 ? "," + secondarySpace : "")); + } + if ("TRACKS".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.TRACKS); + else if ("CYLINDERS".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.CYLINDERS); + else if ("AVBLOCK".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.AVBLOCK); + + if (options != null && !options.isEmpty()) { + config.parseOptions(options); + } + + executeTransfer(config, status, localFile, hostFile); + } + + public void putFile(String command, String hostFile, String localFile, int mode, + String options, ECLXferListener listener) throws ECLErr { + FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile); + putFile(localFile, MVS_TSO, hostFile, mode, statusAdapter, options); + } + + private void executeTransfer(FTConfig config, FileTransferStatusInterface status, + String locFile, String hstFile) throws ECLErr { + if (delegateXfer == null) { + throw new ECLErr("Xfer3270", "ECL0148", "Session or ECLXfer delegate not initialized"); + } + + if (status != null) { + java.io.File f = new java.io.File(locFile); + status.setFileInfo(locFile, f.exists() ? f.length() : -1L); + status.startTransfer(); + } + + ECLXferListener transferListener = new ECLXferListener() { + @Override + public void xferEvent(ECLXferEvent event) { + if (status == null) return; + long bytes = event.getBytesTransferred(); + bytesTransferred = bytes; + status.bytesTransfered(bytes); + long total = event.getTotalBytes(); + int pct = (total > 0) ? (int) ((bytes * 100) / total) : -1; + status.onProgress(bytes, total, pct); + + if (event.isCompleted()) { + status.transferComplete(); + } else if (event.isAborted()) { + sReasonTerminated = event.getMessage(); + } + } + }; + + delegateXfer.addXferListener(transferListener); + try { + int rc; + if (config.isReceive()) { + rc = delegateXfer.ReceiveFile(config.getLocalFilename(), config.getHostFilename(), config.buildCommand()); + } else { + rc = delegateXfer.SendFile(config.getLocalFilename(), config.getHostFilename(), config.buildCommand()); + } + if (rc != 0) { + throw new ECLErr("Xfer3270", "ECL0150", "Transfer initiation failed with code " + rc); + } + } finally { + delegateXfer.removeXferListener(transferListener); + } + } + + private FileTransferStatusInterface createStatusAdapter(ECLXferListener listener, String loc, String hst) { + if (listener == null) return null; + return new FileTransferStatusInterface() { + @Override + public void setFileInfo(String name, long size) { + listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_STARTED, 0, size, 0, "Transfer initiated", loc, hst)); + } + + @Override + public void startTransfer() { + listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_STARTED, 0, 0, 0, "Transfer started", loc, hst)); + } + + @Override + public void bytesTransfered(long bytes) { + listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_PROGRESS, bytes, 0, 0, bytes + " bytes transferred", loc, hst)); + } + + @Override + public void transferComplete() { + listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_COMPLETED, bytesTransferred, 0, 0, "Transfer complete", loc, hst)); + } + }; + } + + // ========== Directory Query Operations ========== + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public Vector getFiles(String hostQuery, int hostType, int timeout, + Vector localFiles, Vector hostFiles, + FileTransferHostDirectoryInterface callback) throws ECLErr { + if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_PROCESSING); + Vector result = new Vector(); + + try { + if (hostType == VM_CMS) { + if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING); + List cmsEntries = CMSDirectoryParser.parse(hostQuery); + for (CMSDirectoryEntry entry : cmsEntries) { + String hName = entry.getFilename() + " " + entry.getFiletype(); + String lName = entry.getFilename() + "." + entry.getFiletype(); + if (hostFiles != null) hostFiles.addElement(hName); + if (localFiles != null) localFiles.addElement(lName); + result.addElement(entry); + } + if (callback != null) { + callback.onDirectoryLoaded(cmsEntries); + callback.setStatus(cmsEntries.isEmpty() ? + FileTransferHostDirectoryInterface.STATUS_EMPTY : + FileTransferHostDirectoryInterface.STATUS_OK); + } + } else { + if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING); + List tsoEntries = TSODirectoryParser.parse(hostQuery); + for (TSODirectoryEntry entry : tsoEntries) { + String name = entry.getName(); + if (hostFiles != null) hostFiles.addElement(name); + if (localFiles != null) localFiles.addElement(name); + result.addElement(entry); + } + if (callback != null) { + callback.onDirectoryLoaded(tsoEntries); + callback.setStatus(tsoEntries.isEmpty() ? + FileTransferHostDirectoryInterface.STATUS_EMPTY : + FileTransferHostDirectoryInterface.STATUS_OK); + } + } + } catch (Exception e) { + if (callback != null) { + callback.onDirectoryError(e.getMessage()); + callback.setStatus(FileTransferHostDirectoryInterface.STATUS_EMPTY); + } + throw new ECLErr("Xfer3270", "ECL0149", "Directory retrieval failed: " + e.getMessage()); + } + + return result; + } + + @Override + public void cancelTransfer() { + this.bCancelRequested = true; + if (delegateXfer != null) { + delegateXfer.cancelTransfer(); + } + } + + @Override + public void setCodePage(CodePage codePage) { + this.currentCodePage = codePage; + if (delegateXfer != null && codePage != null) { + if (translator != null) { + translator.setCodePage(codePage.getCodePageId()); + } + } + } + + @Override + public void setCodePage(String codePageName) { + CodePage cp = CodePageRegistry.get(codePageName); + if (cp != null) { + setCodePage(cp); + } else if (translator != null) { + translator.setCodePage(codePageName); + } + } + + @Override + public String getErrorMessage(Exception ex) { + if (ex != null && ex.getMessage() != null && !ex.getMessage().isEmpty()) { + return ex.getMessage(); + } + return sReasonTerminated.isEmpty() ? "Unknown file transfer error" : sReasonTerminated; + } + + public ECLXfer getDelegateXfer() { + return delegateXfer; + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/converters/HODConvertersPhase8Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/converters/HODConvertersPhase8Test.java new file mode 100644 index 0000000..3dcbc37 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/converters/HODConvertersPhase8Test.java @@ -0,0 +1,475 @@ +package haus.nightmare.lib3270j.converters; + +import haus.nightmare.lib3270j.charset.CodePage; +import haus.nightmare.lib3270j.charset.CodePageRegistry; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Comprehensive verification for Phase 8: Codepage Converters & Adapter Bridge. + * Verifies dynamic resolution of all 275 HoD converter class names, SBCS and DBCS conversions, + * transparent Shift-In (0x0F) / Shift-Out (0x0E) transitions, and error handling. + */ +public class HODConvertersPhase8Test { + + private static final String[] HOD_275_CONVERTER_CLASSES = new String[]{ + "com.ibm.eNetwork.HOD.converters.ByteToChar8859_1", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp037", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp1047", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp1140", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp1146", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp1148", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp285", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp500", + "com.ibm.eNetwork.HOD.converters.ByteToCharCp924", + "com.ibm.eNetwork.HOD.converters.ByteToCharDBCS_ASCII", + "com.ibm.eNetwork.HOD.converters.ByteToCharDBCS_EBCDIC", + "com.ibm.eNetwork.HOD.converters.ByteToCharDBCS_EBCDIC_IBM", + "com.ibm.eNetwork.HOD.converters.ByteToCharSingleByte", + "com.ibm.eNetwork.HOD.converters.CharToByte8859_1", + "com.ibm.eNetwork.HOD.converters.CharToByteCp037", + "com.ibm.eNetwork.HOD.converters.CharToByteCp1008", + "com.ibm.eNetwork.HOD.converters.CharToByteCp1047", + "com.ibm.eNetwork.HOD.converters.CharToByteCp1140", + "com.ibm.eNetwork.HOD.converters.CharToByteCp1146", + "com.ibm.eNetwork.HOD.converters.CharToByteCp1148", + "com.ibm.eNetwork.HOD.converters.CharToByteCp285", + "com.ibm.eNetwork.HOD.converters.CharToByteCp500", + "com.ibm.eNetwork.HOD.converters.CharToByteCp924", + "com.ibm.eNetwork.HOD.converters.CharToByteDBCS_ASCII", + "com.ibm.eNetwork.HOD.converters.CharToByteDBCS_EBCDIC", + "com.ibm.eNetwork.HOD.converters.CharToByteDBCS_EBCDICN_IBM", + "com.ibm.eNetwork.HOD.converters.CharToByteSingleByte", + "com.ibm.eNetwork.HOD.converters.ConverterFT1047", + "com.ibm.eNetwork.HOD.converters.ConverterFT1140", + "com.ibm.eNetwork.HOD.converters.ConverterFT1146", + "com.ibm.eNetwork.HOD.converters.ConverterFT1148", + "com.ibm.eNetwork.HOD.converters.ConverterFT285", + "com.ibm.eNetwork.HOD.converters.ConverterFT500", + "com.ibm.eNetwork.HOD.converters.ConverterFT924", + "com.ibm.eNetwork.HOD.converters.ConverterVT1090", + "com.ibm.eNetwork.HOD.converters.ConverterVT1101", + "com.ibm.eNetwork.HOD.converters.ConverterVT437", + "com.ibm.eNetwork.HOD.converters.ConverterVT819", + "com.ibm.eNetwork.HOD.converters.ConverterVT850", + "com.ibm.eNetwork.HOD.converters.ConverterVT8585", + "com.ibm.eNetwork.HOD.converters.ConverterVT923", + "com.ibm.eNetwork.HOD.converters.HODByteToCharCp1046", + "com.ibm.eNetwork.HOD.converters.HODByteToCharCp437", + "com.ibm.eNetwork.HOD.converters.HODByteToCharCp850", + "com.ibm.eNetwork.HOD.converters.HODCharToByteCp1046", + "com.ibm.eNetwork.HOD.converters.HODCharToByteCp437", + "com.ibm.eNetwork.HOD.converters.HODCharToByteCp850", + "com.ibm.eNetwork.HOD.converters.ar.ConverterBIDIPrinter420", + "com.ibm.eNetwork.HOD.converters.ar.ConverterFT420", + "com.ibm.eNetwork.HOD.converters.ar.ConverterVT1089", + "com.ibm.eNetwork.HOD.converters.ar.ConverterVT449", + "com.ibm.eNetwork.HOD.converters.ar.HODByteToChar8859_6", + "com.ibm.eNetwork.HOD.converters.ar.HODByteToCharCp1256", + "com.ibm.eNetwork.HOD.converters.ar.HODByteToCharCp420", + "com.ibm.eNetwork.HOD.converters.ar.HODByteToCharCp864", + "com.ibm.eNetwork.HOD.converters.ar.HODCharToByte8859_6", + "com.ibm.eNetwork.HOD.converters.ar.HODCharToByteCp1256", + "com.ibm.eNetwork.HOD.converters.ar.HODCharToByteCp420", + "com.ibm.eNetwork.HOD.converters.ar.HODCharToByteCp864", + "com.ibm.eNetwork.HOD.converters.ce.ByteToCharCp1153", + "com.ibm.eNetwork.HOD.converters.ce.ByteToCharCp870", + "com.ibm.eNetwork.HOD.converters.ce.CharToByteCp1153", + "com.ibm.eNetwork.HOD.converters.ce.CharToByteCp870", + "com.ibm.eNetwork.HOD.converters.ce.ConverterFT1153", + "com.ibm.eNetwork.HOD.converters.ce.ConverterFT870", + "com.ibm.eNetwork.HOD.converters.ce.HODByteToChar8859_2", + "com.ibm.eNetwork.HOD.converters.ce.HODByteToCharCp852", + "com.ibm.eNetwork.HOD.converters.ce.HODCharToByte8859_2", + "com.ibm.eNetwork.HOD.converters.ce.HODCharToByteCp852", + "com.ibm.eNetwork.HOD.converters.gr.ByteToCharCp875", + "com.ibm.eNetwork.HOD.converters.gr.CharToByteCp875", + "com.ibm.eNetwork.HOD.converters.gr.ConverterFT875", + "com.ibm.eNetwork.HOD.converters.gr.ConverterVT813", + "com.ibm.eNetwork.HOD.converters.gr.ConverterVT8586", + "com.ibm.eNetwork.HOD.converters.gr.HODByteToChar8859_7", + "com.ibm.eNetwork.HOD.converters.gr.HODByteToCharCp869", + "com.ibm.eNetwork.HOD.converters.gr.HODCharToByte8859_7", + "com.ibm.eNetwork.HOD.converters.gr.HODCharToByteCp869", + "com.ibm.eNetwork.HOD.converters.he.ByteToCharCp424", + "com.ibm.eNetwork.HOD.converters.he.ByteToCharCp803", + "com.ibm.eNetwork.HOD.converters.he.ByteToCharCp856", + "com.ibm.eNetwork.HOD.converters.he.CharToByteCp424", + "com.ibm.eNetwork.HOD.converters.he.CharToByteCp803", + "com.ibm.eNetwork.HOD.converters.he.CharToByteCp856", + "com.ibm.eNetwork.HOD.converters.he.ConverterBIDIPrinter424", + "com.ibm.eNetwork.HOD.converters.he.ConverterBIDIPrinter803", + "com.ibm.eNetwork.HOD.converters.he.ConverterFT424", + "com.ibm.eNetwork.HOD.converters.he.ConverterFT803", + "com.ibm.eNetwork.HOD.converters.he.ConverterVT1134", + "com.ibm.eNetwork.HOD.converters.he.ConverterVT1349", + "com.ibm.eNetwork.HOD.converters.he.ConverterVT916", + "com.ibm.eNetwork.HOD.converters.he.HODByteToChar8859_8", + "com.ibm.eNetwork.HOD.converters.he.HODByteToCharCp1255", + "com.ibm.eNetwork.HOD.converters.he.HODByteToCharCp862", + "com.ibm.eNetwork.HOD.converters.he.HODCharToByte8859_8", + "com.ibm.eNetwork.HOD.converters.he.HODCharToByteCp1255", + "com.ibm.eNetwork.HOD.converters.he.HODCharToByteCp862", + "com.ibm.eNetwork.HOD.converters.hi.ByteToCharCp1137", + "com.ibm.eNetwork.HOD.converters.hi.CharToByteCp1137", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp1390", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp1390JIS2004", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp1399", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp1399JIS2004", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp290", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp930", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp939", + "com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp942", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp1390", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp1390JIS2004", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp1399", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp1399JIS2004", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp290", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp930", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp939", + "com.ibm.eNetwork.HOD.converters.ja.CharToByteCp942", + "com.ibm.eNetwork.HOD.converters.ja.ConverterFT1390", + "com.ibm.eNetwork.HOD.converters.ja.ConverterFT1399", + "com.ibm.eNetwork.HOD.converters.ja.ConverterFT290", + "com.ibm.eNetwork.HOD.converters.ja.ConverterFT930", + "com.ibm.eNetwork.HOD.converters.ja.ConverterFT939", + "com.ibm.eNetwork.HOD.converters.ja.PrtConverterJIS", + "com.ibm.eNetwork.HOD.converters.ko.ByteToCharCp1364", + "com.ibm.eNetwork.HOD.converters.ko.ByteToCharCp933", + "com.ibm.eNetwork.HOD.converters.ko.ByteToCharCp949", + "com.ibm.eNetwork.HOD.converters.ko.CharToByteCp1364", + "com.ibm.eNetwork.HOD.converters.ko.CharToByteCp933", + "com.ibm.eNetwork.HOD.converters.ko.CharToByteCp949", + "com.ibm.eNetwork.HOD.converters.ko.ConverterFT1364", + "com.ibm.eNetwork.HOD.converters.ko.ConverterFT933", + "com.ibm.eNetwork.HOD.converters.ko.PrtConverterKS25550", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp1141", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp1144", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp1145", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp1147", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp273", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp274", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp275", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp280", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp284", + "com.ibm.eNetwork.HOD.converters.onea.ByteToCharCp297", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp1141", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp1144", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp1145", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp1147", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp273", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp274", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp275", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp280", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp284", + "com.ibm.eNetwork.HOD.converters.onea.CharToByteCp297", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT1141", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT1144", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT1145", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT1147", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT273", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT274", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT275", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT280", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT284", + "com.ibm.eNetwork.HOD.converters.onea.ConverterFT297", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT1011", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT1012", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT1020", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT1021", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT1023", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT1104", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT220", + "com.ibm.eNetwork.HOD.converters.onea.ConverterVT860", + "com.ibm.eNetwork.HOD.converters.oneb.ByteToCharCp1142", + "com.ibm.eNetwork.HOD.converters.oneb.ByteToCharCp1143", + "com.ibm.eNetwork.HOD.converters.oneb.ByteToCharCp1149", + "com.ibm.eNetwork.HOD.converters.oneb.ByteToCharCp277", + "com.ibm.eNetwork.HOD.converters.oneb.ByteToCharCp278", + "com.ibm.eNetwork.HOD.converters.oneb.ByteToCharCp871", + "com.ibm.eNetwork.HOD.converters.oneb.CharToByteCp1142", + "com.ibm.eNetwork.HOD.converters.oneb.CharToByteCp1143", + "com.ibm.eNetwork.HOD.converters.oneb.CharToByteCp1149", + "com.ibm.eNetwork.HOD.converters.oneb.CharToByteCp277", + "com.ibm.eNetwork.HOD.converters.oneb.CharToByteCp278", + "com.ibm.eNetwork.HOD.converters.oneb.CharToByteCp871", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterFT1142", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterFT1143", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterFT1149", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterFT277", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterFT278", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterFT871", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterVT1102", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterVT1103", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterVT1105", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterVT1106", + "com.ibm.eNetwork.HOD.converters.oneb.ConverterVT865", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1025", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1112", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1122", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1123", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1154", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1156", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1157", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1158", + "com.ibm.eNetwork.HOD.converters.ru.ByteToCharCp1166", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1025", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1112", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1122", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1123", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1154", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1156", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1157", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1158", + "com.ibm.eNetwork.HOD.converters.ru.CharToByteCp1166", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1025", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1112", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1122", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1123", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1154", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1156", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1157", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1158", + "com.ibm.eNetwork.HOD.converters.ru.ConverterFT1166", + "com.ibm.eNetwork.HOD.converters.ru.HODByteToChar8859_5", + "com.ibm.eNetwork.HOD.converters.ru.HODByteToCharCp855", + "com.ibm.eNetwork.HOD.converters.ru.HODByteToCharCp866", + "com.ibm.eNetwork.HOD.converters.ru.HODCharToByte8859_5", + "com.ibm.eNetwork.HOD.converters.ru.HODCharToByteCp855", + "com.ibm.eNetwork.HOD.converters.ru.HODCharToByteCp866", + "com.ibm.eNetwork.HOD.converters.th.ByteToCharCp1160", + "com.ibm.eNetwork.HOD.converters.th.ByteToCharCp838", + "com.ibm.eNetwork.HOD.converters.th.ByteToCharCp874", + "com.ibm.eNetwork.HOD.converters.th.CharToByteCp1160", + "com.ibm.eNetwork.HOD.converters.th.CharToByteCp838", + "com.ibm.eNetwork.HOD.converters.th.CharToByteCp874", + "com.ibm.eNetwork.HOD.converters.th.ConverterFT1160", + "com.ibm.eNetwork.HOD.converters.th.ConverterFT838", + "com.ibm.eNetwork.HOD.converters.th.ConverterVT874", + "com.ibm.eNetwork.HOD.converters.tr.ByteToCharCp1026", + "com.ibm.eNetwork.HOD.converters.tr.ByteToCharCp1155", + "com.ibm.eNetwork.HOD.converters.tr.CharToByteCp1026", + "com.ibm.eNetwork.HOD.converters.tr.CharToByteCp1155", + "com.ibm.eNetwork.HOD.converters.tr.ConverterFT1026", + "com.ibm.eNetwork.HOD.converters.tr.ConverterFT1155", + "com.ibm.eNetwork.HOD.converters.tr.HODByteToChar8859_9", + "com.ibm.eNetwork.HOD.converters.tr.HODByteToCharCp857", + "com.ibm.eNetwork.HOD.converters.tr.HODCharToByte8859_9", + "com.ibm.eNetwork.HOD.converters.tr.HODCharToByteCp857", + "com.ibm.eNetwork.HOD.converters.utf.ByteToCharUTF8", + "com.ibm.eNetwork.HOD.converters.utf.CharToByteUTF8", + "com.ibm.eNetwork.HOD.converters.vt.ConverterJDKEncodings", + "com.ibm.eNetwork.HOD.converters.zh.ByteToCharCp1381", + "com.ibm.eNetwork.HOD.converters.zh.ByteToCharCp1388", + "com.ibm.eNetwork.HOD.converters.zh.ByteToCharCp935", + "com.ibm.eNetwork.HOD.converters.zh.CharToByteCp1381", + "com.ibm.eNetwork.HOD.converters.zh.CharToByteCp1388", + "com.ibm.eNetwork.HOD.converters.zh.CharToByteCp935", + "com.ibm.eNetwork.HOD.converters.zh.ConverterFT1388", + "com.ibm.eNetwork.HOD.converters.zh.ConverterFT935", + "com.ibm.eNetwork.HOD.converters.zh_TW.ByteToCharCp1371", + "com.ibm.eNetwork.HOD.converters.zh_TW.ByteToCharCp1379", + "com.ibm.eNetwork.HOD.converters.zh_TW.ByteToCharCp937", + "com.ibm.eNetwork.HOD.converters.zh_TW.ByteToCharCp937Macau", + "com.ibm.eNetwork.HOD.converters.zh_TW.ByteToCharCp948", + "com.ibm.eNetwork.HOD.converters.zh_TW.ByteToCharCp950", + "com.ibm.eNetwork.HOD.converters.zh_TW.ByteToCharCp964", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteCp1371", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteCp1379", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteCp937", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteCp937Macau", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteCp948", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteCp950", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteCp964", + "com.ibm.eNetwork.HOD.converters.zh_TW.CharToByteEUC", + "com.ibm.eNetwork.HOD.converters.zh_TW.ConverterFT1371", + "com.ibm.eNetwork.HOD.converters.zh_TW.ConverterFT1379", + "com.ibm.eNetwork.HOD.converters.zh_TW.ConverterFT937", + "com.ibm.eNetwork.HOD.converters.zh_TW.PrtConverterBIG5550", + "com.ibm.eNetwork.HOD.converters.zh_TW.PrtConverterCNS", + "com.ibm.eNetwork.HOD.converters.zh_TW.PrtConverterTCA", + }; + + @Test + public void testAll275HoDConverterClassNamesResolve() throws Exception { + assertEquals(275, HOD_275_CONVERTER_CLASSES.length, "Must cover exactly 275 HoD converter class names"); + + for (String className : HOD_275_CONVERTER_CLASSES) { + // 1. Test resolving via HODByteToCharConverter + HODByteToCharConverter btc = HODByteToCharConverter.getHODConverter(className); + assertNotNull(btc, "ByteToChar converter should not be null for " + className); + assertNotNull(btc.getCodePage(), "Underlying CodePage should not be null for " + className); + + // 2. Test resolving via HODCharToByteConverter + HODCharToByteConverter ctb = HODCharToByteConverter.getHODConverter(className); + assertNotNull(ctb, "CharToByte converter should not be null for " + className); + assertNotNull(ctb.getCodePage(), "Underlying CodePage should not be null for " + className); + + // 3. Test simple name without package qualifier + int lastDot = className.lastIndexOf('.'); + String simpleName = className.substring(lastDot + 1); + HODByteToCharConverter btcSimple = HODByteToCharConverter.getHODConverter(simpleName); + assertNotNull(btcSimple, "ByteToChar converter should resolve simple name: " + simpleName); + + // 4. Test CodePageRegistry query + assertTrue(CodePageRegistry.hasCodePage(className), "Registry should report hasCodePage for " + className); + assertTrue(CodePageRegistry.hasCodePage(simpleName), "Registry should report hasCodePage for " + simpleName); + } + } + + @Test + public void testSbcsRoundTripConversion() throws Exception { + String[] testEncodings = new String[]{"Cp037", "Cp1047", "Cp500", "Cp273", "Cp285", "Cp297", "Cp420", "Cp424", "Cp838", "Cp1025", "Cp1155"}; + String sampleText = "HELLO World 12345 !?"; + + for (String enc : testEncodings) { + HODCharToByteConverter ctb = HODCharToByteConverter.getHODConverter(enc); + HODByteToCharConverter btc = HODByteToCharConverter.getHODConverter(enc); + + byte[] bytes = ctb.hodConvertAll(sampleText.toCharArray()); + assertNotNull(bytes); + assertTrue(bytes.length > 0); + + char[] chars = btc.hodConvertAll(bytes); + assertNotNull(chars); + assertEquals(sampleText, new String(chars), "Round-trip conversion failed for " + enc); + } + } + + @Test + public void testDbcsTransparentShiftInOutTransitions() throws Exception { + // Test Japanese DBCS Cp930 + HODByteToCharConverter btc = HODByteToCharConverter.getHODConverter("Cp930"); + HODCharToByteConverter ctb = HODCharToByteConverter.getHODConverter("Cp930"); + + assertTrue(btc.getCodePage().isDBCS()); + assertTrue(ctb.getCodePage().isDBCS()); + + // Test ideographic space \u3000 (encoded as DBCS 0x4040 in EBCDIC) + String mixedText = "ABC\u3000XYZ"; + byte[] ebcdicBytes = ctb.hodConvertAll(mixedText.toCharArray()); + assertNotNull(ebcdicBytes); + + // Verify that SO (0x0E) and SI (0x0F) were injected into the byte stream + boolean hasSO = false; + boolean hasSI = false; + for (byte b : ebcdicBytes) { + if (b == 0x0E) hasSO = true; + if (b == 0x0F) hasSI = true; + } + assertTrue(hasSO, "CharToByte should automatically inject Shift-Out (0x0E) before DBCS characters"); + assertTrue(hasSI, "CharToByte should automatically inject Shift-In (0x0F) after DBCS characters"); + + // Decode back and verify exact text restoration + char[] decoded = btc.hodConvertAll(ebcdicBytes); + assertEquals(mixedText, new String(decoded), "DBCS roundtrip should restore original string without SO/SI in text"); + } + + @Test + public void testDbcsChunkedConversionAcrossBuffers() throws Exception { + HODByteToCharConverter btc = HODByteToCharConverter.getHODConverter("Cp930"); + + // Construct a stream: 'A' (0xC1), SO (0x0E), DBCS pair (0x40, 0x40 = \u3000), SI (0x0F), 'B' (0xC2) + // Split chunk 1 right between the 2 DBCS bytes: 0xC1, 0x0E, 0x40 + // Chunk 2: 0x40, 0x0F, 0xC2 + byte[] chunk1 = new byte[]{(byte) 0xC1, (byte) 0x0E, (byte) 0x40}; + byte[] chunk2 = new byte[]{(byte) 0x40, (byte) 0x0F, (byte) 0xC2}; + + char[] out1 = new char[10]; + char[] out2 = new char[10]; + + btc.reset(); + int n1 = btc.convert(chunk1, 0, chunk1.length, out1, 0, out1.length); + assertEquals(1, n1, "Chunk 1 should only produce 'A' because DBCS pair is split across chunks"); + assertEquals('A', out1[0]); + + int n2 = btc.convert(chunk2, 0, chunk2.length, out2, 0, out2.length); + assertEquals(2, n2, "Chunk 2 should complete '\u3000' and 'B'"); + assertEquals('\u3000', out2[0]); + assertEquals('B', out2[1]); + } + + @Test + public void testDbcsFlushAutoClosesDBCSShift() throws Exception { + HODCharToByteConverter ctb = HODCharToByteConverter.getHODConverter("Cp930"); + ctb.reset(); + + char[] in = new char[]{'\u3000'}; // Only DBCS character + byte[] out = new byte[10]; + int written = ctb.convert(in, 0, in.length, out, 0, out.length); + assertEquals(3, written, "Should emit SO + 2-byte DBCS"); + assertEquals(0x0E, out[0]); + assertEquals(0x40, out[1]); + assertEquals(0x40, out[2]); + + int flushed = ctb.flush(out, written, out.length); + assertEquals(1, flushed, "Flush should emit Shift-In (0x0F)"); + assertEquals(0x0F, out[written]); + } + + @Test + public void testSubstitutionAndErrorHandling() throws Exception { + HODByteToCharConverter btc = HODByteToCharConverter.getHODConverter("Cp037"); + + // Buffer overflow check + byte[] in = new byte[]{(byte) 0xC1, (byte) 0xC2, (byte) 0xC3}; + char[] smallOut = new char[2]; + assertThrows(HODCharConversionException.class, () -> { + btc.convert(in, 0, in.length, smallOut, 0, smallOut.length); + }); + + // Substitution mode check on CharToByte with unmappable character + HODCharToByteConverter ctb = HODCharToByteConverter.getHODConverter("Cp037"); + ctb.setSubstitutionMode(false); + assertThrows(HODCharConversionException.class, () -> { + ctb.hodConvertAll(new char[]{'\u9999'}); + }); + + ctb.setSubstitutionMode(true); + ctb.setSubstitutionBytes(new byte[]{(byte) 0x6F}); + byte[] substituted = ctb.hodConvertAll(new char[]{'\u9999'}); + assertEquals(1, substituted.length); + assertEquals(0x6F, substituted[0]); + } + + @Test + public void testUnsupportedCodepageExceptionThrown() { + assertThrows(HODUnsupportedCodepageException.class, () -> { + HODByteToCharConverter.getHODConverter("NonExistent_Codepage_12345"); + }); + + assertThrows(HODUnsupportedCodepageException.class, () -> { + HODCharToByteConverter.getHODConverter("NonExistent_Codepage_12345"); + }); + } + + @Test + public void testCodePageGetConverterHelpers() throws Exception { + CodePage cp037 = CodePageRegistry.getCodePage("037"); + HODByteToCharConverter btc = cp037.getByteToCharConverter(); + HODCharToByteConverter ctb = cp037.getCharToByteConverter(); + + assertNotNull(btc); + assertNotNull(ctb); + assertEquals("Cp037", btc.getCharacterEncoding()); + assertEquals("Cp037", ctb.getCharacterEncoding()); + + // Test static helpers on CodePage + assertTrue(CodePage.isSurrogate('\uD800', '\uDC00')); + assertTrue(CodePage.isHighSurrogate('\uD800')); + assertTrue(CodePage.isLowSurrogate('\uDC00')); + assertFalse(CodePage.isHighSurrogate('A')); + } + + @Test + public void testENetworkFacades() throws Exception { + haus.nightmare.lib3270j.converters.HODByteToCharConverter btc = + haus.nightmare.lib3270j.eNetwork.HOD.common.HODByteToCharConverter.getHODConverter("Cp037"); + assertNotNull(btc); + + haus.nightmare.lib3270j.converters.HODCharToByteConverter ctb = + haus.nightmare.lib3270j.eNetwork.HOD.common.HODCharToByteConverter.getHODConverter("Cp037"); + assertNotNull(ctb); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/Dynamic14BitAddressingTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/Dynamic14BitAddressingTest.java new file mode 100644 index 0000000..34e7a3f --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/Dynamic14BitAddressingTest.java @@ -0,0 +1,72 @@ +package haus.nightmare.lib3270j.datastream; + +import org.junit.jupiter.api.Test; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.protocol.DS3270Constants; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static org.junit.jupiter.api.Assertions.*; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +public class Dynamic14BitAddressingTest { + + private final EbcdicTranslator translator = new EbcdicTranslator("037"); + + @Test + public void test14BitAddressEncodingAndDecoding() { + int rows = 62; + int cols = 160; + int totalPositions = rows * cols; // 9920 + + // Test addresses > 4096 (which require 14-bit addressing) + int[] testAddresses = { 4096, 4097, 5000, 7500, 9000, totalPositions - 1 }; + + for (int addr : testAddresses) { + byte[] encoded = DS3270Constants.encodeAddress(addr, rows, cols); + assertNotNull(encoded); + assertEquals(2, encoded.length); + + // In 14-bit addressing, top 2 bits of the first byte must be 00 + assertEquals(0x00, encoded[0] & 0xC0, "Top 2 bits of 14-bit address byte 1 must be 00 for addr=" + addr); + + int decoded = DS3270Constants.decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF); + assertEquals(addr, decoded, "Decoded address must match original for addr=" + addr); + } + } + + @Test + public void testDataStreamProcessorWith14BitSbaOrders() { + ScreenBuffer screen = new ScreenBuffer(24, 80, 62, 160, translator); + DataStreamProcessor ds = new DataStreamProcessor(screen, translator); + + // Erase Write Alternate command to switch to 62x160 + // Record: [CMD_EWA, WCC, ORDER_SBA, b1, b2, 'H', 'E', 'L', 'L', 'O'] + int targetAddr = 50 * 160 + 20; // 8020 (> 4096) + byte[] sbaAddr = DS3270Constants.encodeAddress(targetAddr, 62, 160); + + byte[] record = new byte[] { + (byte) CMD_EWA, + (byte) 0xC3, // WCC + (byte) ORDER_SBA, + sbaAddr[0], + sbaAddr[1], + (byte) 0xC8, // 'H' + (byte) 0xC5, // 'E' + (byte) 0xD3, // 'L' + (byte) 0xD3, // 'L' + (byte) 0xD6 // 'O' + }; + + ds.processRecord(record, 0, record.length, true); + + assertTrue(screen.isScreenAlt()); + assertEquals(62, screen.getRows()); + assertEquals(160, screen.getCols()); + + // Verify characters written at target address 8020..8024 + assertEquals((byte) 0xC8, screen.getCell(targetAddr).ec); // 'H' + assertEquals((byte) 0xC5, screen.getCell(targetAddr + 1).ec); // 'E' + assertEquals((byte) 0xD3, screen.getCell(targetAddr + 2).ec); // 'L' + assertEquals((byte) 0xD3, screen.getCell(targetAddr + 3).ec); // 'L' + assertEquals((byte) 0xD6, screen.getCell(targetAddr + 4).ec); // 'O' + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DynamicQueryReplyTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DynamicQueryReplyTest.java new file mode 100644 index 0000000..99ca5b1 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DynamicQueryReplyTest.java @@ -0,0 +1,72 @@ +package haus.nightmare.lib3270j.datastream; + +import org.junit.jupiter.api.Test; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static org.junit.jupiter.api.Assertions.*; + +public class DynamicQueryReplyTest { + + private final EbcdicTranslator translator = new EbcdicTranslator("037"); + + @Test + public void testUsableAreaDynamicDimensions() { + ScreenBuffer screen = new ScreenBuffer(24, 80, 62, 160, translator); + QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen); + + byte[] usableArea = qrBuilder.buildUsableArea(); + assertNotNull(usableArea); + assertTrue(usableArea.length >= 19); + + // Byte 0: Flags (0x01 or 0x03) - 12/14-bit addressing flag + assertEquals(0x01, usableArea[0] & 0x01); + + // Usable width (cols): bytes 2-3 (big endian) + int width = ((usableArea[2] & 0xFF) << 8) | (usableArea[3] & 0xFF); + assertEquals(160, width); + + // Usable height (rows): bytes 4-5 (big endian) + int height = ((usableArea[4] & 0xFF) << 8) | (usableArea[5] & 0xFF); + assertEquals(62, height); + + // Buffer size: last 2 bytes (bytes 17-18) + int bufSize = ((usableArea[17] & 0xFF) << 8) | (usableArea[18] & 0xFF); + assertEquals(160 * 62, bufSize); + } + + @Test + public void testImplicitPartitionDynamicDimensions() { + ScreenBuffer screen = new ScreenBuffer(24, 80, 62, 160, translator); + QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen); + + byte[] impPart = qrBuilder.buildImplicitPartition(); + assertNotNull(impPart); + assertEquals(13, impPart.length); + + // Default screen size (SDP 1): 80x24 + int defCols = ((impPart[5] & 0xFF) << 8) | (impPart[6] & 0xFF); + int defRows = ((impPart[7] & 0xFF) << 8) | (impPart[8] & 0xFF); + assertEquals(80, defCols); + assertEquals(24, defRows); + + // Alternate screen size: 160x62 + int altCols = ((impPart[9] & 0xFF) << 8) | (impPart[10] & 0xFF); + int altRows = ((impPart[11] & 0xFF) << 8) | (impPart[12] & 0xFF); + assertEquals(160, altCols); + assertEquals(62, altRows); + } + + @Test + public void testAlphaPartitionsDynamicStorage() { + ScreenBuffer screen = new ScreenBuffer(24, 80, 62, 160, translator); + QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen); + + byte[] alphaPart = qrBuilder.buildAlphaPartitions(); + assertNotNull(alphaPart); + assertEquals(4, alphaPart.length); + + // Total partition storage: bytes 1-2 + int storage = ((alphaPart[1] & 0xFF) << 8) | (alphaPart[2] & 0xFF); + assertEquals(160 * 62, storage); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase4EclCoreTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase4EclCoreTest.java new file mode 100644 index 0000000..594fbfc --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase4EclCoreTest.java @@ -0,0 +1,519 @@ +package haus.nightmare.lib3270j.ecl; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.TerminalModel; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +import java.awt.Color; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Comprehensive test suite for Phase 4: Canonical Package Exposure & ECL Core API Expansion. + */ +public class Phase4EclCoreTest { + + private ScreenBuffer screen; + private InputProcessor inputProcessor; + private EbcdicTranslator translator; + private ECLPS ps; + + @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); + } + + // ========================================================================= + // 4.1 ECLSession Tests + // ========================================================================= + + @Test + public void testSessionMetadataAndProperties() { + ECLSession session = new ECLSession(); + session.setSessionName("B"); + assertEquals("B", session.getSessionName()); + assertEquals("B", session.GetSessionName()); + + session.setSessionLabel("Mainframe Prod"); + assertEquals("Mainframe Prod", session.getSessionLabel()); + assertEquals("Mainframe Prod", session.GetSessionLabel()); + + session.setAssociatedDeviceName("DEV3270A"); + assertEquals("DEV3270A", session.getAssociatedDeviceName()); + assertEquals("DEV3270A", session.GetAssociatedDeviceName()); + + session.setSessionCounter(5); + assertEquals(5, session.getSessionCount()); + assertEquals(5, session.GetSessionCount()); + + session.setMacroID(42); + assertEquals(42, session.getMacroID()); + assertEquals(42, session.GetMacroID()); + + session.setKeyStrength(256); + assertEquals(256, session.getKeyStrength()); + assertEquals(256, session.GetKeyStrength()); + + ClassLoader cl = getClass().getClassLoader(); + session.SetCustomizedCAsClassLoader(cl); + assertSame(cl, session.GetCustomizedCAsClassLoader()); + + Properties props = new Properties(); + props.setProperty("USER", "SYSADMIN"); + session.sendNewEnvironmentVariables(props); + assertEquals("SYSADMIN", session.getProperties().getProperty("USER")); + } + + @Test + public void testStartCommunicationWithBlockingDescriptor() throws Exception { + ECLSession session = new ECLSession(); + // Matching screen descriptor with string array convenience overload + String[] patterns = new String[] { "WELCOME", "SIGNON" }; + assertNotNull(patterns); + + // Verify ECLErr on unmatched descriptor with timeout + ECLScreenDesc descFail = new ECLScreenDesc(); + descFail.AddString("UNMATCHABLE_SCREEN_PATTERN", 1, 1); + try { + // timeout in seconds (1s = 1000ms) + session.StartCommunicationWithBlocking(1, descFail); + } catch (ECLErr | IOException expected) { + // Expected either connection failure or descriptor timeout ECLErr + assertNotNull(expected); + } + } + + @Test + public void testSessionWaitWhileScreen() { + ECLSession session = new ECLSession(); + ECLScreenDesc desc = new ECLScreenDesc(); + desc.AddString("NONEXISTENT", 1, 1); + // Should return true immediately as screen does NOT match + assertTrue(session.WaitWhileScreen(desc, 100)); + assertTrue(session.waitWhileScreen(desc, 100)); + } + + // ========================================================================= + // 4.2 ECLPS Presentation Space Tests + // ========================================================================= + + @Test + public void testPSDocModeAndTabHandling() { + // Tab mode "2" replaces \t with spaces + String tabbed = "ABC\tDEF"; + String spaceExpanded = ps.handleTabs(tabbed, "2"); + assertEquals("ABC DEF", spaceExpanded); + + // Tab mode "1" aligns to tab stop columns (8-column default) + String colAligned = ps.handleTabs("A\tB", "1"); + assertEquals("A B", colAligned); + + // Null / unmodified modes + assertEquals("ABC", ps.handleTabs("ABC", "0")); + assertEquals(null, ps.handleTabs(null, "1")); + + // Trim on paste + assertFalse(ps.enableTrimOnPaste()); + assertFalse(ps.EnableTrimOnPaste()); + + // Paste in Doc Mode (when not in DOC mode delegates to normal paste) + int pasted = ps.pasteInDocMode("HELLO", 1, 1); + assertEquals(5, pasted); + assertEquals("HELLO", ps.getString(0, 0, 5)); + } + + @Test + public void testPSCheckBeforeSendKeysAndBadgeReader() throws Exception { + assertEquals(0, ps.CheckBeforeSendKeys("ABC")); + assertEquals(0, ps.CheckBeforeSendKeys("ABC", 1)); + assertEquals(0, ps.CheckBeforeSendKeys("ABC", 1, 1)); + + // Badge reader writes data and sends [enter] + ps.BadgeReader("BADGE123", 1, 1); + assertEquals("BADGE123", ps.getString(0, 0, 8)); + + // Out of bounds badge reader throws ECLErr + assertThrows(ECLErr.class, () -> { + ps.BadgeReader("INVALID", 99999); + }); + + ps.asisBadgeReader("BADGE456", null); + } + + @Test + public void testPSGetScreenRect() { + ps.SetText("ROW1DATA", 1, 1); + ps.SetText("ROW2DATA", 2, 1); + + char[] rectChars = new char[16]; + int count = ps.GetScreenRect(rectChars, 16, 1, 1, 2, 8, ECLConstants.PLANE_TEXT); + assertEquals(16, count); + assertEquals("ROW1DATAROW2DATA", new String(rectChars)); + + String rectStr = ps.GetScreenRect(1, 1, 2, 8); + assertTrue(rectStr.contains("ROW1DATA")); + assertTrue(rectStr.contains("ROW2DATA")); + } + + @Test + public void testPSLanguageAndGraphicsServices() throws Exception { + ECLPSGraphicsServices gs = ps.GetPSGraphicsServices(); + assertNotNull(gs); + assertSame(gs, ps.GetECLPSGraphicsServices()); + assertSame(gs, ps.getGraphicsServices()); + + AtomicBoolean graphicsUpdatedFired = new AtomicBoolean(false); + gs.addGraphicsListener(new ECLPSGraphicsListener() { + @Override + public void graphicsEvent(ECLPSGraphicsEvent event) {} + @Override + public void graphicsUpdated(ECLPSGraphicsEvent event) { + graphicsUpdatedFired.set(true); + } + }); + gs.mousePressed(10, 20, 1); + assertTrue(graphicsUpdatedFired.get()); + + // BIDI services + ECLPSBIDIServices bidi = ps.GetPSBIDIServices(); + assertNotNull(bidi); + bidi.SetNumeralShape(ECLPSBIDIServices.NATIONAL); + assertEquals(ECLPSBIDIServices.NATIONAL, bidi.GetNumeralShape()); + bidi.SetTextOrientation(ECLPSBIDIServices.RIGHT_TO_LEFT); + assertEquals(ECLPSBIDIServices.RIGHT_TO_LEFT, bidi.GetTextOrientation()); + bidi.setNumericSwap(true); + assertTrue(bidi.getNumericSwap()); + + // Hindi services + ECLPSHindiServices hindi = ps.GetPSHindiServices(); + assertNotNull(hindi); + assertEquals(5, hindi.GetHindiCursorCol(1, 5)); + assertEquals(0, hindi.GetHindiCursorLevel(1, 5)); + + // Thai services + ECLPSTHAIServices thai = ps.GetPSTHAIServices(); + assertNotNull(thai); + thai.SetThaiDisplayMode(2); + assertEquals(2, thai.GetThaiDisplayMode()); + } + + @Test + public void testPSGetDSAndParent() { + assertNull(ps.GetParent()); + ECLSession session = new ECLSession(); + ps.setSession(session); + assertSame(session, ps.GetParent()); + assertSame(session, ps.getParent()); + + Object dummyHist = new Object(); + ps.setScreenHistory(dummyHist); + assertSame(dummyHist, ps.getScreenHistory()); + } + + // ========================================================================= + // 4.3 ECLOIA Operator Information Area Tests + // ========================================================================= + + @Test + public void testOIAStatusFlagsAndBitmasks() { + ECLOIA oia = new ECLOIA(screen, inputProcessor, null); + + long flagsEx = oia.GetStatusFlagsEx(); + // Online and controller ready are set by default + assertEquals(ECLOIA.STATE_ONLINE, flagsEx & ECLOIA.STATE_ONLINE); + assertEquals(ECLOIA.STATE_CONTROLLER_READY, flagsEx & ECLOIA.STATE_CONTROLLER_READY); + + // Toggle insert mode and verify status flag reflection + inputProcessor.setInsertMode(true); + assertTrue((oia.GetStatusFlagsEx() & ECLOIA.STATE_INSERT) != 0); + + // DOC Mode & WordWrap + screen.setEntryAssistDOCmode(true); + screen.setEntryAssistWordWrap(true); + assertTrue((oia.GetStatusFlagsEx() & ECLOIA.STATE_DOC_MODE) != 0); + assertTrue((oia.GetStatusFlagsEx() & ECLOIA.STATE_WORDWRAP) != 0); + } + + @Test + public void testOIADoNotEnterAndReadyConnect() { + ECLOIA oia = new ECLOIA(screen, inputProcessor, null); + + // Set do not enter group 8 (SYS LOCK) + oia.setDoNotEnter(8, 0); + assertTrue((oia.GetStatusFlagsEx() & ECLOIA.STATE_SYS_LOCK) != 0); + + // Clear do not enter + oia.clearDoNotEnter(); + assertEquals(0, oia.GetStatusFlagsEx() & ECLOIA.STATE_SYS_LOCK); + + // ReadyConnect state + oia.setReadyConnect(4, "MYJOB"); + assertTrue((oia.GetStatusFlagsEx() & ECLOIA.STATE_MY_JOB) != 0); + + // Msg waiting and OIA invisible + oia.setMsgWaiting(true); + assertTrue((oia.GetStatusFlagsEx() & ECLOIA.STATE_MSG_WAITING) != 0); + + oia.setOiaInvisible(); + assertTrue((oia.GetStatusFlagsEx() & ECLOIA.STATE_OIA_SUPPRESS) != 0); + + oia.writeToOIA("STATUS_MSG"); + } + + @Test + public void testOIALanguageServices() { + ECLOIA oia = new ECLOIA(screen, inputProcessor, null); + assertNotNull(oia.GetECLOIABIDI()); + assertNotNull(oia.GetECLOIATHAI()); + assertNotNull(oia.GetECLOIAHindi()); + } + + // ========================================================================= + // 4.4 ECLConnection JavaBean Properties Tests + // ========================================================================= + + @Test + public void testConnectionJavaBeanProperties() { + ECLConnection conn = new ECLConnection(); + + conn.SetHost("mvs.example.com"); + assertEquals("mvs.example.com", conn.GetHost()); + assertEquals("mvs.example.com", conn.getHost()); + + conn.SetPort(2023); + assertEquals(2023, conn.GetPort()); + assertEquals(2023, conn.getPort()); + + conn.SetCodePage("1047"); + assertEquals("1047", conn.GetCodePage()); + assertEquals("1047", conn.getCodePage()); + + conn.SetDeviceName("IBM-3278-2"); + assertEquals("IBM-3278-2", conn.GetDeviceName()); + + conn.SetLUName("TSO001"); + assertEquals("TSO001", conn.GetLUName()); + + conn.SetWorkstationID("WS42"); + assertEquals("WS42", conn.GetWorkstationID()); + + conn.SetSSL(true); + assertTrue(conn.IsSSL()); + assertTrue(conn.isSSL()); + + conn.setContentionResolution(true); + assertTrue(conn.getContentionResolution()); + assertTrue(conn.isContentionResolution()); + + conn.set_LULU_Session(true); + assertTrue(conn.is_LULU_Session()); + + conn.setNegotiatedCResolution(true); + assertTrue(conn.isNegotiateCResolution()); + + conn.setBIND7FArchitectureViolation(true); + assertTrue(conn.isBIND7FArchitectureViolation()); + + conn.SetKeyRemap("/keys/remap.xml"); + assertEquals("/keys/remap.xml", conn.GetKeyRemap()); + + conn.SetCertificateName("ClientCert1"); + assertEquals("ClientCert1", conn.GetCertificateName()); + + conn.SetCertificateSource("LOCAL_STORE"); + assertEquals("LOCAL_STORE", conn.GetCertificateSource()); + + conn.SetCertificateURL("file:///certs/ca.pem"); + assertEquals("file:///certs/ca.pem", conn.GetCertificateURL()); + + conn.SetCertificatePassword("secret"); + assertEquals("secret", conn.GetCertificatePassword()); + + conn.SetCertificateProvided(true); + assertTrue(conn.isCertificateProvided()); + + conn.SetSecurityProtocol("TLS"); + assertEquals("TLS", conn.GetSecurityProtocol()); + + conn.SetTLSProtocolVersion("TLSv1.3"); + assertEquals("TLSv1.3", conn.GetTLSProtocolVersion()); + + conn.SetUseJSSE(true); + assertTrue(conn.isUseJSSE()); + + conn.SetJSSETrustStore("/lib/security/cacerts"); + assertEquals("/lib/security/cacerts", conn.GetJSSETrustStore()); + + conn.SetJSSETrustStoreType("PKCS12"); + assertEquals("PKCS12", conn.GetJSSETrustStoreType()); + + conn.SetJSSETrustStorePassword("changeit"); + assertEquals("changeit", conn.GetJSSETrustStorePassword()); + + conn.SetProxyType("SOCKS5"); + assertEquals("SOCKS5", conn.GetProxyType()); + + conn.SetProxyServerName("proxy.corp.com"); + assertEquals("proxy.corp.com", conn.GetProxyServerName()); + + conn.SetProxyServerPort("1080"); + assertEquals("1080", conn.GetProxyServerPort()); + + conn.SetProxyUserID("proxyUser"); + assertEquals("proxyUser", conn.GetProxyUserID()); + + conn.SetProxyUserPassword("proxyPass"); + assertEquals("proxyPass", conn.GetProxyUserPassword()); + + conn.SetProxyAuthenMethod("BASIC"); + assertEquals("BASIC", conn.GetProxyAuthenMethod()); + + conn.SetProxySecurityProtocol("TLS"); + assertEquals("TLS", conn.GetProxySecurityProtocol()); + + // convertData boolean translation + Properties props = new Properties(); + props.setProperty("enableSSL", "true"); + props.setProperty("contentionRes", "false"); + props.setProperty("otherProp", "hello"); + conn.convertData(props); + + assertEquals("1", props.getProperty("enableSSL")); + assertEquals("0", props.getProperty("contentionRes")); + assertEquals("hello", props.getProperty("otherProp")); + } + + // ========================================================================= + // 4.5 ECLField & ECLFieldList Tests + // ========================================================================= + + @Test + public void testECLFieldAndFieldListAttributes() { + // Build 2 fields: Field 1 (unprotected, modifiable) and Field 2 (protected, high intensity) + screen.setFieldAttribute(0, (byte) (FA_PROTECT | FA_INT_HIGH_SEL)); // Pos 0: Prot, High + screen.setFieldAttribute(40, (byte) (FA_MODIFY)); // Pos 40: Unprot, Modified + + ECLFieldList fl = ps.getFieldList(); + assertEquals(2, fl.getFieldCount()); + + ECLField f1 = fl.findField(5); + assertNotNull(f1); + assertTrue(f1.IsProtected()); + assertTrue(f1.IsHighIntensity()); + assertEquals(0, f1.getStartFieldPos()); + + // Test field plane copying + char[] planeData = f1.copyPlanes(ECLConstants.PLANE_TEXT); + assertNotNull(planeData); + + // Dynamically alter field attribute + f1.setFieldAttribute((char) (FA_MODIFY | FA_NUMERIC)); + assertTrue(f1.IsModified()); + assertTrue(f1.IsNumeric()); + + // Test ECLFieldList locateField and matchAttributes + // 0x01 = Modified, 0x20 = Protected, 0x10 = High Intensity + ECLField matchedMod = fl.locateField(0x01, null); + assertNotNull(matchedMod); + + ECLField matchedNotMod = fl.locateField(0x100, null); // 0x100 = Not modified + assertNull(matchedNotMod); // Both are modified now + + fl.copyPlanes(ECLConstants.PLANE_TEXT); + } + + // ========================================================================= + // 4.6 ECLScreenDesc & ECLScreenReco Tests + // ========================================================================= + + @Test + public void testScreenDescCriteriaAndBufferMatching() { + ECLScreenDesc desc = new ECLScreenDesc(); + desc.AddString("MAINFRAME", 1, 1, false); + desc.AddCursorPos(1, 10); + + ps.SetText("MAINFRAME", 1, 1); + ps.setCursorPos(0, 9); // 0-indexed row 0, col 9 = 1-indexed row 1, col 10 + + assertTrue(desc.Matches(ps, null)); + assertTrue(desc.Matches(screen)); + + // Add field attribute criteria in a dedicated descriptor test + ECLScreenDesc descFa = new ECLScreenDesc(); + screen.setFieldAttribute(0, (byte) FA_PROTECT); + descFa.AddFieldAttr(1, 0x20); // 0x20 = Protected + assertTrue(descFa.Matches(ps, null)); + assertTrue(descFa.Matches(screen)); + } + + @Test + public void testScreenRecoStaticAndDynamic() { + ps.SetText("LOGIN SCREEN", 1, 1); + ps.SetText("PASSWORD", 5, 1); + + ECLScreenDesc desc = new ECLScreenDesc(); + desc.AddString("LOGIN SCREEN", 1, 1, true); + + // Static matching + assertTrue(ECLScreenReco.IsMatch(ps, desc)); + assertTrue(ECLScreenReco.compareTextAt(ps, "LOGIN SCREEN", 1, 1, true)); + assertTrue(ECLScreenReco.compareTextInRect(ps, "PASSWORD", 5, 1, 5, 8, true)); + assertFalse(ECLScreenReco.compareTextAt(ps, "UNKNOWN", 1, 1, true)); + + // Dynamic recognition with notification callback + ECLScreenReco reco = new ECLScreenReco(ps); + AtomicBoolean screenMatched = new AtomicBoolean(false); + + reco.RegisterScreen(desc, new ECLScreenNotify() { + @Override + public void screenMatching(ECLScreenRecoEvent event) { + screenMatched.set(true); + assertSame(desc, event.getScreenDesc()); + assertSame(ps, event.getPS()); + } + }); + + assertTrue(screenMatched.get()); + + // Test unregister + reco.UnregisterScreen(desc, (ECLScreenNotify) null); + reco.dispose(); + } + + // ========================================================================= + // Drop-in Canonical eNetwork.ECL Hierarchy Tests + // ========================================================================= + + @Test + public void testDropInENetworkECLHierarchy() { + haus.nightmare.lib3270j.eNetwork.ECL.ECLSession session = + new haus.nightmare.lib3270j.eNetwork.ECL.ECLSession(); + assertNotNull(session.GetPS()); + assertNotNull(session.GetOIA()); + assertNotNull(session.GetConnection()); + + haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc desc = + new haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc(); + desc.AddString("TEST"); + + haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenReco reco = + new haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenReco(session); + assertNotNull(reco); + + haus.nightmare.lib3270j.eNetwork.ECL.ECLErr err = + new haus.nightmare.lib3270j.eNetwork.ECL.ECLErr("ECLSession", "ECL0001", "Timeout"); + assertEquals("ECLSession", err.getTag()); + assertEquals("ECL0001", err.getID()); + assertEquals("Timeout", err.getText()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase5EclEventTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase5EclEventTest.java new file mode 100644 index 0000000..5c2d026 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/Phase5EclEventTest.java @@ -0,0 +1,469 @@ +package haus.nightmare.lib3270j.ecl; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +import haus.nightmare.lib3270j.ConnectionState; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; + +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Comprehensive test suite for Phase 5: ECL Event Model & Listener Signatures. + */ +public class Phase5EclEventTest { + + private ScreenBuffer screen; + private InputProcessor inputProcessor; + private EbcdicTranslator translator; + private ECLPS ps; + + @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); + } + + // ========================================================================= + // 5.1 ECLPSEvent and ECLPSUpdate Tests + // ========================================================================= + + @Test + public void testECLPSEventGettersAndCoordinates() { + // Create event with 0-based coordinates row 2, col 5 to row 4, col 10 (on 80 col screen) + ECLPSEvent evt = new ECLPSEvent(ps, ECLPSEvent.PS_UPDATE, ECLPSEvent.HOST_EVENTS, + 2, 5, 4, 10, 100, 200, 24, 80, false, true, 42, true, null); + + assertSame(ps, evt.getPS()); + assertSame(ps, evt.GetPS()); + assertEquals(ECLPSEvent.HOST_EVENTS, evt.GetType()); + assertEquals(ECLPSEvent.HOST_EVENTS, evt.getType()); + assertEquals(ECLPSEvent.PS_UPDATE, evt.getEventType()); + + // 0-based legacy getters + assertEquals(2, evt.getStartRow()); + assertEquals(5, evt.getStartCol()); + assertEquals(4, evt.getEndRow()); + assertEquals(10, evt.getEndCol()); + assertEquals(2 * 80 + 5, evt.getStart()); + assertEquals(4 * 80 + 10, evt.getEnd()); + + // 1-based HoD specification getters + assertEquals(3, evt.GetStartRow()); + assertEquals(6, evt.GetStartCol()); + assertEquals(5, evt.GetEndRow()); + assertEquals(11, evt.GetEndCol()); + assertEquals(2 * 80 + 5 + 1, evt.GetStart()); + assertEquals(4 * 80 + 10 + 1, evt.GetEnd()); + + // Indicator flags and counters + assertTrue(evt.GetCursorVisible()); + assertTrue(evt.isCursorVisible()); + assertEquals(42, evt.GetRingCounter()); + assertEquals(42, evt.getRingCounter()); + assertTrue(evt.isStartPrinterBit()); + assertTrue(evt.IsStartPrinterBit()); + assertFalse(evt.isFullUpdate()); + assertFalse(evt.IsFullUpdate()); + + // ECLPSUpdate object + ECLPSUpdate update = evt.getECLPSUpdate(); + assertNotNull(update); + assertSame(update, evt.GetECLPSUpdate()); + assertEquals(3, update.GetStartRow()); + assertEquals(6, update.GetStartCol()); + assertEquals(5, update.GetEndRow()); + assertEquals(11, update.GetEndCol()); + assertEquals(2 * 80 + 5 + 1, update.GetStart()); + assertEquals(4 * 80 + 10 + 1, update.GetEnd()); + assertFalse(update.isFullUpdate()); + } + + @Test + public void testECLPSUpdateStandalone() { + ECLPSUpdate update = new ECLPSUpdate(ps, 0, 0, 23, 79, 0, 1919, true, "FULL_SCREEN"); + assertSame(ps, update.getPS()); + assertSame(ps, update.GetPS()); + assertEquals(0, update.getStartRow()); + assertEquals(1, update.GetStartRow()); + assertEquals(0, update.getStartCol()); + assertEquals(1, update.GetStartCol()); + assertEquals(23, update.getEndRow()); + assertEquals(24, update.GetEndRow()); + assertEquals(79, update.getEndCol()); + assertEquals(80, update.GetEndCol()); + assertEquals(0, update.getStart()); + assertEquals(1, update.GetStart()); + assertEquals(1919, update.getEnd()); + assertEquals(1920, update.GetEnd()); + assertTrue(update.isFullUpdate()); + assertTrue(update.IsFullUpdate()); + assertEquals("FULL_SCREEN", update.getText()); + assertEquals("FULL_SCREEN", update.GetText()); + } + + // ========================================================================= + // 5.2 ECLPSListener Callbacks, Filtering, and Stop/Error Notifications + // ========================================================================= + + @Test + public void testPSNotifyEventAndFilter() { + AtomicInteger hostEventsReceived = new AtomicInteger(0); + AtomicInteger userEventsReceived = new AtomicInteger(0); + AtomicReference lastHostEvent = new AtomicReference<>(); + + ECLPSListener hostListener = new ECLPSListener() { + @Override + public void PSNotifyEvent(ECLPSEvent event) { + hostEventsReceived.incrementAndGet(); + lastHostEvent.set(event); + } + }; + + ECLPSListener userListener = new ECLPSListener() { + @Override + public void PSNotifyEvent(ECLPSEvent event) { + userEventsReceived.incrementAndGet(); + } + }; + + // Register host listener for HOST_EVENTS only + ps.RegisterPSEvent(hostListener, ECLPS.HOST_EVENTS); + // Register user listener for USER_EVENTS only + ps.RegisterPSEvent(userListener, ECLPS.USER_EVENTS); + + // 1. Dispatch Host update + ps.notifyPSUpdate(1, 1, 1, 10, false, ECLPS.HOST_EVENTS, true); + assertEquals(1, hostEventsReceived.get()); + assertEquals(0, userEventsReceived.get()); + assertNotNull(lastHostEvent.get()); + assertTrue(lastHostEvent.get().isStartPrinterBit()); + assertTrue(lastHostEvent.get().GetRingCounter() > 0); + + // 2. Dispatch User cursor move + ps.notifyCursorMoved(0, 50); + assertEquals(1, hostEventsReceived.get()); + assertEquals(1, userEventsReceived.get()); + + // 3. Dispatch Alarm (HOST_EVENTS) + ps.notifyAlarm(); + assertEquals(2, hostEventsReceived.get()); + assertEquals(1, userEventsReceived.get()); + } + + @Test + public void testPSNotifyErrorAndStop() { + AtomicReference capturedErr = new AtomicReference<>(); + AtomicInteger stopReason = new AtomicInteger(0); + + ECLPSListener listener = new ECLPSListener() { + @Override + public void PSNotifyEvent(ECLPSEvent event) {} + + @Override + public void PSNotifyError(ECLPS p, ECLErr err) { + capturedErr.set(err); + } + + @Override + public void PSNotifyStop(ECLPS p, int reason) { + stopReason.set(reason); + } + }; + + ps.RegisterPSEvent(listener); + + // Fire error + ECLErr testErr = new ECLErr("ECLPS", "ECL0099", "Synthetic test error"); + ps.notifyPSError(testErr); + assertSame(testErr, capturedErr.get()); + + // Unregister triggers STOP_UNREGISTER + ps.UnregisterPSEvent(listener); + assertEquals(ECLPS.STOP_UNREGISTER, stopReason.get()); + } + + @Test + public void testLegacyPSListenerCompatibility() { + AtomicInteger legacyChanged = new AtomicInteger(0); + AtomicInteger legacyCursor = new AtomicInteger(0); + AtomicInteger legacyAlarm = new AtomicInteger(0); + AtomicInteger legacyResize = new AtomicInteger(0); + + ECLPSListener legacyListener = new ECLPSListener() { + @Override + public void psChanged(ECLPSEvent event) { + legacyChanged.incrementAndGet(); + } + + @Override + public void psCursorMoved(ECLPSEvent event) { + legacyCursor.incrementAndGet(); + } + + @Override + public void psAlarm(ECLPSEvent event) { + legacyAlarm.incrementAndGet(); + } + + @Override + public void psResized(ECLPSEvent event) { + legacyResize.incrementAndGet(); + } + }; + + ps.RegisterPSEvent(legacyListener); + + ps.notifyPSUpdate(0, 0, 1, 1, false); + assertEquals(1, legacyChanged.get()); + + ps.notifyCursorMoved(0, 10); + assertEquals(2, legacyChanged.get()); + assertEquals(1, legacyCursor.get()); + + ps.notifyAlarm(); + assertEquals(3, legacyChanged.get()); + assertEquals(1, legacyAlarm.get()); + + ps.notifyScreenResized(24, 80); + assertEquals(4, legacyChanged.get()); + assertEquals(1, legacyResize.get()); + } + + // ========================================================================= + // 5.3 ECLOIAEvent, ECLOIAListener, and ECLOIANotify Tests + // ========================================================================= + + @Test + public void testOIAEventAndListenerSignatures() { + ECLOIA oia = new ECLOIA(screen, inputProcessor, null); + + AtomicInteger oiaListenerEvents = new AtomicInteger(0); + AtomicInteger oiaNotifyEvents = new AtomicInteger(0); + AtomicReference lastOiaEvent = new AtomicReference<>(); + AtomicReference oiaError = new AtomicReference<>(); + AtomicInteger oiaStopReason = new AtomicInteger(0); + + ECLOIAListener listener = new ECLOIAListener() { + @Override + public void OIANotifyEvent(ECLOIAEvent event) { + oiaListenerEvents.incrementAndGet(); + lastOiaEvent.set(event); + } + + @Override + public void OIANotifyError(ECLOIA source, ECLErr err) { + oiaError.set(err); + } + + @Override + public void OIANotifyStop(ECLOIA source, int reason) { + oiaStopReason.set(reason); + } + }; + + ECLOIANotify notify = new ECLOIANotify() { + @Override + public void OIANotifyEvent(ECLOIAEvent event) { + oiaNotifyEvents.incrementAndGet(); + } + }; + + oia.RegisterOIAEvent(listener); + oia.RegisterOIAEvent(notify); + + // Mutate OIA state: lock keyboard + inputProcessor.setKeyboardLocked(true); + assertTrue(oiaListenerEvents.get() > 0); + assertTrue(oiaNotifyEvents.get() > 0); + + ECLOIAEvent event = lastOiaEvent.get(); + assertNotNull(event); + assertSame(oia, event.getOIA()); + assertSame(oia, event.GetOIA()); + assertEquals(ECLOIAEvent.OIA_UPDATE, event.GetType()); + assertTrue(event.isInputInhibited()); + assertTrue(event.IsInputInhibited()); + + // Error notification + ECLErr err = new ECLErr("ECLOIA", "OIA001", "OIA Comm Fail"); + oia.notifyOIAError(err); + assertSame(err, oiaError.get()); + + // Unregister triggers stop + oia.UnregisterOIAEvent(listener); + assertEquals(ECLOIA.STOP_UNREGISTER, oiaStopReason.get()); + } + + // ========================================================================= + // 5.4 ECLCommEvent and ECLCommListener Tests + // ========================================================================= + + @Test + public void testCommEventAndListenerSignatures() { + ECLConnection conn = new ECLConnection(); + + AtomicInteger commListenerEvents = new AtomicInteger(0); + AtomicBoolean commNotifyFlag = new AtomicBoolean(false); + AtomicReference lastCommEvent = new AtomicReference<>(); + AtomicReference commError = new AtomicReference<>(); + AtomicInteger commStopReason = new AtomicInteger(0); + + ECLCommListener listener = new ECLCommListener() { + @Override + public void CommNotifyEvent(ECLCommEvent event) { + commListenerEvents.incrementAndGet(); + lastCommEvent.set(event); + } + + @Override + public void CommNotifyError(ECLConnection c, ECLErr err) { + commError.set(err); + } + + @Override + public void CommNotifyStop(ECLConnection c, int reason) { + commStopReason.set(reason); + } + }; + + ECLCommNotify notify = new ECLCommNotify() { + @Override + public void CommNotify(boolean connected) { + commNotifyFlag.set(connected); + } + }; + + conn.RegisterCommEvent(listener); + conn.RegisterCommEvent(notify, false); + + // Trigger connection state change + ECLCommEvent testCommEvent = new ECLCommEvent(conn, ECLCommEvent.COMM_CONNECTED, + ConnectionState.NOT_CONNECTED, ConnectionState.CONNECTED_3270, "Connected OK", "IBM-3278-2", "TSO01"); + + // Use reflection or error dispatch to trigger comm event + conn.notifyCommError(new ECLErr("ECLConnection", "ERR01", "Comm failure")); + assertNotNull(commError.get()); + + // Unregister triggers stop + conn.UnregisterCommEvent(listener); + assertEquals(ECLConnection.STOP_UNREGISTER, commStopReason.get()); + } + + // ========================================================================= + // 5.5 ECLPSGraphicsEvent and ECLPSGraphicsListener Tests + // ========================================================================= + + @Test + public void testPSGraphicsEventAndListener() { + ECLPSGraphicsServices gs = ps.GetPSGraphicsServices(); + assertNotNull(gs); + + AtomicInteger graphicsEventCount = new AtomicInteger(0); + AtomicInteger graphicsUpdatedCount = new AtomicInteger(0); + AtomicReference lastGEvent = new AtomicReference<>(); + + ECLPSGraphicsListener gListener = new ECLPSGraphicsListener() { + @Override + public void graphicsEvent(ECLPSGraphicsEvent event) { + graphicsEventCount.incrementAndGet(); + lastGEvent.set(event); + } + + @Override + public void graphicsUpdated(ECLPSGraphicsEvent event) { + graphicsUpdatedCount.incrementAndGet(); + lastGEvent.set(event); + } + }; + + gs.addGraphicsListener(gListener); + + // Fire graphics update via mousePressed + gs.mousePressed(50, 100, 1); + assertEquals(1, graphicsUpdatedCount.get()); + assertNotNull(lastGEvent.get()); + assertEquals(ECLPSGraphicsEvent.GRAPHICS_UPDATED, lastGEvent.get().GetID()); + assertSame(ps, lastGEvent.get().GetPS()); + assertSame(ps, lastGEvent.get().getSource()); + + // Fire custom graphics event + if (gs instanceof DefaultPSGraphicsServices) { + ((DefaultPSGraphicsServices) gs).fireGraphicsEvent(ECLPSGraphicsEvent.GRAPHICS_ACTIVATED); + assertEquals(1, graphicsEventCount.get()); + assertEquals(ECLPSGraphicsEvent.GRAPHICS_ACTIVATED, lastGEvent.get().GetID()); + } + + // Test with image and rectangle + Image testImage = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); + Rectangle rect = new Rectangle(0, 0, 32, 32); + ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect); + assertSame(testImage, fullGEvent.GetImage()); + assertSame(testImage, fullGEvent.getImage()); + assertEquals(rect, fullGEvent.GetRectangle()); + assertEquals(rect, fullGEvent.getRectangle()); + + // Remove listener + gs.removeGraphicsListener(gListener); + gs.mousePressed(50, 100, 1); + assertEquals(1, graphicsUpdatedCount.get()); // Did not increase + } + + // ========================================================================= + // 5.6 Drop-in Canonical eNetwork.ECL.event.* Facade Tests + // ========================================================================= + + @Test + public void testDropInENetworkEventFacades() { + haus.nightmare.lib3270j.eNetwork.ECL.ECLPSUpdate eUpdate = + new haus.nightmare.lib3270j.eNetwork.ECL.ECLPSUpdate(ps, 0, 0, 23, 79, 0, 1919, true, "TEST"); + assertEquals(1, eUpdate.GetStartRow()); + assertEquals(24, eUpdate.GetEndRow()); + + haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSEvent ePsEvent = + new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSEvent(ps, ECLPSEvent.PS_UPDATE, 0, 0, 5, 5, 0, 0, 24, 80, false); + assertEquals(1, ePsEvent.GetStartRow()); + assertEquals(6, ePsEvent.GetEndRow()); + + AtomicBoolean listenerNotified = new AtomicBoolean(false); + haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSListener eListener = + new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSListener() { + @Override + public void PSNotifyEvent(ECLPSEvent event) { + listenerNotified.set(true); + } + }; + + ps.RegisterPSEvent(eListener); + ps.notifyPSUpdate(0, 0, 1, 1, false); + assertTrue(listenerNotified.get()); + ps.UnregisterPSEvent(eListener); + + // OIA facade + haus.nightmare.lib3270j.eNetwork.ECL.event.ECLOIAEvent eOiaEvent = + new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLOIAEvent(null, 1, 0, 0, false, "READY"); + assertEquals("READY", eOiaEvent.GetStatusString()); + + // Comm facade + haus.nightmare.lib3270j.eNetwork.ECL.event.ECLCommEvent eCommEvent = + new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLCommEvent(null, 1, ConnectionState.NOT_CONNECTED, + ConnectionState.CONNECTED_3270, "OK", "3278", "LU1"); + assertTrue(eCommEvent.IsConnected()); + + // Graphics facade + haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSGraphicsEvent eGraphicsEvent = + new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSGraphicsEvent(ps, 5); + assertEquals(5, eGraphicsEvent.GetID()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java index a5d2905..6398847 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java @@ -81,7 +81,7 @@ public class FillAreaTest { int red = 0xFFFF0000; int blue = 0xFF0000FF; // Background overpaint color area.fill(plane, red, 0, 5, false, 0, - GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.MIX_OVER, blue, null); + GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.BMX_OPAQUE, blue, null); int[] buffer = plane.getRgbBuffer(); int redCount = 0; @@ -94,6 +94,6 @@ public class FillAreaTest { } } assertTrue(redCount > 0, "Expected pattern foreground red pixels"); - assertTrue(blueCount > 0, "Expected pattern background blue pixels under MIX_OVER"); + assertTrue(blueCount > 0, "Expected pattern background blue pixels under BMX_OPAQUE"); } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/Phase1ColorCalibrationTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/Phase1ColorCalibrationTest.java new file mode 100644 index 0000000..d59ab94 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/Phase1ColorCalibrationTest.java @@ -0,0 +1,194 @@ +package haus.nightmare.lib3270j.graphics; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.screen.ExtendedAttribute; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.awt.Color; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verification test suite for Phase 1: Screen & Presentation Engine Fidelity. + * Covers items 1.1 through 1.4 of the PhasedUpdates specification. + */ +public class Phase1ColorCalibrationTest { + + // ==================================================================================================== + // ITEM 1.1: GOCA 16-Color Graphics Palette Calibration + // ==================================================================================================== + @Test + @DisplayName("Item 1.1: GOCA 16-color palette calibration matches IBM Host On-Demand") + public void testItem1_1_GocaColorPaletteCalibration() { + assertEquals(0xFFFFA200, GocaConstants.GOCA_COLORS[10], "GOCA Index 10 (Orange) must be 0xFFFFA200"); + assertEquals(0xFFA0A000, GocaConstants.GOCA_COLORS[14], "GOCA Index 14 (Mustard) must be 0xFFA0A000"); + + // Verify primary palette colors + assertEquals(0xFF00FF00, GocaConstants.GOCA_COLORS[0], "Index 0: Default (Green)"); + assertEquals(0xFF7890F0, GocaConstants.GOCA_COLORS[1], "Index 1: Blue (0x7890F0)"); + assertEquals(0xFFFF0000, GocaConstants.GOCA_COLORS[2], "Index 2: Red"); + assertEquals(0xFFFF00FF, GocaConstants.GOCA_COLORS[3], "Index 3: Pink"); + assertEquals(0xFF00FF00, GocaConstants.GOCA_COLORS[4], "Index 4: Green"); + assertEquals(0xFF00FFFF, GocaConstants.GOCA_COLORS[5], "Index 5: Turquoise"); + assertEquals(0xFFFFFF00, GocaConstants.GOCA_COLORS[6], "Index 6: Yellow"); + assertEquals(0xFFFFFFFF, GocaConstants.GOCA_COLORS[7], "Index 7: Neutral White"); + assertEquals(0xFF000000, GocaConstants.GOCA_COLORS[8], "Index 8: Black"); + } + + // ==================================================================================================== + // ITEM 1.2: Base 4-Color Model Realignment & ExtendedAttribute Helper + // ==================================================================================================== + @Test + @DisplayName("Item 1.2: Base 4-color model helper maps field attributes correctly") + public void testItem1_2_Base4ColorMapping() { + // Normal Unprotected: Green (0xF4) + byte faNormalUnprotected = 0x00; // not protected, not high + assertEquals(ExtendedAttribute.COLOR_BASE_NORMAL_UNPROTECT, ExtendedAttribute.getBase3270Color(faNormalUnprotected)); + assertEquals((byte) 0xF4, ExtendedAttribute.getBase3270Color(faNormalUnprotected)); + + // Intensified Unprotected: Red (0xF2) + byte faIntensifiedUnprotected = 0x08; // high, not protected + assertEquals(ExtendedAttribute.COLOR_BASE_INTENSIFY_UNPROTECT, ExtendedAttribute.getBase3270Color(faIntensifiedUnprotected)); + assertEquals((byte) 0xF2, ExtendedAttribute.getBase3270Color(faIntensifiedUnprotected)); + + // Normal Protected: Turquoise / Cyan (0xF5) + byte faNormalProtected = 0x20; // protected, not high + assertEquals(ExtendedAttribute.COLOR_BASE_NORMAL_PROTECT, ExtendedAttribute.getBase3270Color(faNormalProtected)); + assertEquals((byte) 0xF5, ExtendedAttribute.getBase3270Color(faNormalProtected)); + + // Intensified Protected: White (0xF7) + byte faIntensifiedProtected = 0x28; // protected and high + assertEquals(ExtendedAttribute.COLOR_BASE_INTENSIFY_PROTECT, ExtendedAttribute.getBase3270Color(faIntensifiedProtected)); + assertEquals((byte) 0xF7, ExtendedAttribute.getBase3270Color(faIntensifiedProtected)); + } + + // ==================================================================================================== + // ITEM 1.3: HODTransparentColorFilter and HODColorChangeFilter + // ==================================================================================================== + @Test + @DisplayName("Item 1.3: HODTransparentColorFilter keys out background color to alpha 0x00") + public void testItem1_3_TransparentColorFilter() { + HODTransparentColorFilter filter = new HODTransparentColorFilter(Color.BLACK); + assertEquals(0, filter.getTransparentRgb()); + + // Black pixel (0xFF000000) should become transparent (0x00000000) + int filteredBlack = filter.filterRGB(0, 0, 0xFF000000); + assertEquals(0x00000000, filteredBlack); + + // Non-black pixel should be preserved + int redPixel = 0xFFFF0000; + int filteredRed = filter.filterRGB(0, 0, redPixel); + assertEquals(redPixel, filteredRed); + + // Change transparent color to white + filter.setTransparentRgb(0xFFFFFF); + int whitePixel = 0xFFFFFFFF; + assertEquals(0x00000000, filter.filterRGB(0, 0, whitePixel)); + } + + @Test + @DisplayName("Item 1.3: HODColorChangeFilter replaces old color with new color") + public void testItem1_3_ColorChangeFilter() { + HODColorChangeFilter filter = new HODColorChangeFilter(0x0000FF, 0xFFA200); + + // Blue pixel should become Orange + int bluePixel = 0xFF0000FF; + int result = filter.filterRGB(0, 0, bluePixel); + assertEquals(0xFFA200, result); + + // Green pixel should remain unchanged + int greenPixel = 0xFF00FF00; + assertEquals(greenPixel, filter.filterRGB(0, 0, greenPixel)); + } + + // ==================================================================================================== + // ITEM 1.4: ScreenBuffer Effective Attributes & Multi-plane Extraction + // ==================================================================================================== + @Test + @DisplayName("Item 1.4: ScreenBuffer effective attribute derivation and plane copying") + public void testItem1_4_ScreenBufferEffectiveAttributes() { + ScreenBuffer buffer = new ScreenBuffer(TerminalModel.IBM_3278_2, new EbcdicTranslator()); + + // Write a field header at position 0: normal unprotected + buffer.setFieldAttribute(0, (byte) 0x00); + + // Character at row 0, col 1: no explicit EA attributes -> should inherit base color (0xF4 Green) + ExtendedAttribute ea1 = new ExtendedAttribute(); + ea1.ucs4 = 'A'; + buffer.setExtAttr(0, 1, ea1); + + assertEquals((byte) 0xF4, buffer.getEffectiveForegroundColor(1)); + assertEquals((byte) 0x00, buffer.getEffectiveBackgroundColor(1)); + assertEquals((byte) 0x00, buffer.getEffectiveHighlighting(1)); + + // Character at row 0, col 2: explicit EA foreground (0xF1 Blue) and GR (underline) + ExtendedAttribute ea2 = new ExtendedAttribute(); + ea2.ucs4 = 'B'; + ea2.fg = (byte) 0xF1; + ea2.gr = ExtendedAttribute.GR_UNDERLINE; + buffer.setExtAttr(0, 2, ea2); + + assertEquals((byte) 0xF1, buffer.getEffectiveForegroundColor(2)); + assertEquals(ExtendedAttribute.GR_UNDERLINE, buffer.getEffectiveHighlighting(2)); + + // Test plane copying + char[] textPlane = new char[80]; + buffer.copyPlanes(ScreenBuffer.PLANE_TEXT, textPlane, 0, 80); + assertEquals('A', textPlane[1]); + assertEquals('B', textPlane[2]); + + char[] fgPlane = new char[80]; + buffer.copyPlanes(ScreenBuffer.PLANE_COLOR, fgPlane, 0, 80); + assertEquals((char) 0xF4, fgPlane[1]); + assertEquals((char) 0xF1, fgPlane[2]); + + char[] grPlane = new char[80]; + buffer.copyPlanes(ScreenBuffer.PLANE_HILITE, grPlane, 0, 80); + assertEquals((char) 0x00, grPlane[1]); + assertEquals((char) ExtendedAttribute.GR_UNDERLINE, grPlane[2]); + } + + // ==================================================================================================== + // ITEM 1.4 (GOCA Fill Area): Background Mix Modes (BMX_OPAQUE vs BMX_TRANSPARENT) + // ==================================================================================================== + @Test + @DisplayName("Item 1.4: FillArea respects BMX_OPAQUE and BMX_TRANSPARENT background mixing") + public void testItem1_4_FillAreaBackgroundMix() { + GraphicsPlane plane = new GraphicsPlane(10, 10); + + // Test transparent black fill: should not paint when BMX_TRANSPARENT + FillArea fillAreaTrans = new FillArea(GocaConstants.FILL_RULE_EVEN_ODD); + fillAreaTrans.addEdge(0, 0, 10, 0); + fillAreaTrans.addEdge(10, 0, 10, 10); + fillAreaTrans.addEdge(10, 10, 0, 10); + fillAreaTrans.addEdge(0, 10, 0, 0); + fillAreaTrans.fill(plane, 0xFF000000, 0, GocaConstants.PT_SOLID, false, 0, 0, 1, + GocaConstants.BMX_TRANSPARENT, 0xFF0000FF, GocaConstants.FILL_RULE_EVEN_ODD, null); + + // Verify plane pixels remain unpainted (transparent / 0) + assertEquals(0, plane.getRgbBuffer()[0]); + + // Test opaque background fill: with patterned fill and BMX_OPAQUE + GraphicsPlane plane2 = new GraphicsPlane(8, 8); + FillArea fillAreaOpaque = new FillArea(GocaConstants.FILL_RULE_EVEN_ODD); + fillAreaOpaque.addEdge(0, 0, 8, 0); + fillAreaOpaque.addEdge(8, 0, 8, 8); + fillAreaOpaque.addEdge(8, 8, 0, 8); + fillAreaOpaque.addEdge(0, 8, 0, 0); + fillAreaOpaque.fill(plane2, 0xFFFF0000, 0, 1, false, 0, 0, 1, + GocaConstants.BMX_OPAQUE, 0xFF0000FF, GocaConstants.FILL_RULE_EVEN_ODD, null); + + // At least one pixel should be Red (pattern foreground) and at least one Blue (opaque background) + boolean hasRed = false; + boolean hasBlue = false; + for (int p : plane2.getRgbBuffer()) { + if (p == 0xFFFF0000) hasRed = true; + if (p == 0xFF0000FF) hasBlue = true; + } + assertTrue(hasRed, "FillArea should render foreground pattern pixels"); + assertTrue(hasBlue, "FillArea should render opaque background pixels when BMX_OPAQUE"); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/Phase2GocaEngineTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/Phase2GocaEngineTest.java new file mode 100644 index 0000000..abc02aa --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/Phase2GocaEngineTest.java @@ -0,0 +1,313 @@ +package haus.nightmare.lib3270j.graphics; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Comprehensive test suite verifying Phase 2: Host Vector Graphics & GOCA Rendering Engine. + */ +public class Phase2GocaEngineTest { + + private GraphicsPlane plane; + private GocaDecoder decoder; + private GddmCoordinateTransform transform; + + @BeforeEach + public void setUp() { + plane = new GraphicsPlane(720, 384); + decoder = new GocaDecoder(plane); + transform = new GddmCoordinateTransform(80, 24, 9, 16); + } + + @Test + @DisplayName("Test GOCA Image Orders: 0xD1, 0x92, 0x93 (Standard End Image)") + public void testStandardImageOrders() { + // 0xD1: Begin Image Absolute (x=10, y=20, w=8, h=8, bpp=1, comp=0) + // 0x92: Image Data (8 bytes, each with 0xAA) + // 0x93: End Image + byte[] stream = new byte[] { + (byte) 0xD1, 0x0A, + 0x00, 0x0A, // x = 10 + 0x00, 0x14, // y = 20 + 0x00, 0x08, // w = 8 + 0x00, 0x08, // h = 8 + 0x01, // 1 bpp + 0x00, // uncompressed + (byte) 0x92, 0x08, + (byte) 0xAA, (byte) 0x55, (byte) 0xAA, (byte) 0x55, + (byte) 0xAA, (byte) 0x55, (byte) 0xAA, (byte) 0x55, + (byte) 0x93, 0x00 + }; + + decoder.decodeGoca(stream, 0, stream.length); + assertTrue(plane.hasContent(), "Graphics plane should have raster content after image order"); + } + + @Test + @DisplayName("Test GOCA Image Orders: 0x91 (Begin Image Current Position) and 0x93 (End Image)") + public void testBeginImageCurrentPosition() { + // Set current position to (30, 40) via G_GCLINE or G_GSCOL + decoder.setGraphicCursorPosition(30, 40); + + // 0x91: Begin Image Current Position (w=8, h=8, bpp=1, comp=0) + // 0x92: Image Data (8 bytes) + // 0x93: End Image + byte[] stream = new byte[] { + (byte) 0x91, 0x06, + 0x00, 0x08, // w = 8 + 0x00, 0x08, // h = 8 + 0x01, // 1 bpp + 0x00, // uncompressed + (byte) 0x92, 0x08, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0x93, 0x00 + }; + + decoder.decodeGoca(stream, 0, stream.length); + assertTrue(plane.hasContent(), "Graphics plane should have raster content from current position image"); + } + + @Test + @DisplayName("Test Procedure Orders: P_ATTCUR, P_SETCUR, P_DETCUR") + public void testCursorProcedureOrders() { + // P_ATTCUR (0x08), P_SETCUR (0x31: len=4, x=150, y=250), P_DETCUR (0x09) + byte[] proc = new byte[] { + 0x08, 0x00, // Attach cursor + 0x31, 0x04, 0x00, (byte) 150, 0x00, (byte) 250, // Set cursor (150, 250) + }; + + decoder.processProcedureOrders(proc, 0, proc.length); + assertTrue(decoder.isGraphicsCursorActive(), "Graphic cursor should be active after P_ATTCUR"); + assertTrue(plane.isGraphicCursorAttached(), "Plane graphic cursor should be attached"); + assertEquals(150, decoder.getGraphicCursorX()); + assertEquals(250, decoder.getGraphicCursorY()); + assertEquals(150, plane.getGraphicCursorX()); + assertEquals(250, plane.getGraphicCursorY()); + assertEquals(GocaConstants.GCURSOR_SHAPE_CROSSHAIR, plane.getHodCursorShape()); + + // Detach cursor + byte[] detach = new byte[] { 0x09, 0x00 }; + decoder.processProcedureOrders(detach, 0, detach.length); + assertFalse(decoder.isGraphicsCursorActive(), "Graphic cursor should be inactive after P_DETCUR"); + assertFalse(plane.isGraphicCursorAttached(), "Plane graphic cursor should be detached"); + assertEquals(GocaConstants.GCURSOR_SHAPE_BOX, plane.getHodCursorShape()); + } + + @Test + @DisplayName("Test GddmCoordinateTransform 3179G Conversion Methods") + public void testCoordinateTransformConversions() { + // 80 cols x 24 rows, cell pitch 9x16 -> 720x384 + assertEquals(1, transform.convertAddressToRow(0)); + assertEquals(1, transform.convertAddressToColumn(0)); + assertEquals(2, transform.convertAddressToRow(80)); + assertEquals(1, transform.convertAddressToColumn(80)); + assertEquals(2, transform.convertAddressToRow(85)); + assertEquals(6, transform.convertAddressToColumn(85)); + + assertEquals(0, transform.convertColumnToX(0)); + assertEquals(18, transform.convertColumnToX(2)); + assertEquals(0, transform.convertRowToY(0)); + assertEquals(32, transform.convertRowToY(2)); + + assertEquals(1, transform.convertXtoColumn(0)); + assertEquals(2, transform.convertXtoColumn(9)); + assertEquals(1, transform.convertYtoRow(0)); + assertEquals(2, transform.convertYtoRow(16)); + assertEquals(2, transform.convertXYToRow(100, 16)); + } + + @Test + @DisplayName("Test Triple-Plane Programmed Symbols Composite Colors against Calibrated GOCA Palette") + public void testTriplePlaneCompositeColors() { + ProgramSymbolSet pss = new ProgramSymbolSet(true); // Triple plane + pss.setLcid(0x40); + + // Define a 2x2 symbol where pixel 0=1 (Red), pixel 1=2 (Green), pixel 2=4 (Blue), pixel 3=7 (White) + byte[] raw = new byte[] { 1, 2, 4, 7 }; + ProgramSymbolSet.SymbolSlot slot = new ProgramSymbolSet.SymbolSlot(2, 2, raw, true); + + int[] rgb = slot.getRgbPixels(0xFFFFFFFF, 0x00000000); + assertEquals(GocaConstants.GOCA_COLORS[2], rgb[0], "Mask 1 should map to calibrated Red"); + assertEquals(GocaConstants.GOCA_COLORS[4], rgb[1], "Mask 2 should map to calibrated Green"); + assertEquals(GocaConstants.GOCA_COLORS[1], rgb[2], "Mask 4 should map to calibrated Blue (0xFF7890F0)"); + assertEquals(GocaConstants.GOCA_COLORS[7], rgb[3], "Mask 7 should map to calibrated White"); + } + + @Test + @DisplayName("Test Edge Class Scanline Algorithm and Quicksort") + public void testEdgeClass() { + Edge e1 = new Edge(10, 20, 30, 40); + assertEquals(20, e1.ymin); + assertEquals(40, e1.ymax); + assertTrue(e1.dy > 0); + + int scanX = e1.edgeScan(); + assertTrue(scanX >= 10); + + Edge[] edges = new Edge[] { + new Edge(50, 10, 50, 20), + new Edge(20, 10, 20, 20), + new Edge(80, 10, 80, 20) + }; + Edge.quicksort(edges, edges.length); + assertTrue(edges[0].xi <= edges[1].xi); + assertTrue(edges[1].xi <= edges[2].xi); + } + + @Test + @DisplayName("Test HODBounds and HODTransform") + public void testHODBoundsAndTransform() { + HODBounds bounds = new HODBounds(); + bounds.set(10, 20, 100, 200); + assertEquals(10, bounds.getHODUpperLeftX()); + assertEquals(20, bounds.getHODUpperLeftY()); + assertEquals(100, bounds.getHODLowerRightX()); + assertEquals(200, bounds.getHODLowerRightY()); + assertTrue(bounds.isHODValid()); + assertEquals(91, bounds.getWidth()); + assertEquals(181, bounds.getHeight()); + + Rectangle r = bounds.toHODRectangle(); + assertEquals(10, r.x); + assertEquals(20, r.y); + + HODTransform trans = new HODTransform(18, 32, 9, 16); // 2x scale + assertEquals(2.0, trans.getXTrans(), 0.001); + assertEquals(2.0, trans.getYTrans(), 0.001); + + Point pt = trans.calculate(new Point(10, 20)); + assertEquals(20, pt.x); + assertEquals(40, pt.y); + + Rectangle scaledR = trans.calculate(new Rectangle(5, 10, 15, 25)); + assertEquals(10, scaledR.x); + assertEquals(20, scaledR.y); + assertEquals(30, scaledR.width); + assertEquals(50, scaledR.height); + } + + @Test + @DisplayName("Test HODWallpaper Tile, Center, and Stretch") + public void testHODWallpaper() { + BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); + HODWallpaper wp = new HODWallpaper(img, HODWallpaper.HOD_CENTER); + assertEquals(HODWallpaper.HOD_CENTER, wp.getDisplay()); + + wp.setDisplay(HODWallpaper.HOD_TILE); + assertEquals(HODWallpaper.HOD_TILE, wp.getDisplay()); + + wp.setDisplay(HODWallpaper.HOD_STRETCH); + assertEquals(HODWallpaper.HOD_STRETCH, wp.getDisplay()); + + BufferedImage canvas = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics g = canvas.getGraphics(); + wp.paint(new java.awt.Canvas(), g, 0, 0, 100, 100); + g.dispose(); + } + + @Test + @DisplayName("Test HODGraphicsPlane Headless Facade Delegating to GraphicsPlane") + public void testHODGraphicsPlaneFacade() { + HODGraphicsPlane hodPlane = new HODGraphicsPlane(720, 384); + hodPlane.setHODGraphColor(2); // Red + hodPlane.drawHODLine(10, 10, 50, 50); + + HODBounds b = hodPlane.getHODBounds(); + assertTrue(b.getHODUpperLeftX() <= 10); + assertTrue(b.getHODLowerRightX() >= 50); + + hodPlane.drawHODArc(60, 60, 40, 40, 0, 180); + hodPlane.fillHODArc(100, 100, 30, 30, 0, 360); + + FillArea area = new FillArea(new int[] { 10, 30, 20 }, new int[] { 10, 10, 30 }, new int[] { 3 }, 1, Color.BLUE); + hodPlane.fillHODArea(area); + + assertNotNull(hodPlane.getHODImage()); + assertTrue(hodPlane.getDelegate().hasContent()); + } + + @Test + @DisplayName("Test HODProgramSymbolManager Facade") + public void testHODProgramSymbolManagerFacade() { + ProgramSymbolManager psm = new ProgramSymbolManager(); + HODProgramSymbolManager hodPsm = new HODProgramSymbolManager(psm); + + // Load a 1-character single-plane symbol set + byte[] loadps = new byte[22]; + loadps[0] = 0x01; // format 1 + loadps[1] = 0x41; // LCID 0x41 + loadps[2] = 0x41; // codepoint 0x41 + loadps[3] = 0x02; // RWS 2 + for (int i = 4; i < 22; i++) loadps[i] = (byte) 0xAA; + + psm.loadps(loadps); + assertNotNull(hodPsm.getSymbol(0x41, 0x41)); + + HODBitImage bi = hodPsm.getHODBitImage(0x41, 0x41); + assertNotNull(bi); + assertEquals(9, bi.getImageSize().width); + } + + @Test + @DisplayName("Test FilletPts and FillArea Compatibility Signatures") + public void testFilletAndFillAreaCompatibility() { + FilletPts fillet = new FilletPts(); + assertEquals(60, fillet.getPointsRequired(3)); + + int[] xIn = new int[] { 0, 50, 100 }; + int[] yIn = new int[] { 0, 100, 0 }; + int[] xOut = new int[100]; + int[] yOut = new int[100]; + int count = fillet.getFilletPoints(xIn, yIn, 3, xOut, yOut); + assertTrue(count > 0, "Fillet points should be generated"); + + FillArea fa = new FillArea(new int[] { 0, 50, 100 }, new int[] { 0, 100, 0 }, new int[] { 3 }, 1, Color.RED); + fa.setFillModeOR(); + assertTrue(fa.isFillModeOR()); + fa.set8x8Pattern(new byte[] { -1, 0, -1, 0, -1, 0, -1, 0 }); + Rectangle bounds = fa.getBounds(); + assertEquals(0, bounds.x); + assertEquals(0, bounds.y); + assertEquals(101, bounds.width); + assertEquals(101, bounds.height); + + Image img = fa.getImage(); + assertNotNull(img); + fa.dispose(); + } + + @Test + @DisplayName("Test Drop-in HostGraphics Package Facades") + public void testDropInPackageFacades() { + haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODBounds b = new haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODBounds(); + b.set(5, 5, 25, 25); + assertEquals(21, b.getWidth()); + + haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.Edge e = new haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.Edge(0, 0, 10, 10); + assertEquals(10, e.ymax); + + haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODTransform t = new haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODTransform(9, 16, 9, 16); + assertEquals(1.0, t.getXTrans(), 0.001); + + haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODWallpaper w = new haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODWallpaper(0); + assertEquals(0, w.getDisplay()); + + haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODGraphicsPlane gp = new haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODGraphicsPlane(); + assertNotNull(gp.getDelegate()); + + haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODColorChangeFilter cf = new haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics.HODColorChangeFilter(0xFF00FF00); + assertNotNull(cf); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/DS3270PPhase7Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/DS3270PPhase7Test.java new file mode 100644 index 0000000..6abf0ab --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/DS3270PPhase7Test.java @@ -0,0 +1,167 @@ +package haus.nightmare.lib3270j.printer; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.protocol.DS3270Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for Phase 7 DS3270P 3270 printer data stream processor, short[] and byte[] ingestion, + * LU1 (SCS) and LU3 dispatch, WSF LU1 structured fields, WCC processing, and lifecycle states. + */ +public class DS3270PPhase7Test { + + private PrinterConfig config; + private PD3270 pd; + private EbcdicTranslator translator; + private PrintSCS3270 scs; + private PrintPS3270 printPs; + private DS3270P ds3270p; + + @BeforeEach + public void setUp() { + config = new PrinterConfig("localhost", 23); + config.setFormFeedAtEoj(true); + config.setAutoFlushOnEoj(true); + pd = new PD3270(config); + pd.openPrinter(null); + translator = new EbcdicTranslator("037"); + scs = new PrintSCS3270(config, pd, translator); + printPs = new PrintPS3270(config, pd, translator); + ds3270p = new DS3270P(null, config, pd, scs, printPs, translator); + } + + @Test + public void testConstructorsAndAccessors() { + DS3270P defaultDs = new DS3270P(); + assertNotNull(defaultDs.getConfig()); + assertNotNull(defaultDs.getPD()); + assertNotNull(defaultDs.getPrintSCS()); + assertNotNull(defaultDs.getPrintPS()); + assertNotNull(defaultDs.getTranslator()); + + DS3270P configDs = new DS3270P(config); + assertSame(config, configDs.getConfig()); + + DS3270P partialDs = new DS3270P(config, pd, translator); + assertSame(pd, partialDs.getPD()); + assertSame(translator, partialDs.getTranslator()); + + ds3270p.setActiveLuType(PrinterConstants.LU_TYPE_1_SCS); + assertEquals(PrinterConstants.LU_TYPE_1_SCS, ds3270p.getActiveLuType()); + } + + @Test + public void testReceiveDataLU1WithByteArrayAndShortArray() { + ds3270p.setActiveLuType(PrinterConstants.LU_TYPE_1_SCS); + + // Byte array test + byte[] text1 = translator.stringToEbcdic("LINE 1 VIA BYTE ARRAY"); + ByteArrayOutputStream stream1 = new ByteArrayOutputStream(); + stream1.write(text1, 0, text1.length); + stream1.write(PrinterConstants.SCS_NL); + byte[] payload1 = stream1.toByteArray(); + + int consumed = ds3270p.receiveData(payload1, 0, payload1.length); + assertEquals(payload1.length, consumed); + + // Short array test + byte[] text2 = translator.stringToEbcdic("LINE 2 VIA SHORT ARRAY"); + short[] sArray = new short[text2.length + 1]; + for (int i = 0; i < text2.length; i++) { + sArray[i] = (short) (text2[i] & 0xFF); + } + sArray[text2.length] = (short) PrinterConstants.SCS_NL; + + consumed = ds3270p.receiveData(sArray, 0, sArray.length); + assertEquals(sArray.length, consumed); + + ds3270p.endOfRecord(); + + String captured = pd.getCapturedText(); + assertTrue(captured.contains("LINE 1 VIA BYTE ARRAY")); + assertTrue(captured.contains("LINE 2 VIA SHORT ARRAY")); + } + + @Test + public void testReceiveDataLU3With3270DS() { + ds3270p.setActiveLuType(PrinterConstants.LU_TYPE_3_DS); + + byte[] text = translator.stringToEbcdic("LU3 VIA DS3270P"); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.write(DS3270Constants.CMD_WRITE); + stream.write(PrinterConstants.WCC_START_PRINT_BIT); // Start print + stream.write(PrinterConstants.ORDER_SBA); + stream.write(0x40); + stream.write(0x40); + stream.write(text, 0, text.length); + + byte[] payload = stream.toByteArray(); + int consumed = ds3270p.receiveData(payload, 0, payload.length); + assertEquals(payload.length, consumed); + + String captured = pd.getCapturedText(); + assertTrue(captured.contains("LU3 VIA DS3270P")); + } + + @Test + public void testReceiveDataLU1PendingBuffer() { + ds3270p.setActiveLuType(PrinterConstants.LU_TYPE_1_SCS); + + byte[] part1 = translator.stringToEbcdic("PARTIAL "); + byte[] part2 = translator.stringToEbcdic("LINE BUFFERED"); + ds3270p.bufferDataLU1(part1, 0, part1.length); + ds3270p.bufferDataLU1(part2, 0, part2.length); + + // Process buffered data + ds3270p.receiveDataLU1(); + ds3270p.endOfRecord(); + + String captured = pd.getCapturedText(); + assertTrue(captured.contains("PARTIAL LINE BUFFERED")); + } + + @Test + public void testProcessWSFLU1StructuredField() { + ds3270p.setActiveLuType(PrinterConstants.LU_TYPE_1_SCS); + + byte[] msg = translator.stringToEbcdic("STRUCTURED FIELD DATA"); + ByteArrayOutputStream sfStream = new ByteArrayOutputStream(); + // SF length = 2 (len bytes) + 1 (id byte) + msg.length + int sfLen = 3 + msg.length; + sfStream.write((sfLen >> 8) & 0xFF); + sfStream.write(sfLen & 0xFF); + sfStream.write(0x65); // SCS data unit SF ID + sfStream.write(msg, 0, msg.length); + + byte[] sfPayload = sfStream.toByteArray(); + ds3270p.processWSFLU1(sfPayload, 0, sfPayload.length); + ds3270p.endOfRecord(); + + String captured = pd.getCapturedText(); + assertTrue(captured.contains("STRUCTURED FIELD DATA")); + } + + @Test + public void testProcessWCCAndEndOfFile() { + ds3270p.setActiveLuType(PrinterConstants.LU_TYPE_3_DS); + + // Test WCC with Start Print = 0 schedules flush + ds3270p.processWCC((short) 0x00); + + // Test WCC with Start Print = 1 flushes immediately + ds3270p.processWCC((short) PrinterConstants.WCC_START_PRINT_BIT); + + // End of file + ds3270p.endOfFile(); + assertEquals(1, pd.getPageCount()); + + // Reset state + ds3270p.reset(); + assertEquals(0, pd.getPageCount()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintDBCSPhase7Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintDBCSPhase7Test.java new file mode 100644 index 0000000..08a1e6d --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/PrintDBCSPhase7Test.java @@ -0,0 +1,141 @@ +package haus.nightmare.lib3270j.printer; + +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for Phase 7 DBCS printer extensions: PrintSCS3270DB and PrintPS3270DB. + * Verifies SO/SI state switching, double-byte character decoding, 2-cell spacing, + * ideographic spaces, and grid/ruling line attributes. + */ +public class PrintDBCSPhase7Test { + + private PrinterConfig config; + private PD3270 pd; + private EbcdicTranslator translator; + + @BeforeEach + public void setUp() { + // Use Japanese DBCS code page Cp930 + config = new PrinterConfig("localhost", 23); + config.setCodePage("930"); + pd = new PD3270(config); + pd.openPrinter(null); + translator = new EbcdicTranslator("930"); + } + + @Test + public void testPrintSCS3270DBSOSIModeSwitchingAndCharacterSpacing() { + PrintSCS3270DB scsDb = new PrintSCS3270DB(config, pd, translator); + assertFalse(scsDb.isDBCSMode()); + assertEquals(5, scsDb.getDBCSCPI()); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + // SBCS 'A' (0xC1) + stream.write(0xC1); + + // Shift Out (0x0E) to enter DBCS + stream.write(PrinterConstants.SCS_SO); + + // Ideographic blank (0x40 0x40 in EBCDIC) + stream.write(0x40); + stream.write(0x40); + + // Shift In (0x0F) to return to SBCS + stream.write(PrinterConstants.SCS_SI); + + // SBCS 'B' (0xC2) + stream.write(0xC2); + + byte[] payload = stream.toByteArray(); + scsDb.processHostData(payload, 0, payload.length); + scsDb.flushLineBuffer(); + + String captured = pd.getCapturedText(); + assertNotNull(captured); + assertTrue(captured.contains("A")); + assertTrue(captured.contains("\u3000")); + assertTrue(captured.contains("B")); + } + + @Test + public void testPrintSCS3270DBGridLines() { + PrintSCS3270DB scsDb = new PrintSCS3270DB(config, pd, translator); + + // Configure all 4 border grid lines + scsDb.setGridLines(true, true, true, true); + assertTrue(scsDb.hasLeftGrid()); + assertTrue(scsDb.hasRightGrid()); + assertTrue(scsDb.hasTopGrid()); + assertTrue(scsDb.hasBottomGrid()); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.write(PrinterConstants.SCS_SO); + stream.write(0x40); + stream.write(0x40); + stream.write(PrinterConstants.SCS_SI); + + scsDb.processHostData(stream.toByteArray(), 0, stream.size()); + scsDb.flushLineBuffer(); + + String captured = pd.getCapturedText(); + // Box borders should be present in captured output + assertTrue(captured.contains("┌")); + assertTrue(captured.contains("┐")); + assertTrue(captured.contains("└")); + assertTrue(captured.contains("┘")); + assertTrue(captured.contains("│")); + + // Clear grid lines + scsDb.clearGridLines(); + assertEquals(0, scsDb.getGridFlags()); + } + + @Test + public void testPrintPS3270DBDBCSBufferingAndGridLines() { + PrintPS3270DB psDb = new PrintPS3270DB(config, pd, translator); + + // Place a DBCS character '日' at row 0, col 5 + char kanji = '\u65E5'; + psDb.setDBCSChar(0, 5, kanji); + + int addrLeft = 5; + int addrRight = 6; + assertTrue(psDb.isDBCSCell(addrLeft)); + assertTrue(psDb.isDBCSLeft(addrLeft)); + assertTrue(psDb.isDBCSCell(addrRight)); + assertTrue(psDb.isDBCSRight(addrRight)); + assertFalse(psDb.isDBCSCell(0)); + + // Set top and bottom grid lines on the row + psDb.setGridLine(addrLeft, PrintPS3270DB.GRID_TOP | PrintPS3270DB.GRID_LEFT); + psDb.setGridLine(addrRight, PrintPS3270DB.GRID_BOTTOM | PrintPS3270DB.GRID_RIGHT); + + assertEquals(PrintPS3270DB.GRID_TOP | PrintPS3270DB.GRID_LEFT, psDb.getGridLine(addrLeft)); + assertEquals(PrintPS3270DB.GRID_BOTTOM | PrintPS3270DB.GRID_RIGHT, psDb.getGridLine(addrRight)); + + // Format and render line 0 + psDb.printLine(0, 80); + + String captured = pd.getCapturedText(); + assertTrue(captured.contains(String.valueOf(kanji))); + assertTrue(captured.contains("┌") || captured.contains("│") || captured.contains("─")); + } + + @Test + public void testPrintPS3270DBEraseBuffer() { + PrintPS3270DB psDb = new PrintPS3270DB(config, pd, translator); + psDb.setDBCSChar(1, 10, '\u672C'); + psDb.setGridLine(10, PrintPS3270DB.GRID_TOP); + + assertTrue(psDb.isDBCSCell(1 * 80 + 10)); + psDb.erasePrintBuffer(); + assertFalse(psDb.isDBCSCell(1 * 80 + 10)); + assertEquals(0, psDb.getGridLine(10)); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/TN3270PPackagePhase7Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/TN3270PPackagePhase7Test.java new file mode 100644 index 0000000..97e4eed --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/TN3270PPackagePhase7Test.java @@ -0,0 +1,135 @@ +package haus.nightmare.lib3270j.printer; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests verifying instantiation, inheritance, and compatibility of classes in: + * - haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.* + * - haus.nightmare.lib3270j.tn3270p.* + */ +public class TN3270PPackagePhase7Test { + + @Test + public void testENetworkECLTN3270PPackageClasses() { + PrinterConfig config = new PrinterConfig("localhost", 23); + + // Telnet3270EP + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.Telnet3270EP ep = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.Telnet3270EP(config); + assertNotNull(ep); + assertTrue(ep instanceof haus.nightmare.lib3270j.printer.Telnet3270EP); + + // DS3270P + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.DS3270P ds = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.DS3270P(config); + assertNotNull(ds); + assertTrue(ds instanceof haus.nightmare.lib3270j.printer.DS3270P); + + // PD3270 + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PD3270 pd = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PD3270(config); + assertNotNull(pd); + assertTrue(pd instanceof haus.nightmare.lib3270j.printer.PD3270); + + // PrintSCS3270 + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintSCS3270 scs = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintSCS3270(config, pd, null); + assertNotNull(scs); + assertTrue(scs instanceof haus.nightmare.lib3270j.printer.PrintSCS3270); + + // PrintPS3270 + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintPS3270 printPs = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintPS3270(config, pd, null); + assertNotNull(printPs); + assertTrue(printPs instanceof haus.nightmare.lib3270j.printer.PrintPS3270); + + // PrintSCS3270DB + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintSCS3270DB scsDb = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintSCS3270DB(config); + assertNotNull(scsDb); + assertTrue(scsDb instanceof haus.nightmare.lib3270j.printer.PrintSCS3270DB); + + // PrintPS3270DB + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintPS3270DB psDb = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrintPS3270DB(config); + assertNotNull(psDb); + assertTrue(psDb instanceof haus.nightmare.lib3270j.printer.PrintPS3270DB); + + // PrinterDefinitionTable & PDT + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrinterDefinitionTable pdt = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PrinterDefinitionTable("TEST", "Desc"); + assertEquals("TEST", pdt.getName()); + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PDT pdtAlias = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.PDT("ALIAS", "Desc"); + assertEquals("ALIAS", pdtAlias.getName()); + + // Timer & TimerEvent + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.Timer timer = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.Timer(100); + assertNotNull(timer); + haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.TimerEvent event = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270p.TimerEvent(timer, "Timer1"); + assertEquals("Timer1", event.getTimerId()); + } + + @Test + public void testTN3270PPackageClasses() { + PrinterConfig config = new PrinterConfig("localhost", 23); + + // Telnet3270EP + haus.nightmare.lib3270j.tn3270p.Telnet3270EP ep = + new haus.nightmare.lib3270j.tn3270p.Telnet3270EP(config); + assertNotNull(ep); + assertTrue(ep instanceof haus.nightmare.lib3270j.printer.Telnet3270EP); + + // DS3270P + haus.nightmare.lib3270j.tn3270p.DS3270P ds = + new haus.nightmare.lib3270j.tn3270p.DS3270P(config); + assertNotNull(ds); + assertTrue(ds instanceof haus.nightmare.lib3270j.printer.DS3270P); + + // PD3270 + haus.nightmare.lib3270j.tn3270p.PD3270 pd = + new haus.nightmare.lib3270j.tn3270p.PD3270(config); + assertNotNull(pd); + assertTrue(pd instanceof haus.nightmare.lib3270j.printer.PD3270); + + // PrintSCS3270 + haus.nightmare.lib3270j.tn3270p.PrintSCS3270 scs = + new haus.nightmare.lib3270j.tn3270p.PrintSCS3270(config, pd, null); + assertNotNull(scs); + + // PrintPS3270 + haus.nightmare.lib3270j.tn3270p.PrintPS3270 printPs = + new haus.nightmare.lib3270j.tn3270p.PrintPS3270(config, pd, null); + assertNotNull(printPs); + + // PrintSCS3270DB + haus.nightmare.lib3270j.tn3270p.PrintSCS3270DB scsDb = + new haus.nightmare.lib3270j.tn3270p.PrintSCS3270DB(config); + assertNotNull(scsDb); + + // PrintPS3270DB + haus.nightmare.lib3270j.tn3270p.PrintPS3270DB psDb = + new haus.nightmare.lib3270j.tn3270p.PrintPS3270DB(config); + assertNotNull(psDb); + + // PrinterDefinitionTable & PDT + haus.nightmare.lib3270j.tn3270p.PrinterDefinitionTable pdt = + new haus.nightmare.lib3270j.tn3270p.PrinterDefinitionTable("TEST", "Desc"); + assertEquals("TEST", pdt.getName()); + haus.nightmare.lib3270j.tn3270p.PDT pdtAlias = + new haus.nightmare.lib3270j.tn3270p.PDT("ALIAS", "Desc"); + assertEquals("ALIAS", pdtAlias.getName()); + + // Timer & TimerEvent + haus.nightmare.lib3270j.tn3270p.Timer timer = + new haus.nightmare.lib3270j.tn3270p.Timer(100); + assertNotNull(timer); + haus.nightmare.lib3270j.tn3270p.TimerEvent event = + new haus.nightmare.lib3270j.tn3270p.TimerEvent(timer, "Timer2"); + assertEquals("Timer2", event.getTimerId()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/TimerPhase7Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/TimerPhase7Test.java new file mode 100644 index 0000000..746f4ee --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/printer/TimerPhase7Test.java @@ -0,0 +1,128 @@ +package haus.nightmare.lib3270j.printer; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for Phase 7 Timer, TimerListener, and TimerEvent lifecycle and callbacks. + */ +public class TimerPhase7Test { + + @Test + public void testTimerExpirationCallback() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean fired = new AtomicBoolean(false); + + Timer timer = new Timer(50, new TimerListener() { + @Override + public void timerExpired(TimerEvent event) { + assertEquals("TestTimer", event.getTimerId()); + assertNotNull(event.getTimer()); + assertTrue(event.getTimestamp() > 0); + fired.set(true); + latch.countDown(); + } + }, false, "TestTimer"); + + assertEquals(50, timer.getInterval()); + assertEquals("TestTimer", timer.getTimerId()); + assertFalse(timer.isRepeating()); + + timer.start(); + assertTrue(timer.isRunning()); + + boolean completed = latch.await(500, TimeUnit.MILLISECONDS); + assertTrue(completed, "Timer did not fire within expected timeout"); + assertTrue(fired.get()); + assertFalse(timer.isRunning()); + } + + @Test + public void testTimerCancelPreventsFiring() throws InterruptedException { + AtomicBoolean fired = new AtomicBoolean(false); + + Timer timer = new Timer(150, new TimerListener() { + @Override + public void timerExpired(TimerEvent event) { + fired.set(true); + } + }); + + timer.start(); + assertTrue(timer.isRunning()); + timer.cancel(); + assertFalse(timer.isRunning()); + + Thread.sleep(250); + assertFalse(fired.get(), "Cancelled timer should not have fired"); + } + + @Test + public void testTimerRestartAndReset() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger count = new AtomicInteger(0); + + Timer timer = new Timer(80); + timer.addTimerListener(new TimerListener() { + @Override + public void timerExpired(TimerEvent event) { + count.incrementAndGet(); + latch.countDown(); + } + }); + + timer.start(); + Thread.sleep(30); + // Restart resets the countdown + timer.restart(); + assertTrue(timer.isRunning()); + + boolean completed = latch.await(300, TimeUnit.MILLISECONDS); + assertTrue(completed); + assertEquals(1, count.get()); + } + + @Test + public void testRepeatingTimer() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(3); + AtomicInteger count = new AtomicInteger(0); + + Timer timer = new Timer(30, new TimerListener() { + @Override + public void timerExpired(TimerEvent event) { + count.incrementAndGet(); + latch.countDown(); + } + }, true, "RepeatingTimer"); + + assertTrue(timer.isRepeating()); + timer.start(); + + boolean completed = latch.await(500, TimeUnit.MILLISECONDS); + timer.stop(); + assertFalse(timer.isRunning()); + + assertTrue(completed); + assertTrue(count.get() >= 3); + } + + @Test + public void testTimerListenerAddRemove() { + Timer timer = new Timer(100); + TimerListener listener = new TimerListener() { + @Override + public void timerExpired(TimerEvent event) {} + }; + + timer.addTimerListener(listener); + timer.removeTimerListener(listener); + // Should not throw or fail + assertNotNull(timer); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/DynamicScreenBufferTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/DynamicScreenBufferTest.java new file mode 100644 index 0000000..6a5dfe4 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/DynamicScreenBufferTest.java @@ -0,0 +1,88 @@ +package haus.nightmare.lib3270j.screen; + +import org.junit.jupiter.api.Test; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import static org.junit.jupiter.api.Assertions.*; + +public class DynamicScreenBufferTest { + + private final EbcdicTranslator translator = new EbcdicTranslator("037"); + + @Test + public void testDynamicScreenBufferInitialization() { + ScreenBuffer buffer = new ScreenBuffer(24, 80, 62, 160, translator); + assertEquals(24, buffer.getDefRows()); + assertEquals(80, buffer.getDefCols()); + assertEquals(62, buffer.getAltRows()); + assertEquals(160, buffer.getAltCols()); + assertEquals(62, buffer.getMaxRows()); + assertEquals(160, buffer.getMaxCols()); + assertEquals(24, buffer.getRows()); + assertEquals(80, buffer.getCols()); + assertFalse(buffer.isScreenAlt()); + } + + @Test + public void testDynamicScreenSwitching() { + ScreenBuffer buffer = new ScreenBuffer(24, 80, 62, 160, translator); + + // Erase to alternate (62x160) + buffer.erase(true); + assertTrue(buffer.isScreenAlt()); + assertEquals(62, buffer.getRows()); + assertEquals(160, buffer.getCols()); + + // Address beyond 4096 (e.g., row 50, col 50 = 50 * 160 + 50 = 8050) + int testAddr = 50 * 160 + 50; + assertTrue(testAddr > 4096); + ExtendedAttribute ea = buffer.getCell(testAddr); + assertNotNull(ea); + ea.ec = (byte) 0xC1; // EBCDIC 'A' + assertEquals((byte) 0xC1, buffer.getCell(testAddr).ec); + + // Erase to default (24x80) + buffer.erase(false); + assertFalse(buffer.isScreenAlt()); + assertEquals(24, buffer.getRows()); + assertEquals(80, buffer.getCols()); + } + + @Test + public void testDynamicAlternateDimensionsExpansion() { + ScreenBuffer buffer = new ScreenBuffer(24, 80, 43, 132, translator); + assertEquals(43, buffer.getMaxRows()); + assertEquals(132, buffer.getMaxCols()); + + // Expand to 62x160 dynamically + buffer.setAlternateDimensions(62, 160); + assertEquals(62, buffer.getAltRows()); + assertEquals(160, buffer.getAltCols()); + assertEquals(62, buffer.getMaxRows()); + assertEquals(160, buffer.getMaxCols()); + + buffer.erase(true); + assertEquals(62, buffer.getRows()); + assertEquals(160, buffer.getCols()); + + int maxAddr = 62 * 160 - 1; // 9919 + buffer.getCell(maxAddr).ec = (byte) 0xE9; // 'Z' + assertEquals((byte) 0xE9, buffer.getCell(maxAddr).ec); + } + + @Test + public void testTerminalModelDynamic() { + TerminalModel model = TerminalModel.IBM_DYNAMIC; + assertTrue(model.isDynamic()); + assertEquals(0, model.getModelNumber()); + assertTrue(model.isColor()); + assertEquals("IBM-DYNAMIC-E", model.getTerminalType()); + assertEquals("IBM-DYNAMIC", model.getBaseTerminalType()); + + ScreenBuffer buffer = new ScreenBuffer(model, translator); + assertEquals(24, buffer.getDefRows()); + assertEquals(80, buffer.getDefCols()); + assertEquals(62, buffer.getAltRows()); + assertEquals(160, buffer.getAltCols()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/security/Phase9SslSecurityTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/security/Phase9SslSecurityTest.java new file mode 100644 index 0000000..bcac959 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/security/Phase9SslSecurityTest.java @@ -0,0 +1,443 @@ +package haus.nightmare.lib3270j.security; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.*; + +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.ecl.ECLConnection; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.security.ssl.HODSSLECLSessionImpl; +import haus.nightmare.lib3270j.tls.TlsCertificateVerifier; +import haus.nightmare.lib3270j.tls.TlsTrustManager; + +import javax.net.ssl.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Comprehensive test suite for Phase 9: SSL/TLS Security Facade. + */ +public class Phase9SslSecurityTest { + + @TempDir + Path tempDir; + + private ConnectionConfig config; + + @BeforeEach + public void setUp() { + config = new ConnectionConfig("localhost", 992, TerminalModel.IBM_3279_4, true); + } + + // ========================================================================= + // 9.1 Constructors & Property Configuration + // ========================================================================= + + @Test + public void testConstructorsAndProperties() { + // Default constructor + HODSSLECLSessionImpl ssl1 = new HODSSLECLSessionImpl(); + assertNotNull(ssl1.getConfig()); + assertTrue(ssl1.isUseJSSE()); + assertTrue(ssl1.IsUseJSSE()); + + // ConnectionConfig constructor + HODSSLECLSessionImpl ssl2 = new HODSSLECLSessionImpl(config); + assertSame(config, ssl2.getConfig()); + assertSame(config, ssl2.GetConfig()); + + // ECLSession constructor + ECLSession session = new ECLSession("zos.example.com", 992, TerminalModel.IBM_3279_4, true); + HODSSLECLSessionImpl ssl3 = new HODSSLECLSessionImpl(session); + assertSame(session, ssl3.getSession()); + assertSame(session, ssl3.GetSession()); + + // ECLConnection constructor + ECLConnection conn = session.getConnection(); + HODSSLECLSessionImpl ssl4 = new HODSSLECLSessionImpl(conn); + assertSame(conn, ssl4.getConnection()); + assertSame(conn, ssl4.GetConnection()); + + // Properties constructor + Properties props = new Properties(); + props.setProperty(HODSSLECLSessionImpl.SESSION_SSL_USE_JSSE, "true"); + props.setProperty(HODSSLECLSessionImpl.SESSION_CERT_NAME, "MyCertAlias"); + props.setProperty(HODSSLECLSessionImpl.SESSION_CERT_URL, "/path/to/cert.p12"); + props.setProperty(HODSSLECLSessionImpl.SESSION_CERT_PASSWORD, "certpass"); + props.setProperty(HODSSLECLSessionImpl.SESSION_TRUSTSTORE, "/path/to/trust.p12"); + props.setProperty(HODSSLECLSessionImpl.SESSION_TRUSTSTORE_PASSWORD, "trustpass"); + props.setProperty(HODSSLECLSessionImpl.SESSION_TRUSTSTORE_TYPE, "PKCS12"); + props.setProperty(HODSSLECLSessionImpl.SESSION_TLS_VERSION, "TLSv1.3"); + + HODSSLECLSessionImpl ssl5 = new HODSSLECLSessionImpl(props); + assertEquals("MyCertAlias", ssl5.getCertificateAlias()); + assertEquals("MyCertAlias", ssl5.GetCertificateAlias()); + assertEquals("MyCertAlias", ssl5.getCertificateName()); + assertEquals("MyCertAlias", ssl5.GetCertificateName()); + assertEquals("/path/to/cert.p12", ssl5.getKeyStorePath()); + assertEquals("/path/to/cert.p12", ssl5.GetKeyStorePath()); + assertEquals("certpass", ssl5.getKeyStorePassword()); + assertEquals("/path/to/trust.p12", ssl5.getTrustStorePath()); + assertEquals("trustpass", ssl5.getTrustStorePassword()); + assertEquals("PKCS12", ssl5.getTrustStoreType()); + assertEquals("TLSv1.3", ssl5.getTLSProtocolVersion()); + assertEquals("TLSv1.3", ssl5.GetTLSProtocolVersion()); + + // Get/Set properties + ssl5.setProperty("custom.security.level", "HIGH"); + assertEquals("HIGH", ssl5.getProperty("custom.security.level")); + assertEquals("HIGH", ssl5.GetProperty("custom.security.level")); + assertEquals("DEFAULT", ssl5.getProperty("nonexistent", "DEFAULT")); + assertEquals("DEFAULT", ssl5.GetProperty("nonexistent", "DEFAULT")); + } + + // ========================================================================= + // 9.2 Client Keystore Loading & KeyManager Generation + // ========================================================================= + + @Test + public void testClientKeyStoreLoadingAndKeyManagers() throws Exception { + HODSSLECLSessionImpl ssl = new HODSSLECLSessionImpl(config); + + // Generate a test PKCS12 keystore in-memory + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, "testpwd".toCharArray()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ks.store(baos, "testpwd".toCharArray()); + byte[] ksBytes = baos.toByteArray(); + + // 1. Test loadKeyStore from InputStream + KeyStore loadedKs = ssl.loadKeyStore(new ByteArrayInputStream(ksBytes), "testpwd".toCharArray(), "PKCS12"); + assertNotNull(loadedKs); + assertSame(loadedKs, ssl.getKeyStore()); + assertSame(loadedKs, ssl.GetKeyStore()); + + // 2. Test loadKeyStore from file path + File ksFile = tempDir.resolve("client_keystore.p12").toFile(); + try (FileOutputStream fos = new FileOutputStream(ksFile)) { + fos.write(ksBytes); + } + ssl.setKeyStorePath(ksFile.getAbsolutePath()); + ssl.setKeyStorePassword("testpwd"); + ssl.setKeyStoreType("PKCS12"); + ssl.setCertificateAlias("client_alias"); + + assertEquals(ksFile.getAbsolutePath(), ssl.getKeyStorePath()); + assertEquals("testpwd", ssl.getKeyStorePassword()); + assertEquals("PKCS12", ssl.getKeyStoreType()); + assertEquals("client_alias", ssl.getCertificateAlias()); + + KeyManager[] kms = ssl.createKeyManagers(); + assertNotNull(kms); + assertTrue(kms.length > 0); + assertSame(kms, ssl.getKeyManagers()); + assertSame(kms, ssl.GetKeyManagers()); + + // 3. Test explicit setKeyManagers + ssl.setKeyManagers(kms); + assertSame(kms, ssl.getKeyManagers()); + } + + // ========================================================================= + // 9.3 Truststore & CustomizedCAs Handling + // ========================================================================= + + @Test + public void testTrustStoreAndCustomizedCAs() throws Exception { + HODSSLECLSessionImpl ssl = new HODSSLECLSessionImpl(config); + + // Generate in-memory truststore with standard default HoD password "hod" + KeyStore ts = KeyStore.getInstance("PKCS12"); + ts.load(null, "hod".toCharArray()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ts.store(baos, "hod".toCharArray()); + byte[] tsBytes = baos.toByteArray(); + + // 1. Test loadTrustStore from InputStream + KeyStore loadedTs = ssl.loadTrustStore(new ByteArrayInputStream(tsBytes), "hod".toCharArray(), "PKCS12"); + assertNotNull(loadedTs); + assertSame(loadedTs, ssl.getTrustStore()); + assertSame(loadedTs, ssl.GetTrustStore()); + + // 2. Test loadTrustStore from file path + File tsFile = tempDir.resolve("custom_truststore.p12").toFile(); + try (FileOutputStream fos = new FileOutputStream(tsFile)) { + fos.write(tsBytes); + } + ssl.setTrustStorePath(tsFile.getAbsolutePath()); + ssl.setTrustStorePassword("hod"); + ssl.setTrustStoreType("PKCS12"); + + assertEquals(tsFile.getAbsolutePath(), ssl.getTrustStorePath()); + assertEquals("hod", ssl.getTrustStorePassword()); + assertEquals("PKCS12", ssl.getTrustStoreType()); + + TrustManager[] tms = ssl.createTrustManagers(); + assertNotNull(tms); + assertEquals(1, tms.length); + assertTrue(tms[0] instanceof TlsTrustManager); + assertSame(tms, ssl.getTrustManagers()); + assertSame(tms, ssl.GetTrustManagers()); + + // 3. Test CustomizedCAs ClassLoader lookup with default password "hod" + ClassLoader mockCl = new ClassLoader(getClass().getClassLoader()) { + @Override + public java.io.InputStream getResourceAsStream(String name) { + if ("CustomizedCAs.p12".equals(name)) { + return new ByteArrayInputStream(tsBytes); + } + return super.getResourceAsStream(name); + } + }; + + HODSSLECLSessionImpl sslCustomCAs = new HODSSLECLSessionImpl(); + sslCustomCAs.setCustomizedCAsClassLoader(mockCl); + assertSame(mockCl, sslCustomCAs.getCustomizedCAsClassLoader()); + assertSame(mockCl, sslCustomCAs.GetCustomizedCAsClassLoader()); + + KeyStore cCAs = sslCustomCAs.loadCustomizedCAs(); + assertNotNull(cCAs, "Expected loadCustomizedCAs to find CustomizedCAs.p12 via ClassLoader with default password"); + } + + // ========================================================================= + // 9.4 TLS Protocol & Cipher Suite Negotiation + // ========================================================================= + + @Test + public void testTlsProtocolAndCipherSuiteNegotiation() throws Exception { + HODSSLECLSessionImpl ssl = new HODSSLECLSessionImpl(config); + + // Default enabled protocols (TLSv1.3 and TLSv1.2) + String[] defaultProtocols = ssl.getEnabledProtocols(); + assertNotNull(defaultProtocols); + assertTrue(defaultProtocols.length >= 2); + assertEquals("TLSv1.3", defaultProtocols[0]); + assertEquals("TLSv1.2", defaultProtocols[1]); + + // Supported protocols + String[] supportedProtocols = ssl.getSupportedProtocols(); + assertNotNull(supportedProtocols); + assertTrue(supportedProtocols.length > 0); + + // Set explicit TLS protocol version + ssl.setTLSProtocolVersion("TLSv1.2"); + assertEquals("TLSv1.2", ssl.getTLSProtocolVersion()); + assertEquals("TLSv1.2", ssl.GetTLSProtocolVersion()); + assertArrayEquals(new String[] { "TLSv1.2" }, ssl.getEnabledProtocols()); + + // Set multiple protocols + ssl.setEnabledProtocols(new String[] { "TLSv1.3", "TLSv1.2" }); + assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2" }, ssl.getEnabledProtocols()); + assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2" }, ssl.GetEnabledProtocols()); + + // Cipher suites + String[] supportedCiphers = ssl.getSupportedCipherSuites(); + assertNotNull(supportedCiphers); + assertTrue(supportedCiphers.length > 0); + + String[] testCiphers = new String[] { + supportedCiphers[0], + supportedCiphers.length > 1 ? supportedCiphers[1] : supportedCiphers[0] + }; + ssl.setEnabledCipherSuites(testCiphers); + assertArrayEquals(testCiphers, ssl.getEnabledCipherSuites()); + assertArrayEquals(testCiphers, ssl.GetEnabledCipherSuites()); + + // Test applySocketSettings on dummy SSLSocket + SSLContext ctx = ssl.createSSLContext(); + SSLSocket dummySocket = (SSLSocket) ctx.getSocketFactory().createSocket(); + ssl.applySocketSettings(dummySocket); + assertTrue(dummySocket.getEnabledProtocols().length > 0); + dummySocket.close(); + } + + // ========================================================================= + // 9.5 SSLContext & Socket Factory Creation + // ========================================================================= + + @Test + public void testSSLContextAndSocketFactory() throws Exception { + HODSSLECLSessionImpl ssl = new HODSSLECLSessionImpl(config); + ssl.setSecurityProtocol("TLS"); + assertEquals("TLS", ssl.getSecurityProtocol()); + assertEquals("TLS", ssl.GetSecurityProtocol()); + + SSLContext ctx = ssl.createSSLContext(); + assertNotNull(ctx); + assertSame(ctx, ssl.getSSLContext()); + assertSame(ctx, ssl.GetSSLContext()); + + SSLSocketFactory factory = ssl.getSSLSocketFactory(); + assertNotNull(factory); + assertSame(factory, ssl.GetSSLSocketFactory()); + + // initSSLContext alias + SSLContext ctx2 = ssl.initSSLContext(); + assertNotNull(ctx2); + + // Explicit setSSLContext + ssl.setSSLContext(ctx); + assertSame(ctx, ssl.getSSLContext()); + } + + // ========================================================================= + // 9.6 Interactive Certificate Verification & Bypass + // ========================================================================= + + @Test + public void testInteractiveCertificateVerificationAndBypass() throws Exception { + HODSSLECLSessionImpl ssl = new HODSSLECLSessionImpl(config); + + // Verification bypass + ssl.setTlsVerifyCert(false); + assertFalse(ssl.isTlsVerifyCert()); + assertFalse(ssl.IsTlsVerifyCert()); + + TrustManager[] tmsBypass = ssl.createTrustManagers(); + assertNotNull(tmsBypass); + X509TrustManager tm = (X509TrustManager) tmsBypass[0]; + assertDoesNotThrow(() -> tm.checkServerTrusted(new X509Certificate[0], "RSA")); + + // Verifier callback + ssl.setTlsVerifyCert(true); + assertTrue(ssl.isTlsVerifyCert()); + + AtomicBoolean verifierCalled = new AtomicBoolean(false); + TlsCertificateVerifier customVerifier = (chain, authType, exception) -> { + verifierCalled.set(true); + return true; + }; + ssl.setCertificateVerifier(customVerifier); + assertSame(customVerifier, ssl.getCertificateVerifier()); + assertSame(customVerifier, ssl.GetCertificateVerifier()); + + TrustManager[] tmsVerifier = ssl.createTrustManagers(); + X509TrustManager tm2 = (X509TrustManager) tmsVerifier[0]; + assertDoesNotThrow(() -> tm2.checkServerTrusted(new X509Certificate[0], "RSA")); + assertTrue(verifierCalled.get(), "Expected verifier callback to be invoked"); + + // Verifier rejection + ssl.setCertificateVerifier(TlsCertificateVerifier.REJECT_ALL); + TrustManager[] tmsReject = ssl.createTrustManagers(); + X509TrustManager tmReject = (X509TrustManager) tmsReject[0]; + assertThrows(CertificateException.class, () -> tmReject.checkServerTrusted(new X509Certificate[0], "RSA")); + } + + // ========================================================================= + // 9.7 Drop-in Canonical Facade (haus.nightmare.lib3270j.eNetwork.security.ssl) + // ========================================================================= + + @Test + public void testCanonicalDropInFacade() { + haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl facade = + new haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl(); + assertNotNull(facade); + assertTrue(facade instanceof HODSSLECLSessionImpl); + + haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl facadeConfig = + new haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl(config); + assertSame(config, facadeConfig.getConfig()); + + ECLSession session = new ECLSession(); + haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl facadeSession = + new haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl(session); + assertSame(session, facadeSession.getSession()); + + ECLConnection conn = session.getConnection(); + haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl facadeConn = + new haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl(conn); + assertSame(conn, facadeConn.getConnection()); + + Properties props = new Properties(); + props.setProperty(HODSSLECLSessionImpl.SESSION_CERT_NAME, "CanonicalCert"); + haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl facadeProps = + new haus.nightmare.lib3270j.eNetwork.security.ssl.HODSSLECLSessionImpl(props); + assertEquals("CanonicalCert", facadeProps.getCertificateAlias()); + } + + // ========================================================================= + // 9.8 ECLSession & ECLConnection Integration + // ========================================================================= + + @Test + public void testECLSessionAndConnectionIntegration() { + ECLSession session = new ECLSession("mainframe.corp.com", 992, TerminalModel.IBM_3279_4, true); + assertNotNull(session.getSSLSessionImpl()); + assertSame(session.getSSLSessionImpl(), session.GetSSLSessionImpl()); + + ECLConnection connection = session.getConnection(); + assertNotNull(connection.getSSLSessionImpl()); + assertSame(connection.getSSLSessionImpl(), connection.GetSSLSessionImpl()); + + // Synchronizing properties + connection.setCertificateName("PROD_KEY"); + assertEquals("PROD_KEY", connection.getCertificateName()); + assertEquals("PROD_KEY", session.getClient().getConfig().getKeyStoreAlias()); + + connection.setCertificateURL("/keystores/client.p12"); + assertEquals("/keystores/client.p12", connection.getCertificateURL()); + assertEquals("/keystores/client.p12", session.getClient().getConfig().getKeyStorePath()); + + connection.setCertificatePassword("prodsecret"); + assertEquals("prodsecret", connection.getCertificatePassword()); + assertEquals("prodsecret", session.getClient().getConfig().getKeyStorePassword()); + + connection.setJSSETrustStore("/keystores/trust.p12"); + assertEquals("/keystores/trust.p12", connection.getJSSETrustStore()); + assertEquals("/keystores/trust.p12", session.getClient().getConfig().getTrustStorePath()); + + connection.setJSSETrustStorePassword("trustsecret"); + assertEquals("trustsecret", connection.getJSSETrustStorePassword()); + assertEquals("trustsecret", session.getClient().getConfig().getTrustStorePassword()); + + connection.setJSSETrustStoreType("PKCS12"); + assertEquals("PKCS12", connection.getJSSETrustStoreType()); + assertEquals("PKCS12", session.getClient().getConfig().getTrustStoreType()); + + connection.setTLSProtocolVersion("TLSv1.3"); + assertEquals("TLSv1.3", connection.getTLSProtocolVersion()); + assertEquals("TLSv1.3", session.getClient().getConfig().getSslProtocol()); + } + + // ========================================================================= + // 9.9 Security Status & Key Strength + // ========================================================================= + + @Test + public void testSecurityStatusAndKeyStrength() { + HODSSLECLSessionImpl ssl = new HODSSLECLSessionImpl(); + + // When inactive + assertNull(ssl.getSSLSession()); + assertNull(ssl.GetSSLSession()); + assertEquals(0, ssl.getKeyStrength()); + assertEquals(0, ssl.GetKeyStrength()); + assertEquals("NONE", ssl.getCipherSuite()); + assertEquals("NONE", ssl.GetCipherSuite()); + assertEquals(0, ssl.getPeerCertificates().length); + assertEquals(0, ssl.GetPeerCertificates().length); + assertEquals(0, ssl.getLocalCertificates().length); + assertEquals(0, ssl.GetLocalCertificates().length); + assertFalse(ssl.isAuthenticated()); + assertFalse(ssl.IsAuthenticated()); + assertFalse(ssl.isConnected()); + assertFalse(ssl.IsConnected()); + + // Reset and close + assertDoesNotThrow(ssl::close); + assertDoesNotThrow(ssl::Close); + assertDoesNotThrow(ssl::reset); + assertDoesNotThrow(ssl::Reset); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/DynamicTelnetFSMTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/DynamicTelnetFSMTest.java new file mode 100644 index 0000000..a6d9ec8 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/DynamicTelnetFSMTest.java @@ -0,0 +1,122 @@ +package haus.nightmare.lib3270j.telnet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +public class DynamicTelnetFSMTest { + + private ConnectionConfig config; + private ScreenBuffer screenBuffer; + private DataStreamProcessor dsProcessor; + private TelnetFSM fsm; + private MockConnection connection; + + private static class MockConnection extends TelnetConnection { + final List sentData = new ArrayList<>(); + + MockConnection(ConnectionConfig config, TelnetFSM fsm) { + super(config, fsm); + } + + @Override + public synchronized void sendRaw(byte[] data) { + sentData.add(data.clone()); + } + + @Override + public synchronized void sendRaw(byte[] data, int offset, int length) { + byte[] b = new byte[length]; + System.arraycopy(data, offset, b, 0, length); + sentData.add(b); + } + } + + @BeforeEach + public void setup() { + config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_DYNAMIC); + config.setDynamicDimensions(62, 160); + screenBuffer = new ScreenBuffer(24, 80, 62, 160, new EbcdicTranslator()); + dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator()); + fsm = new TelnetFSM(config, screenBuffer, dsProcessor); + connection = new MockConnection(config, fsm); + fsm.setConnection(connection); + } + + private void feedBytes(int... bytes) { + for (int b : bytes) { + fsm.feedByte(b & 0xFF); + } + } + + @Test + public void testCandidateTerminalTypesIncludesDynamic() { + // Feed DO TERMINAL-TYPE: IAC DO TTYPE + feedBytes(255, 253, 24); + + // Host requests TTYPE: IAC SB TTYPE SEND IAC SE + feedBytes(255, 250, 24, 1, 255, 240); + + // Verify sent response is IBM-DYNAMIC-E + assertFalse(connection.sentData.isEmpty()); + byte[] lastSent = connection.sentData.get(connection.sentData.size() - 1); + String s = new String(lastSent); + assertTrue(s.contains("IBM-DYNAMIC-E") || s.contains("IBM-DYNAMIC"), + "Expected terminal type negotiation to send IBM-DYNAMIC, got: " + s); + } + + @Test + public void testBindWithQuerySSIZE03SetsDynamicDimensions() { + // Simulate TN3270E BIND packet with SSIZE = 0x03 + // EH_SIZE = 5 bytes header + BIND parameters (at least 25 bytes) + byte[] bindData = new byte[5 + 26]; + // BIND_OFF_RD = 20, BIND_OFF_CD = 21, BIND_OFF_RA = 22, BIND_OFF_CA = 23, BIND_OFF_SSIZE = 24 + bindData[5 + 20] = 24; // Primary rows + bindData[5 + 21] = 80; // Primary cols + bindData[5 + 22] = 0; // Alt rows + bindData[5 + 23] = 0; // Alt cols + bindData[5 + 24] = 0x03; // SSIZE = 0x03 (Query) + + fsm.process_bind(bindData, 0, 1); + + assertEquals(62, screenBuffer.getAltRows()); + assertEquals(160, screenBuffer.getAltCols()); + } + + @Test + public void testBindWithExplicitSSIZE7FSetsCustomDimensions() { + // Simulate TN3270E BIND packet with SSIZE = 0x7F and custom 50x132 dimensions + byte[] bindData = new byte[5 + 26]; + bindData[5 + 20] = 24; // Primary rows + bindData[5 + 21] = 80; // Primary cols + bindData[5 + 22] = 50; // Alt rows + bindData[5 + 23] = (byte) 132; // Alt cols + bindData[5 + 24] = 0x7F; // SSIZE = 0x7F (Explicit) + + fsm.process_bind(bindData, 0, 1); + + assertEquals(50, screenBuffer.getAltRows()); + assertEquals(132, screenBuffer.getAltCols()); + } + + @Test + public void testParseHostStringWithDynamicPrefix() { + ConnectionConfig cfg = ConnectionConfig.parseHostString("dynamic:mainframe.example.com:23", 23, null); + assertTrue(cfg.isDynamicModel()); + assertEquals(62, cfg.getDynamicRows()); + assertEquals(160, cfg.getDynamicCols()); + + ConnectionConfig cfgCustom = ConnectionConfig.parseHostString("dynamic[43x132]:mainframe.example.com:23", 23, null); + assertTrue(cfgCustom.isDynamicModel()); + assertEquals(43, cfgCustom.getDynamicRows()); + assertEquals(132, cfgCustom.getDynamicCols()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/tn3270/Phase3ProtocolEngineTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/tn3270/Phase3ProtocolEngineTest.java new file mode 100644 index 0000000..0291d02 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/tn3270/Phase3ProtocolEngineTest.java @@ -0,0 +1,353 @@ +package haus.nightmare.lib3270j.tn3270; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ExtendedAttribute; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.telnet.TelnetFSM; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +import java.util.concurrent.atomic.AtomicReference; + +public class Phase3ProtocolEngineTest { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + private DataStreamProcessor dsp; + private InputProcessor inputProcessor; + private TelnetFSM fsm; + private ECLSession session; + + @BeforeEach + public void setup() { + translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator); + dsp = new DataStreamProcessor(screen, translator); + fsm = new TelnetFSM(new haus.nightmare.lib3270j.ConnectionConfig("localhost", 23), screen, dsp); + inputProcessor = new InputProcessor(screen, translator, fsm); + dsp.setInputProcessor(inputProcessor); + session = new ECLSession("localhost", 23, TerminalModel.IBM_3278_2); + } + + // ========================================== + // 3.1 com.ibm.eNetwork.ECL.tn3270.DS3270 + // ========================================== + + @Test + public void testDS3270ConstantsAndInstantiation() { + DS3270 ds = new DS3270(dsp); + assertNotNull(ds.getDataStreamProcessor()); + + // Verify command constants match HoD specifications + assertEquals(1, DS3270.CMD_W); + assertEquals(5, DS3270.CMD_EW); + assertEquals(13, DS3270.CMD_EWA); + assertEquals(2, DS3270.CMD_RB); + assertEquals(6, DS3270.CMD_RM); + assertEquals(14, DS3270.CMD_RMA); + assertEquals(15, DS3270.CMD_EAU); + assertEquals(17, DS3270.CMD_WSF); + + assertEquals(241, DS3270.SNA_CMD_W); + assertEquals(245, DS3270.SNA_CMD_EW); + assertEquals(126, DS3270.SNA_CMD_EWA); + assertEquals(242, DS3270.SNA_CMD_RB); + assertEquals(246, DS3270.SNA_CMD_RM); + assertEquals(110, DS3270.SNA_CMD_RMA); + assertEquals(111, DS3270.SNA_CMD_EAU); + assertEquals(243, DS3270.SNA_CMD_WSF); + + // Verify orders + assertEquals(29, DS3270.ORD_SF); + assertEquals(41, DS3270.ORD_SFE); + assertEquals(17, DS3270.ORD_SBA); + assertEquals(40, DS3270.ORD_SA); + assertEquals(44, DS3270.ORD_MF); + assertEquals(19, DS3270.ORD_IC); + assertEquals(5, DS3270.ORD_PT); + assertEquals(60, DS3270.ORD_RA); + assertEquals(18, DS3270.ORD_EUA); + assertEquals(8, DS3270.ORD_GE); + + // Verify default query lists + assertEquals(21, ds.qreplylist.length); + assertEquals(21, ds.queryEquiv.length); + assertEquals(64, ds.attr_conversion_table.length); + + // Verify drop-in eNetwork facade + haus.nightmare.lib3270j.eNetwork.ECL.tn3270.DS3270 facade = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270.DS3270(dsp); + assertNotNull(facade.getDataStreamProcessor()); + assertTrue(facade instanceof DS3270); + } + + @Test + public void testDS3270OrderExecution() { + DS3270 ds = new DS3270(dsp); + screen.erase(false); + + // SBA to pos 80 (Row 1 Col 0) + ds.processSBA(80); + assertEquals(80, screen.getBufferAddress()); + + // SF unprotected field + ds.processSF((byte) FA_PRINTABLE); + assertTrue(screen.isFormatted()); + assertTrue(screen.getCell(80).isFieldAttribute()); + assertEquals(81, screen.getBufferAddress()); + + // IC (Insert Cursor) + ds.processIC(); + assertEquals(81, screen.getCursorAddress()); + + // SA foreground red + ds.processSA(XA_FOREGROUND, 0xF2); + + // RA fill with 'A' (EBCDIC 0xC1) up to pos 90 + ds.processRA(90, 0xC1); + assertEquals(90, screen.getBufferAddress()); + assertEquals('A', screen.getCell(81).ucs4); + assertEquals('A', screen.getCell(89).ucs4); + + // EUA (Erase Unprotected to Address) up to pos 85 + ds.processSBA(81); + ds.processEUA(85); + assertEquals(0, screen.getCell(81).ucs4); + assertEquals(0, screen.getCell(84).ucs4); + assertEquals('A', screen.getCell(85).ucs4); + + // GE (Graphic Escape) + ds.processSBA(95); + ds.processGE(0xC1); + assertEquals(96, screen.getBufferAddress()); + + // WCC alarm, restore, reset MDT + inputProcessor.setKeyboardLocked(true); + ds.processWCC((short) (DS3270.WCC_RESTORE | DS3270.WCC_RMDT | DS3270.WCC_ALARM)); + assertFalse(inputProcessor.isKeyboardLocked()); + } + + @Test + public void testDS3270ShortBufferProcessingAndSendAid() { + AtomicReference sentData = new AtomicReference<>(); + dsp.setOutputSender(sentData::set); + + DS3270 ds = new DS3270(dsp); + + // Build a short[] stream: Write + WCC + SBA(0, 0) + 'H', 'E', 'L', 'L', 'O' + short[] stream = new short[] { + DS3270.SNA_CMD_EW, + (short) (DS3270.WCC_RESET | DS3270.WCC_RESTORE), + DS3270.ORD_SBA, 0x40, 0x40, // Address 0 + 0xC8, 0xC5, 0xD3, 0xD3, 0xD6 // EBCDIC "HELLO" + }; + ds.processData(stream, 0, stream.length); + + assertEquals('H', screen.getCell(0).ucs4); + assertEquals('E', screen.getCell(1).ucs4); + assertEquals('L', screen.getCell(2).ucs4); + assertEquals('L', screen.getCell(3).ucs4); + assertEquals('O', screen.getCell(4).ucs4); + + // sendAid + ds.sendAid((short) AID_ENTER, 50); + assertNotNull(sentData.get()); + assertTrue(sentData.get().length > 0); + assertEquals((byte) AID_ENTER, sentData.get()[0]); + } + + @Test + public void testQrElemAndQueryReplyObject() { + qr_elem elem = new qr_elem(qr_elem.TYPE_COLOR, qr_elem.SEND_YES); + assertEquals(qr_elem.TYPE_COLOR, elem.getType()); + assertEquals(qr_elem.SEND_YES, elem.getSendFlag()); + assertTrue(elem.isSendEnabled()); + + elem.setSendEnabled(false); + assertFalse(elem.isSendEnabled()); + assertEquals(qr_elem.SEND_NO, elem.getSendFlag()); + + // Drop-in eNetwork facade + haus.nightmare.lib3270j.eNetwork.ECL.tn3270.qr_elem facadeElem = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270.qr_elem(qr_elem.TYPE_DDM, qr_elem.SEND_YES); + assertTrue(facadeElem instanceof qr_elem); + assertEquals(qr_elem.TYPE_DDM, facadeElem.getType()); + + // DS3270.QueryReplyObject + DS3270 ds = new DS3270(dsp); + DS3270.QueryReplyObject qro = ds.new QueryReplyObject(); + qro.addReply("\u0000\u0005\u0081\u0086\u0000"); + byte[] bytes = qro.getBytes(); + assertNotNull(bytes); + assertTrue(bytes.length > 0); + + // qr_update and qr_set + qr_elem[] list = new qr_elem[] { + new qr_elem(129, 0), + new qr_elem(134, 0) + }; + ds.qr_update(list, (char) 134, (short) 1, 2); + assertEquals(1, list[1].getSendFlag()); + + ds.qr_set(list, (short) 1, 2); + assertEquals(1, list[0].getSendFlag()); + assertEquals(1, list[1].getSendFlag()); + } + + // ========================================== + // 3.2 com.ibm.eNetwork.ECL.tn3270.PS3270 + // ========================================== + + @Test + public void testPS3270KeystrokesAndLocking() { + PS3270 ps = new PS3270(screen, inputProcessor, translator); + screen.erase(false); + screen.setCursorAddress(0); + + // Printable char key down + boolean handled = ps.keyDown('A', false); + assertTrue(handled); + assertEquals('A', screen.getCell(0).ucs4); + + // CharKeyStrokes direct + ps.CharKeyStrokes('B'); + assertEquals('B', screen.getCell(1).ucs4); + + // Keyboard locking + ps.lockKeyboard(7); // TWAIT + assertTrue(ps.islocked_TWAIT()); + assertTrue(inputProcessor.isKeyboardLocked()); + + // Keystroke ignored when locked + boolean lockedIgnored = ps.keyDown('C', false); + assertFalse(lockedIgnored); + assertEquals(0, screen.getCell(2).ucs4); + + // Unlock with Escape key (27) + boolean unlocked = ps.keyDown(27, false); + assertTrue(unlocked); + assertFalse(inputProcessor.isKeyboardLocked()); + + // Special EAB manipulation + ps.SetSpecialEAB(10, (byte) 0x55); + assertEquals((byte) 0x55, ps.GetSpecialEAB(10)); + assertEquals((byte) 0x55, screen.getSpecialEAB(10)); + + // LU-LU Session flag + assertFalse(ps.is_LULU_Session()); + ps.set_LULU_Session(true); + assertTrue(ps.is_LULU_Session()); + + // Drop-in eNetwork facade + haus.nightmare.lib3270j.eNetwork.ECL.tn3270.PS3270 facadePs = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270.PS3270(screen, inputProcessor, translator); + assertTrue(facadePs instanceof PS3270); + } + + @Test + public void testPS3270DBCSInputChar() { + PS3270 ps = new PS3270(screen, inputProcessor, translator); + screen.erase(false); + + // Place unprotected field at 0 + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setCursorAddress(1); + + int res = ps.DBCSinputChar('X', 1); + assertEquals(1, res); + assertEquals('X', screen.getCell(1).ucs4); + + // On protected field, DBCSinputChar should return 0 + screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT)); + int resProt = ps.DBCSinputChar('Y', 11); + assertEquals(0, resProt); + } + + // ========================================== + // 3.3 com.ibm.eNetwork.ECL.tn3270.Telnet3270E + // ========================================== + + @Test + public void testTelnet3270EConstantsAndFunctions() { + Telnet3270E telnet = new Telnet3270E(fsm, null); + + assertEquals(40, Telnet3270E.TELOPT_TN3270E); + assertEquals(0, Telnet3270E.TN3270E_ASSOCIATE); + assertEquals(1, Telnet3270E.TN3270E_CONNECT); + assertEquals(2, Telnet3270E.TN3270E_DEVICE_TYPE); + assertEquals(3, Telnet3270E.TN3270E_FUNCTIONS); + assertEquals(4, Telnet3270E.TN3270E_IS); + assertEquals(7, Telnet3270E.TN3270E_REQUEST); + + assertEquals(0, Telnet3270E.DATA_TYPE_3270_DATA); + assertEquals(1, Telnet3270E.DATA_TYPE_SCS_DATA); + assertEquals(2, Telnet3270E.DATA_TYPE_RESPONSE); + assertEquals(3, Telnet3270E.DATA_TYPE_BIND_IMAGE); + assertEquals(4, Telnet3270E.DATA_TYPE_UNBIND); + assertEquals(5, Telnet3270E.DATA_TYPE_NVT_DATA); + assertEquals(7, Telnet3270E.DATA_TYPE_SSCP_LU_DATA); + + assertEquals(fsm, telnet.getFSM()); + + // Drop-in eNetwork facade + haus.nightmare.lib3270j.eNetwork.ECL.tn3270.Telnet3270E facadeTelnet = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270.Telnet3270E(fsm, null); + assertTrue(facadeTelnet instanceof Telnet3270E); + } + + // ========================================== + // 3.4 com.ibm.eNetwork.ECL.tn3270.NVT3270 + // ========================================== + + @Test + public void testNVT3270OutboundFramingAndAddress() { + DS3270 ds = new DS3270(dsp); + ECLPS ps = new ECLPS(screen, inputProcessor, translator); + NVT3270 nvt = new NVT3270("test-host", null, ps, ds); + + assertEquals(ds, nvt.getDS()); + assertEquals(ps, nvt.getPS()); + + // Address derivation at cursor 258 (0x0102 -> high 1, low 2) + screen.setCursorAddress(258); + NVT3270 nvt2 = new NVT3270("test-host", null, ps, ds); + assertEquals(1, nvt2.address1st()); + assertEquals(2, nvt2.address2nd()); + + // process_outbound short array + short[] outbound = new short[] { 0xC1, 0xC2 }; + nvt.process_outbound(outbound, 0, outbound.length); + assertTrue(ps.isNVTmode()); + + // Drop-in eNetwork facade + haus.nightmare.lib3270j.eNetwork.ECL.tn3270.NVT3270 facadeNvt = + new haus.nightmare.lib3270j.eNetwork.ECL.tn3270.NVT3270("host", null, ps, ds); + assertTrue(facadeNvt instanceof NVT3270); + } + + // ========================================== + // 3.5 ECLSession Integration + // ========================================== + + @Test + public void testECLSessionIntegrationAccessors() { + assertNotNull(session.GetDS()); + assertNotNull(session.getDS()); + assertNotNull(session.GetTelnet()); + assertNotNull(session.getTelnet()); + assertNotNull(session.GetPS3270()); + assertNotNull(session.getPS3270()); + + assertTrue(session.getDS() instanceof DS3270); + assertTrue(session.getTelnet() instanceof Telnet3270E); + assertTrue(session.getPS3270() instanceof PS3270); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/xfer/Phase6FileTransferTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/xfer/Phase6FileTransferTest.java new file mode 100644 index 0000000..6dd7a1e --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/xfer/Phase6FileTransferTest.java @@ -0,0 +1,295 @@ +package haus.nightmare.lib3270j.xfer; + +import haus.nightmare.lib3270j.charset.CodePageRegistry; +import haus.nightmare.lib3270j.charset.Cp037; +import haus.nightmare.lib3270j.ft.FTConfig; +import haus.nightmare.lib3270j.ft.FTConfig.AllocationUnit; +import haus.nightmare.lib3270j.ft.FTConfig.CrAction; +import haus.nightmare.lib3270j.ft.FTConfig.ExistAction; +import haus.nightmare.lib3270j.ft.FTConfig.HostType; +import haus.nightmare.lib3270j.ft.FTConfig.RecordFormat; +import haus.nightmare.lib3270j.ft.FTConfig.TransferMode; +import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry; +import haus.nightmare.lib3270j.xfer3270.Xfer3270; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verification test suite for Phase 6: File Transfer Subsystem & Interface Compliance. + */ +public class Phase6FileTransferTest { + + @Test + @DisplayName("Test FileTransferInterface Constants and Name Transformations") + public void testFileTransferInterfaceAndNaming() { + Xfer3270 xfer = new Xfer3270(); + + // 1. Constants verification + assertEquals(0, FileTransferInterface.VM_CMS); + assertEquals(1, FileTransferInterface.MVS_TSO); + assertEquals(2, FileTransferInterface.CICS); + assertEquals(3, FileTransferInterface.OS400); + assertEquals(0, FileTransferInterface.ASCII); + assertEquals(1, FileTransferInterface.BINARY); + assertEquals(0, FileTransferInterface.GET); + assertEquals(1, FileTransferInterface.PUT); + + // 2. VM/CMS host and local filename formatting + assertEquals("test file", xfer.getHostFileName("test.file", FileTransferInterface.VM_CMS)); + assertEquals("TEST FILE", xfer.getHostFileName("TEST.FILE", FileTransferInterface.VM_CMS)); + assertEquals("longfile extensio", xfer.getHostFileName("longfilename.extensionlong", FileTransferInterface.VM_CMS)); + assertEquals("NONE NONE", xfer.getHostFileName("", FileTransferInterface.VM_CMS)); + assertEquals("MYFILE.DATA", xfer.getLocalFileName("MYFILE DATA", FileTransferInterface.VM_CMS)); + + // 3. MVS/TSO host and local dataset formatting + assertEquals("USER.TEST.DATA", xfer.getHostFileName("USER.TEST.DATA", FileTransferInterface.MVS_TSO)); + assertEquals("user.test.data", xfer.getHostFileName("user.test.data", FileTransferInterface.MVS_TSO)); + assertEquals("QUALIFIE.DATASET", xfer.getHostFileName("QUALIFIERVERYLONG.DATASET", FileTransferInterface.MVS_TSO)); + assertEquals("MY.DATASET.NAME", xfer.getLocalFileName("'MY.DATASET.NAME'", FileTransferInterface.MVS_TSO)); + assertEquals("UNQUOTED.DS", xfer.getLocalFileName("UNQUOTED.DS", FileTransferInterface.MVS_TSO)); + + // 4. CICS file formatting + assertEquals("cicsfile", xfer.getHostFileName("cicsfile", FileTransferInterface.CICS)); + assertEquals("CICSFILE", xfer.getHostFileName("CICSFILE", FileTransferInterface.CICS)); + assertEquals("TOOLONGF", xfer.getHostFileName("TOOLONGFILENAME", FileTransferInterface.CICS)); + } + + @Test + @DisplayName("Test FileTransferFileObject Mainframe Dataset Encapsulation") + public void testFileTransferFileObject() { + // Standard HoD constructor + FileTransferFileObject fo1 = new FileTransferFileObject("MYFILE", 1024L, false); + assertEquals("MYFILE", fo1.getName()); + assertEquals(1024L, fo1.getSize()); + assertFalse(fo1.isDirectory()); + assertEquals("MYFILE:1024:false", fo1.toString()); + + // Extended dataset configuration + FileTransferFileObject fo2 = new FileTransferFileObject("test.txt", "USER.SRC.COBOL(PGM1)"); + assertEquals("test.txt", fo2.getLocalFile()); + assertEquals("USER.SRC.COBOL(PGM1)", fo2.getHostDatasetName()); + fo2.setHostType(HostType.TSO); + fo2.setTransferMode(TransferMode.ASCII); + fo2.setCrAction(CrAction.REMOVE); + fo2.setExistAction(ExistAction.REPLACE); + fo2.setRecfm(RecordFormat.FIXED); + fo2.setLrecl(80); + fo2.setBlksize(3120); + fo2.setPrimarySpace(10); + fo2.setSecondarySpace(5); + fo2.setSpaceUnits(AllocationUnit.TRACKS); + + // Convert to FTConfig + FTConfig config = fo2.toFTConfig(); + assertEquals("USER.SRC.COBOL(PGM1)", config.getHostFilename()); + assertEquals("test.txt", config.getLocalFilename()); + assertEquals(HostType.TSO, config.getHostType()); + assertEquals(TransferMode.ASCII, config.getTransferMode()); + assertEquals(RecordFormat.FIXED, config.getRecfm()); + assertEquals(80, config.getLrecl()); + assertEquals(3120, config.getBlksize()); + assertEquals(10, config.getPrimarySpace()); + assertEquals(5, config.getSecondarySpace()); + assertEquals(AllocationUnit.TRACKS, config.getUnits()); + assertTrue(config.isOverwrite()); + + // Round-trip back from FTConfig + FileTransferFileObject fo3 = new FileTransferFileObject(config); + assertEquals("USER.SRC.COBOL(PGM1)", fo3.getHostDatasetName()); + assertEquals("test.txt", fo3.getLocalFile()); + assertEquals(RecordFormat.FIXED, fo3.getRecfm()); + assertEquals(80, fo3.getLrecl()); + assertEquals(3120, fo3.getBlksize()); + } + + @Test + @DisplayName("Test FileTransferStatusInterface Callbacks") + public void testFileTransferStatusInterface() { + List events = new ArrayList<>(); + long[] progressBytes = new long[1]; + int[] progressPct = new int[1]; + + FileTransferStatusInterface status = new FileTransferStatusInterface() { + @Override + public void setFileInfo(String fileName, long fileSize) { + events.add("INFO:" + fileName + ":" + fileSize); + } + + @Override + public void startTransfer() { + events.add("START"); + } + + @Override + public void bytesTransfered(long bytes) { + progressBytes[0] = bytes; + } + + @Override + public void onProgress(long bytesTransferred, long totalBytes, int percent) { + progressBytes[0] = bytesTransferred; + progressPct[0] = percent; + events.add("PROGRESS:" + bytesTransferred + "/" + totalBytes + " (" + percent + "%)"); + } + + @Override + public void transferComplete() { + events.add("COMPLETE"); + } + }; + + status.setFileInfo("TEST.DAT", 1000L); + status.startTransfer(); + status.onProgress(500L, 1000L, 50); + status.transferComplete(); + + assertEquals(4, events.size()); + assertEquals("INFO:TEST.DAT:1000", events.get(0)); + assertEquals("START", events.get(1)); + assertEquals("PROGRESS:500/1000 (50%)", events.get(2)); + assertEquals("COMPLETE", events.get(3)); + assertEquals(500L, progressBytes[0]); + assertEquals(50, progressPct[0]); + } + + @Test + @DisplayName("Test Xfer3270 setOptionFlags Comprehensive Parsing") + public void testXfer3270OptionFlagsParsing() { + Xfer3270 xfer = new Xfer3270(); + + // 1. Standard options + xfer.setOptionFlags("ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(10,5) TRACKS REPLACE BUFFERSIZE(4096)"); + assertTrue(xfer.option_ASCII); + assertTrue(xfer.option_CRLF); + assertFalse(xfer.option_APPEND); + assertFalse(xfer.option_UNICODE); + assertEquals(4096, xfer.GetMTUSize()); + + // 2. UNICODE UTF-8 options + xfer.setOptionFlags("BINARY NOCRLF UNICODE(UTF-8) APPEND CYLINDERS SPACE(50,20) LRECL(133)"); + assertFalse(xfer.option_ASCII); + assertFalse(xfer.option_CRLF); + assertTrue(xfer.option_APPEND); + assertTrue(xfer.option_UNICODE); + assertEquals(Xfer3270.UNICODE_UTF8, xfer.unicodeType); + + // 3. UNICODE UCS-2 options + xfer.setOptionFlags("ASCII UNICODE(UCS2) RECFM(V) LRECL(256) AVBLOCK(1024) SPACE(100)"); + assertTrue(xfer.option_ASCII); + assertTrue(xfer.option_UNICODE); + assertEquals(Xfer3270.UNICODE_UCS2, xfer.unicodeType); + } + + @Test + @DisplayName("Test Unicode Streams (XferFileOutputUnicode and XferFileInputUnicode)") + public void testUnicodeStreams(@TempDir Path tempDir) throws Exception { + File targetFile = tempDir.resolve("unicode_test.txt").toFile(); + Cp037 cp = new Cp037(); + + // 1. Output stream: write host EBCDIC bytes to local UTF-8 file with BOM + XferFileOutputUnicode outStream = new XferFileOutputUnicode( + targetFile, false, new byte[]{13, 10}, true, cp, + XferUnicodeConverter.UNICODE_UTF8, false, false); + + // Host bytes: "HELLO" in EBCDIC 037 = [0xC8, 0xC5, 0xD3, 0xD3, 0xD6] + byte[] ebcdicHello = new byte[]{(byte) 0xC8, (byte) 0xC5, (byte) 0xD3, (byte) 0xD3, (byte) 0xD6}; + outStream.write(ebcdicHello, 0, ebcdicHello.length); + outStream.close(); + + // Verify written file contains UTF-8 BOM followed by "HELLO" + byte[] writtenBytes = Files.readAllBytes(targetFile.toPath()); + assertTrue(writtenBytes.length >= 8); // 3-byte BOM + 5 bytes "HELLO" + assertEquals((byte) 0xEF, writtenBytes[0]); + assertEquals((byte) 0xBB, writtenBytes[1]); + assertEquals((byte) 0xBF, writtenBytes[2]); + assertEquals('H', (char) writtenBytes[3]); + assertEquals('E', (char) writtenBytes[4]); + assertEquals('L', (char) writtenBytes[5]); + assertEquals('L', (char) writtenBytes[6]); + assertEquals('O', (char) writtenBytes[7]); + + // 2. Input stream: read UTF-8 file with BOM, strip BOM, and translate back to host EBCDIC + XferFileInputUnicode inStream = new XferFileInputUnicode( + targetFile, new byte[]{13, 10}, cp, + XferUnicodeConverter.UNICODE_UTF8, 1, false); + + byte[] hostBuffer = new byte[32]; + int read = inStream.readData(hostBuffer, 0, hostBuffer.length); + inStream.close(); + + assertEquals(5, read); + assertEquals((byte) 0xC8, hostBuffer[0]); + assertEquals((byte) 0xC5, hostBuffer[1]); + assertEquals((byte) 0xD3, hostBuffer[2]); + assertEquals((byte) 0xD3, hostBuffer[3]); + assertEquals((byte) 0xD6, hostBuffer[4]); + } + + @Test + @DisplayName("Test Directory Retrieval with FileTransferHostDirectoryInterface") + public void testDirectoryRetrieval() throws Exception { + Xfer3270 xfer = new Xfer3270(); + + // Sample VM/CMS FILELIST response + String cmsQuery = "PROFILE EXEC A1 F 80 12 1 2026-08-20 12:00:00\n" + + "NOTE TXT A1 V 133 5 1 2026-08-21 14:30:00"; + + Vector localFiles = new Vector<>(); + Vector hostFiles = new Vector<>(); + final int[] reportedStatus = new int[]{-1}; + final int[] entryCount = new int[]{0}; + + FileTransferHostDirectoryInterface callback = new FileTransferHostDirectoryInterface() { + @Override + public void setStatus(int status) { + reportedStatus[0] = status; + } + + @Override + public void onDirectoryLoaded(List entries) { + entryCount[0] = entries.size(); + } + }; + + Vector result = xfer.getFiles(cmsQuery, FileTransferInterface.VM_CMS, 30, localFiles, hostFiles, callback); + assertEquals(2, result.size()); + assertEquals(2, hostFiles.size()); + assertEquals(2, localFiles.size()); + assertEquals("PROFILE EXEC", hostFiles.get(0)); + assertEquals("PROFILE.EXEC", localFiles.get(0)); + assertEquals("NOTE TXT", hostFiles.get(1)); + assertEquals("NOTE.TXT", localFiles.get(1)); + assertEquals(FileTransferHostDirectoryInterface.STATUS_OK, reportedStatus[0]); + assertEquals(2, entryCount[0]); + } + + @Test + @DisplayName("Test Drop-In Canonical Package Exposure (eNetwork.ECL.xfer.*)") + public void testCanonicalPackageFacades() { + haus.nightmare.lib3270j.eNetwork.ECL.xfer.FileTransferFileObject eFileObj = + new haus.nightmare.lib3270j.eNetwork.ECL.xfer.FileTransferFileObject("DATASET.NAME", 5000L); + assertEquals("DATASET.NAME", eFileObj.getName()); + assertEquals(5000L, eFileObj.getSize()); + + haus.nightmare.lib3270j.eNetwork.ECL.xfer3270.Xfer3270 eXfer = + new haus.nightmare.lib3270j.eNetwork.ECL.xfer3270.Xfer3270(); + assertNotNull(eXfer); + assertEquals("Idle", eXfer.stateIs(0)); + assertEquals("TEST.TXT", eXfer.getLocalFileName("'TEST.TXT'", FileTransferInterface.MVS_TSO)); + + haus.nightmare.lib3270j.eNetwork.ECL.xfer3270.CMSPrintXfer ePrint = + new haus.nightmare.lib3270j.eNetwork.ECL.xfer3270.CMSPrintXfer(eXfer); + assertNotNull(ePrint); + } +}