From 8a30efe9c039f8e578f8d04dca303a3476a7d737 Mon Sep 17 00:00:00 2001 From: Rudi Date: Fri, 18 Sep 2026 17:33:58 +0000 Subject: [PATCH] Add bugfix for ATTN --- .../java/haus/nightmare/j3270/J3270App.java | 13 + .../haus/nightmare/j3270/ui/MacroDialog.java | 392 ++++++++++++++ .../macrovariable/MacroExpressionParser.java | 14 + .../ECL/macrovariable/MacroVariables.java | 14 + .../com/ibm/eNetwork/beans/HOD/Macro.java | 23 + .../ibm/eNetwork/beans/HOD/MacroAction.java | 10 + .../ibm/eNetwork/beans/HOD/MacroActions.java | 14 + .../eNetwork/beans/HOD/MacroNextScreens.java | 14 + .../ibm/eNetwork/beans/HOD/MacroScreen.java | 18 + .../ibm/eNetwork/beans/HOD/MacroScreens.java | 14 + .../haus/nightmare/lib3270j/ecl/ECLPS.java | 10 + .../nightmare/lib3270j/ecl/ECLScreenDesc.java | 20 + .../lib3270j/input/InputProcessor.java | 11 +- .../haus/nightmare/lib3270j/macro/Macro.java | 236 +++++++++ .../lib3270j/macro/MacroActions.java | 101 ++++ .../lib3270j/macro/MacroNextScreens.java | 91 ++++ .../nightmare/lib3270j/macro/MacroScreen.java | 149 ++++++ .../lib3270j/macro/MacroScreens.java | 196 +++++++ .../lib3270j/macro/MacroXmlParser.java | 385 ++++++++++++++ .../nightmare/lib3270j/macro/PlayThread.java | 237 +++++++++ .../lib3270j/macro/action/MacroAction.java | 108 ++++ .../macro/action/MacroActionCommWait.java | 69 +++ .../macro/action/MacroActionExtract.java | 144 +++++ .../lib3270j/macro/action/MacroActionIf.java | 87 ++++ .../macro/action/MacroActionInput.java | 114 ++++ .../macro/action/MacroActionMessage.java | 60 +++ .../macro/action/MacroActionPause.java | 62 +++ .../macro/action/MacroActionPlayMacro.java | 61 +++ .../macro/action/MacroActionPrompt.java | 176 +++++++ .../macro/action/MacroActionVarUpdate.java | 57 ++ .../macro/variable/MacroExpressionParser.java | 490 ++++++++++++++++++ .../macro/variable/MacroVariables.java | 262 ++++++++++ .../nightmare/lib3270j/telnet/TelnetFSM.java | 36 ++ .../macro/Phase24MacroRuntimeTest.java | 359 +++++++++++++ .../lib3270j/telnet/AttnTransmissionTest.java | 160 ++++++ 35 files changed, 4201 insertions(+), 6 deletions(-) create mode 100644 j3270/src/main/java/haus/nightmare/j3270/ui/MacroDialog.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroExpressionParser.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroVariables.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/Macro.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroAction.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroActions.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroNextScreens.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreen.java create mode 100644 lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreens.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/Macro.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroActions.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroNextScreens.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreen.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreens.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroXmlParser.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/PlayThread.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroAction.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionCommWait.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionExtract.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionIf.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionInput.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionMessage.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPause.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPlayMacro.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPrompt.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionVarUpdate.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroExpressionParser.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroVariables.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/macro/Phase24MacroRuntimeTest.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/AttnTransmissionTest.java diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index f10fe1d..c1ff2c9 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -42,6 +42,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate // Dialog instances private FindDialog findDialog; private ScriptDialog scriptDialog; + private MacroDialog macroDialog; private FieldInspectorDialog fieldInspectorDialog; private PrinterSessionDialog printerSessionDialog; @@ -136,6 +137,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } if (findDialog != null && findDialog.isVisible()) return true; if (scriptDialog != null && scriptDialog.isVisible()) return true; + if (macroDialog != null && macroDialog.isVisible()) return true; if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true; if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true; for (Window owned : getOwnedWindows()) { @@ -453,6 +455,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }, true)); actionsMenu.addSeparator(); actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); + actionsMenu.add(createMenuItem("Macro Runtime ()...", KeyEvent.VK_M, this::showMacroDialog, true)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true)); menuBar.add(actionsMenu); @@ -559,6 +562,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate scriptDialog.setVisible(true); } + private void showMacroDialog() { + if (macroDialog == null) { + macroDialog = new MacroDialog(this, client, terminalPanel); + } + macroDialog.setVisible(true); + } + private void showFieldInspectorDialog() { if (client == null) { JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE); @@ -800,6 +810,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate fileTransfer.cancel(); fileTransfer = null; } + if (macroDialog != null) { + macroDialog.stopMacro(); + } Telnet3270Client c = client; client = null; new Thread(() -> { diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/MacroDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/MacroDialog.java new file mode 100644 index 0000000..abc6482 --- /dev/null +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/MacroDialog.java @@ -0,0 +1,392 @@ +package haus.nightmare.j3270.ui; + +import haus.nightmare.lib3270j.Telnet3270Client; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; +import haus.nightmare.lib3270j.macro.MacroScreen; +import haus.nightmare.lib3270j.macro.MacroScreens; +import haus.nightmare.lib3270j.macro.MacroXmlParser; +import haus.nightmare.lib3270j.macro.action.MacroAction; +import haus.nightmare.lib3270j.macro.action.MacroActionPrompt; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Interactive Swing dialog for loading, editing, running, and debugging + * IBM Host On-Demand XML Macros ( / .mac files). + */ +public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler { + + private final Telnet3270Client client; + private final TerminalPanel terminalPanel; + + private JTextArea xmlArea; + private JTextArea logArea; + private JLabel statusLabel; + private JButton playBtn; + private JButton pauseBtn; + private JButton stopBtn; + + private Macro macro; + + public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) { + super(parent, "IBM HoD XML Macro Runtime ()", false); + this.client = client; + this.terminalPanel = terminalPanel; + AppIcon.applyTo(this); + + this.macro = new Macro(client); + this.macro.addMacroListener(this); + this.macro.setPromptHandler(this); + + buildUI(); + setSize(780, 580); + setLocationRelativeTo(parent); + } + + private void buildUI() { + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); + + Font labelFont = ThemeManager.getUiFont(); + + // Top info label + JPanel topPanel = new JPanel(new BorderLayout(5, 5)); + topPanel.setOpaque(false); + JLabel descLabel = new JLabel("Load, edit, and execute IBM Host On-Demand XML Macro files (.mac / <HASCRIPT>).
" + + "Automates host flows via screen descriptors, actions, variables, and state transitions."); + descLabel.setFont(labelFont); + topPanel.add(descLabel, BorderLayout.CENTER); + mainPanel.add(topPanel, BorderLayout.NORTH); + + // Center split: XML editor and Execution Log + xmlArea = new JTextArea(); + xmlArea.setFont(ThemeManager.getMonospacedUiFont()); + xmlArea.setLineWrap(false); + xmlArea.setText(getDefaultMacroXml()); + ThemeManager.styleTextArea(xmlArea); + + JScrollPane xmlScroll = new JScrollPane(xmlArea); + ThemeManager.styleScrollPane(xmlScroll); + xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML ()")); + + logArea = new JTextArea(); + logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1)); + logArea.setEditable(false); + logArea.setLineWrap(true); + ThemeManager.styleTextArea(logArea); + + JScrollPane logScroll = new JScrollPane(logArea); + ThemeManager.styleScrollPane(logScroll); + logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log")); + + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll); + splitPane.setResizeWeight(0.65); + splitPane.setOpaque(false); + mainPanel.add(splitPane, BorderLayout.CENTER); + + // Bottom control panel + JPanel bottomPanel = new JPanel(new BorderLayout(5, 5)); + bottomPanel.setOpaque(false); + + statusLabel = new JLabel("Status: Stopped"); + statusLabel.setForeground(ThemeManager.getFgMuted()); + statusLabel.setFont(labelFont); + bottomPanel.add(statusLabel, BorderLayout.WEST); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); + buttonPanel.setOpaque(false); + + JButton loadBtn = new JButton("Load .mac..."); + ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT); + loadBtn.setFont(labelFont); + loadBtn.addActionListener(e -> loadMacroFile()); + + JButton saveBtn = new JButton("Save .mac..."); + ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT); + saveBtn.setFont(labelFont); + saveBtn.addActionListener(e -> saveMacroFile()); + + playBtn = new JButton("Play"); + ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY); + playBtn.setFont(labelFont); + playBtn.addActionListener(e -> playMacro()); + + pauseBtn = new JButton("Pause"); + ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT); + pauseBtn.setFont(labelFont); + pauseBtn.setEnabled(false); + pauseBtn.addActionListener(e -> togglePause()); + + stopBtn = new JButton("Stop"); + ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT); + stopBtn.setFont(labelFont); + stopBtn.setEnabled(false); + stopBtn.addActionListener(e -> stopMacro()); + + JButton clearLogBtn = new JButton("Clear Log"); + ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT); + clearLogBtn.setFont(labelFont); + clearLogBtn.addActionListener(e -> logArea.setText("")); + + JButton closeBtn = new JButton("Close"); + ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL); + closeBtn.setFont(labelFont); + closeBtn.addActionListener(e -> dispose()); + + buttonPanel.add(loadBtn); + buttonPanel.add(saveBtn); + buttonPanel.add(Box.createHorizontalStrut(8)); + buttonPanel.add(playBtn); + buttonPanel.add(pauseBtn); + buttonPanel.add(stopBtn); + buttonPanel.add(clearLogBtn); + buttonPanel.add(Box.createHorizontalStrut(8)); + buttonPanel.add(closeBtn); + + bottomPanel.add(buttonPanel, BorderLayout.EAST); + mainPanel.add(bottomPanel, BorderLayout.SOUTH); + + setContentPane(mainPanel); + ThemeManager.applyThemeToWindow(this); + } + + private void loadMacroFile() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Open IBM HoD Macro"); + chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml")); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + File f = chooser.getSelectedFile(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = r.readLine()) != null) { + sb.append(line).append("\n"); + } + xmlArea.setText(sb.toString()); + log("Loaded macro file: " + f.getName()); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + } + } + } + + private void saveMacroFile() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Save IBM HoD Macro"); + chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml")); + if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { + File f = chooser.getSelectedFile(); + if (!f.getName().contains(".")) { + f = new File(f.getParentFile(), f.getName() + ".mac"); + } + try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) { + w.write(xmlArea.getText()); + log("Saved macro file: " + f.getName()); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + } + } + } + + public void playMacro() { + String xml = xmlArea.getText(); + if (xml == null || xml.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE); + return; + } + + if (client == null || !client.getConnectionState().isFullSession()) { + int opt = JOptionPane.showConfirmDialog(this, + "Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?", + "Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (opt != JOptionPane.YES_OPTION) { + return; + } + } + + try { + MacroScreens screens = MacroXmlParser.parse(xml); + macro.setScreens(screens); + macro.setClient(client); + + log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName())); + macro.play(); + } catch (Exception ex) { + log("Parse / Launch Error: " + ex.getMessage()); + JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE); + } + } + + public void togglePause() { + if (macro.getState() == Macro.STATE_PLAYING) { + macro.pause(); + pauseBtn.setText("Resume"); + log("Macro paused."); + } else if (macro.getState() == Macro.STATE_PAUSED) { + macro.resume(); + pauseBtn.setText("Pause"); + log("Macro resumed."); + } + } + + public void stopMacro() { + macro.stop(); + log("Macro stopped by user."); + } + + private void log(String msg) { + SwingUtilities.invokeLater(() -> { + logArea.append(msg + "\n"); + logArea.setCaretPosition(logArea.getDocument().getLength()); + }); + } + + // ========== MacroListener Callbacks ========== + + @Override + public void macroStateChanged(Macro macro, int oldState, int newState) { + SwingUtilities.invokeLater(() -> { + switch (newState) { + case Macro.STATE_PLAYING: + statusLabel.setText("Status: Playing"); + statusLabel.setForeground(ThemeManager.getFgMain()); + playBtn.setEnabled(false); + pauseBtn.setEnabled(true); + pauseBtn.setText("Pause"); + stopBtn.setEnabled(true); + break; + case Macro.STATE_PAUSED: + statusLabel.setText("Status: Paused"); + statusLabel.setForeground(ThemeManager.getFgMuted()); + pauseBtn.setText("Resume"); + break; + case Macro.STATE_ERROR: + statusLabel.setText("Status: Error"); + statusLabel.setForeground(Color.RED); + playBtn.setEnabled(true); + pauseBtn.setEnabled(false); + stopBtn.setEnabled(false); + break; + case Macro.STATE_STOPPED: + default: + statusLabel.setText("Status: Stopped"); + statusLabel.setForeground(ThemeManager.getFgMuted()); + playBtn.setEnabled(true); + pauseBtn.setEnabled(false); + pauseBtn.setText("Pause"); + stopBtn.setEnabled(false); + break; + } + }); + } + + @Override + public void macroScreenMatching(MacroScreen screen) { + log("Matched screen: [" + screen.getName() + "]"); + if (terminalPanel != null) { + terminalPanel.repaint(); + } + } + + @Override + public void macroActionExecuting(MacroAction action) { + log(" -> Executing: " + action.getClass().getSimpleName()); + if (terminalPanel != null) { + terminalPanel.repaint(); + } + } + + @Override + public void macroMessage(String title, String message) { + log("Message [" + title + "]: " + message); + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE); + }); + } + + @Override + public void macroError(String error) { + log("ERROR: " + error); + } + + // ========== MacroPromptHandler Callback ========== + + @Override + public String prompt(MacroActionPrompt prompt) { + AtomicReference result = new AtomicReference<>(null); + + if (SwingUtilities.isEventDispatchThread()) { + result.set(showPromptModal(prompt)); + } else { + try { + SwingUtilities.invokeAndWait(() -> { + result.set(showPromptModal(prompt)); + }); + } catch (Exception e) { + return null; + } + } + return result.get(); + } + + private String showPromptModal(MacroActionPrompt prompt) { + JPanel panel = new JPanel(new BorderLayout(5, 5)); + JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription()); + panel.add(msgLabel, BorderLayout.NORTH); + + JTextField textField; + if (prompt.isEncrypted()) { + textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15); + } else { + textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15); + } + panel.add(textField, BorderLayout.CENTER); + + int opt = JOptionPane.showConfirmDialog(this, panel, + prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); + + if (opt == JOptionPane.OK_OPTION) { + if (textField instanceof JPasswordField) { + return new String(((JPasswordField) textField).getPassword()); + } + return textField.getText(); + } + return null; + } + + private String getDefaultMacroXml() { + return "\n" + + " \n" + + " \n" + + " \n" + + " \n\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroExpressionParser.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroExpressionParser.java new file mode 100644 index 0000000..4474cb3 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroExpressionParser.java @@ -0,0 +1,14 @@ +package com.ibm.eNetwork.ECL.macrovariable; + +import java.io.Serializable; + +/** + * Compatibility facade for IBM Host On-Demand MacroExpressionParser. + */ +public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable { + private static final long serialVersionUID = 1L; + + public MacroExpressionParser() { + super(); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroVariables.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroVariables.java new file mode 100644 index 0000000..d1e3595 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/macrovariable/MacroVariables.java @@ -0,0 +1,14 @@ +package com.ibm.eNetwork.ECL.macrovariable; + +import java.io.Serializable; + +/** + * Compatibility facade for IBM Host On-Demand MacroVariables. + */ +public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable { + private static final long serialVersionUID = 1L; + + public MacroVariables() { + super(); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/Macro.java b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/Macro.java new file mode 100644 index 0000000..6bd0a5b --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/Macro.java @@ -0,0 +1,23 @@ +package com.ibm.eNetwork.beans.HOD; + +import java.io.Serializable; +import com.ibm.eNetwork.ECL.ECLSession; + +/** + * Compatibility facade for IBM Host On-Demand Macro bean. + */ +public class Macro extends haus.nightmare.lib3270j.macro.Macro implements Serializable { + private static final long serialVersionUID = 1L; + + public Macro() { + super(); + } + + public Macro(ECLSession session) { + super(session != null ? session.getDelegate() : null); + } + + public Macro(haus.nightmare.lib3270j.ecl.ECLSession session) { + super(session); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroAction.java b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroAction.java new file mode 100644 index 0000000..31c3a11 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroAction.java @@ -0,0 +1,10 @@ +package com.ibm.eNetwork.beans.HOD; + +import java.io.Serializable; + +/** + * Compatibility facade for IBM Host On-Demand MacroAction. + */ +public abstract class MacroAction extends haus.nightmare.lib3270j.macro.action.MacroAction implements Serializable { + private static final long serialVersionUID = 1L; +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroActions.java b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroActions.java new file mode 100644 index 0000000..dea1fdf --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroActions.java @@ -0,0 +1,14 @@ +package com.ibm.eNetwork.beans.HOD; + +import java.io.Serializable; + +/** + * Compatibility facade for IBM Host On-Demand MacroActions. + */ +public class MacroActions extends haus.nightmare.lib3270j.macro.MacroActions implements Serializable { + private static final long serialVersionUID = 1L; + + public MacroActions() { + super(); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroNextScreens.java b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroNextScreens.java new file mode 100644 index 0000000..32afc19 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroNextScreens.java @@ -0,0 +1,14 @@ +package com.ibm.eNetwork.beans.HOD; + +import java.io.Serializable; + +/** + * Compatibility facade for IBM Host On-Demand MacroNextScreens. + */ +public class MacroNextScreens extends haus.nightmare.lib3270j.macro.MacroNextScreens implements Serializable { + private static final long serialVersionUID = 1L; + + public MacroNextScreens() { + super(); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreen.java b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreen.java new file mode 100644 index 0000000..6640b28 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreen.java @@ -0,0 +1,18 @@ +package com.ibm.eNetwork.beans.HOD; + +import java.io.Serializable; + +/** + * Compatibility facade for IBM Host On-Demand MacroScreen. + */ +public class MacroScreen extends haus.nightmare.lib3270j.macro.MacroScreen implements Serializable { + private static final long serialVersionUID = 1L; + + public MacroScreen() { + super(); + } + + public MacroScreen(String name) { + super(name); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreens.java b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreens.java new file mode 100644 index 0000000..4ed3474 --- /dev/null +++ b/lib3270j/src/main/java/com/ibm/eNetwork/beans/HOD/MacroScreens.java @@ -0,0 +1,14 @@ +package com.ibm.eNetwork.beans.HOD; + +import java.io.Serializable; + +/** + * Compatibility facade for IBM Host On-Demand MacroScreens. + */ +public class MacroScreens extends haus.nightmare.lib3270j.macro.MacroScreens implements Serializable { + private static final long serialVersionUID = 1L; + + public MacroScreens() { + super(); + } +} 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 f444b52..9c30f4b 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java @@ -448,6 +448,16 @@ public class ECLPS implements ECLConstants { setText(text, p); } + /** + * Insert text at current cursor position. + */ + public void SetText(String text) { + SetText(text, GetCursorPos()); + } + public void setText(String text) { + SetText(text); + } + public synchronized void SetString(String text, int pos) throws com.ibm.eNetwork.ECL.ECLErr { if (text == null) { throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"text\"", "null"); 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 f7692aa..709f9c3 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLScreenDesc.java @@ -460,6 +460,26 @@ public class ECLScreenDesc implements Serializable, Cloneable { } } + @Override + public Object clone() { + try { + ECLScreenDesc copy = (ECLScreenDesc) super.clone(); + copy.descriptors.clear(); + for (ECLScreenDescriptor d : descriptors) { + copy.descriptors.add((ECLScreenDescriptor) d.clone()); + } + return copy; + } catch (CloneNotSupportedException e) { + ECLScreenDesc copy = new ECLScreenDesc(); + copy.SetName(this.name); + copy.SetActive(this.active); + for (ECLScreenDescriptor d : descriptors) { + copy.AddDescriptor((ECLScreenDescriptor) d.clone()); + } + return copy; + } + } + @Override public String toString() { return getXML(); 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 745e36f..be27d33 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -1528,14 +1528,13 @@ public class InputProcessor { screen.updateDisplaySnapshot(); } - /** Attention key (sends Telnet Interrupt Process + IAC EOR break signal). */ + /** Attention key (sends Telnet Interrupt Process or PA1/BREAK signal). */ public void processAttn() { + if (insertOffOnAid && insertMode) { + setInsertMode(false); + } if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) { - byte[] ipEor = new byte[] { - (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP, - (byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR - }; - fsm.sendRecord(ipEor); + fsm.handleAttn(); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/Macro.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/Macro.java new file mode 100644 index 0000000..85dc633 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/Macro.java @@ -0,0 +1,236 @@ +package haus.nightmare.lib3270j.macro; + +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; + +import haus.nightmare.lib3270j.Telnet3270Client; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.action.MacroAction; +import haus.nightmare.lib3270j.macro.action.MacroActionPrompt; +import haus.nightmare.lib3270j.macro.variable.MacroVariables; + +/** + * High-level Macro engine matching IBM Host On-Demand Macro. + * Coordinates loading, variable scopes, screen state transitions, and background PlayThread execution. + */ +public class Macro implements Serializable { + + private static final long serialVersionUID = 1L; + + public static final int STATE_STOPPED = 1; + public static final int STATE_PLAYING = 2; + public static final int STATE_PAUSED = 4; + public static final int STATE_ERROR = 8; + + public interface MacroListener { + void macroStateChanged(Macro macro, int oldState, int newState); + void macroScreenMatching(MacroScreen screen); + void macroActionExecuting(MacroAction action); + void macroMessage(String title, String message); + void macroError(String error); + } + + public interface MacroPromptHandler { + String prompt(MacroActionPrompt prompt); + } + + private MacroScreens macScreens = new MacroScreens(); + private ECLSession eclSess; + private Telnet3270Client client; + private PlayThread playThread; + private int state = STATE_STOPPED; + private MacroPromptHandler promptHandler; + private final List listeners = new CopyOnWriteArrayList<>(); + private final Map chainedMacros = new HashMap<>(); + + public Macro() {} + + public Macro(MacroScreens screens) { + setScreens(screens); + } + + public Macro(ECLSession session) { + this.eclSess = session; + } + + public Macro(Telnet3270Client client) { + setClient(client); + } + + public void setScreens(MacroScreens screens) { + this.macScreens = (screens != null) ? screens : new MacroScreens(); + } + + public MacroScreens getScreens() { + return macScreens; + } + + public void setSession(ECLSession session) { + this.eclSess = session; + } + + public ECLSession getECLSession() { + return eclSess; + } + + public void setClient(Telnet3270Client client) { + this.client = client; + if (client != null && this.eclSess == null) { + this.eclSess = new ECLSession(client); + } + } + + public Telnet3270Client getClient() { + return client; + } + + public MacroVariables getVariables() { + return (macScreens != null) ? macScreens.getVariables() : null; + } + + public boolean isUseVars() { + return macScreens != null && macScreens.isUseVars(); + } + + public int getState() { + return state; + } + + public synchronized void setState(int newState) { + if (this.state != newState) { + int old = this.state; + this.state = newState; + for (MacroListener l : listeners) { + try { + l.macroStateChanged(this, old, newState); + } catch (Throwable ignored) {} + } + } + } + + public void setPromptHandler(MacroPromptHandler handler) { + this.promptHandler = handler; + } + + public MacroPromptHandler getPromptHandler() { + return promptHandler; + } + + public void addMacroListener(MacroListener listener) { + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + } + } + + public void removeMacroListener(MacroListener listener) { + listeners.remove(listener); + } + + public void load(String xml) { + setScreens(MacroXmlParser.parse(xml)); + } + + public synchronized void play() { + play(null); + } + + public synchronized void play(String screenName) { + if (state == STATE_PLAYING) { + return; + } + if (state == STATE_PAUSED && playThread != null) { + resume(); + return; + } + + stop(); + + if (eclSess == null && client != null) { + eclSess = new ECLSession(client); + } + + if (macScreens.getVariables() != null) { + macScreens.getVariables().reset(); + } + + playThread = new PlayThread(this, macScreens, eclSess, screenName); + playThread.start(); + } + + public synchronized void pause() { + if (state == STATE_PLAYING && playThread != null) { + playThread.pausePlay(); + setState(STATE_PAUSED); + } + } + + public synchronized void resume() { + if (state == STATE_PAUSED && playThread != null) { + playThread.resumePlay(); + setState(STATE_PLAYING); + } + } + + public synchronized void stop() { + if (playThread != null) { + playThread.halt(); + playThread = null; + } + setState(STATE_STOPPED); + } + + public void halt() { + stop(); + } + + public void playChainedMacro(String name, String startScreen) { + Macro chained = chainedMacros.get(name); + if (chained != null) { + chained.setSession(this.eclSess); + chained.setClient(this.client); + chained.setPromptHandler(this.promptHandler); + chained.play(startScreen); + } else { + logError("Chained macro not found: " + name); + } + } + + public void registerChainedMacro(String name, Macro macro) { + if (name != null && macro != null) { + chainedMacros.put(name, macro); + } + } + + public void notifyScreenMatching(MacroScreen screen) { + for (MacroListener l : listeners) { + try { + l.macroScreenMatching(screen); + } catch (Throwable ignored) {} + } + } + + public void notifyActionExecuting(MacroAction action) { + for (MacroListener l : listeners) { + try { + l.macroActionExecuting(action); + } catch (Throwable ignored) {} + } + } + + public void notifyMessage(String title, String message) { + for (MacroListener l : listeners) { + try { + l.macroMessage(title, message); + } catch (Throwable ignored) {} + } + } + + public void logError(String error) { + for (MacroListener l : listeners) { + try { + l.macroError(error); + } catch (Throwable ignored) {} + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroActions.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroActions.java new file mode 100644 index 0000000..8a5e1c3 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroActions.java @@ -0,0 +1,101 @@ +package haus.nightmare.lib3270j.macro; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import haus.nightmare.lib3270j.macro.action.MacroAction; + +/** + * Collection of MacroAction items matching IBM Host On-Demand MacroActions. + */ +public class MacroActions implements Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + private final List actions = new ArrayList<>(); + private boolean promptAll = false; + + public MacroActions() {} + + public MacroActions(List actions) { + if (actions != null) { + this.actions.addAll(actions); + } + } + + public void add(MacroAction action) { + if (action != null) { + actions.add(action); + } + } + + public void add(int index, MacroAction action) { + if (action != null) { + actions.add(index, action); + } + } + + public MacroAction get(int index) { + return actions.get(index); + } + + public int size() { + return actions.size(); + } + + public boolean isEmpty() { + return actions.isEmpty(); + } + + public void clear() { + actions.clear(); + } + + public boolean remove(MacroAction action) { + return actions.remove(action); + } + + public MacroAction remove(int index) { + return actions.remove(index); + } + + public List getActions() { + return Collections.unmodifiableList(actions); + } + + public boolean isPromptAll() { + return promptAll; + } + + public void setPromptAll(boolean promptAll) { + this.promptAll = promptAll; + } + + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + String ind = ""; + for (int i = 0; i < indent; i++) ind += " "; + + sb.append(ind).append("\n"); + + for (MacroAction act : actions) { + sb.append(act.format(indent + 1)).append("\n"); + } + + sb.append(ind).append(""); + return sb.toString(); + } + + @Override + public MacroActions clone() { + MacroActions clone = new MacroActions(); + clone.promptAll = this.promptAll; + for (MacroAction act : actions) { + clone.add(act.clone()); + } + return clone; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroNextScreens.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroNextScreens.java new file mode 100644 index 0000000..8df6c8f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroNextScreens.java @@ -0,0 +1,91 @@ +package haus.nightmare.lib3270j.macro; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Next screen references for state transitions matching IBM HoD MacroNextScreens. + */ +public class MacroNextScreens implements Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + private final List nextScreens = new ArrayList<>(); + private int timeout = 0; + + public MacroNextScreens() {} + + public MacroNextScreens(int timeout) { + this.timeout = timeout; + } + + public void addNextScreen(String name) { + if (name != null && !name.trim().isEmpty() && !nextScreens.contains(name.trim())) { + nextScreens.add(name.trim()); + } + } + + public boolean containsName(String name) { + return name != null && nextScreens.contains(name.trim()); + } + + public List getNextScreenNames() { + return Collections.unmodifiableList(nextScreens); + } + + public int size() { + return nextScreens.size(); + } + + public boolean isEmpty() { + return nextScreens.isEmpty(); + } + + public void clear() { + nextScreens.clear(); + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + String ind = ""; + for (int i = 0; i < indent; i++) ind += " "; + + sb.append(ind).append(" 0) sb.append(" timeout=\"").append(timeout).append("\""); + sb.append(">\n"); + + for (String name : nextScreens) { + sb.append(ind).append(" \n"); + } + + sb.append(ind).append(""); + return sb.toString(); + } + + private static String escapeXml(String text) { + if (text == null) return ""; + return text.replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">"); + } + + @Override + public MacroNextScreens clone() { + MacroNextScreens clone = new MacroNextScreens(timeout); + for (String s : nextScreens) { + clone.addNextScreen(s); + } + return clone; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreen.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreen.java new file mode 100644 index 0000000..b38a29c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreen.java @@ -0,0 +1,149 @@ +package haus.nightmare.lib3270j.macro; + +import java.io.Serializable; +import haus.nightmare.lib3270j.ecl.ECLScreenDesc; + +/** + * Screen state machine representation matching IBM Host On-Demand MacroScreen. + */ +public class MacroScreen implements Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + private String name = ""; + private boolean entryScreen = false; + private boolean exitScreen = false; + private boolean transientScreen = false; + private int pauseTime = -1; + + private ECLScreenDesc description = new ECLScreenDesc(); + private MacroActions actions = new MacroActions(); + private MacroNextScreens nextScreens = new MacroNextScreens(); + + private int recoLimit = 0; + private String recoGoto = ""; + private int recoCount = 0; + + public MacroScreen() {} + + public MacroScreen(String name) { + this.name = (name != null) ? name : ""; + } + + public String getName() { return name; } + public void setName(String name) { + this.name = (name != null) ? name : ""; + if (description != null) { + description.SetName(this.name); + } + } + + public boolean isEntryScreen() { return entryScreen; } + public void setEntryScreen(boolean entryScreen) { this.entryScreen = entryScreen; } + public boolean isStartScreen() { return isEntryScreen(); } + public void setStartScreen(boolean startScreen) { setEntryScreen(startScreen); } + + public boolean isExitScreen() { return exitScreen; } + public void setExitScreen(boolean exitScreen) { this.exitScreen = exitScreen; } + public boolean isStopScreen() { return isExitScreen(); } + public void setStopScreen(boolean stopScreen) { setExitScreen(stopScreen); } + + public boolean isTransientScreen() { return transientScreen; } + public void setTransientScreen(boolean transientScreen) { this.transientScreen = transientScreen; } + public boolean isTransient() { return isTransientScreen(); } + public void setTransient(boolean transientScreen) { setTransientScreen(transientScreen); } + + public int getPauseTime() { return pauseTime; } + public void setPauseTime(int pauseTime) { this.pauseTime = pauseTime; } + + public ECLScreenDesc getDescription() { return description; } + public void setDescription(ECLScreenDesc description) { + this.description = (description != null) ? description : new ECLScreenDesc(); + if (this.description != null && !this.name.isEmpty()) { + this.description.SetName(this.name); + } + } + + public MacroActions getActions() { return actions; } + public void setActions(MacroActions actions) { + this.actions = (actions != null) ? actions : new MacroActions(); + } + + public MacroNextScreens getNextScreens() { return nextScreens; } + public void setNextScreens(MacroNextScreens nextScreens) { + this.nextScreens = (nextScreens != null) ? nextScreens : new MacroNextScreens(); + } + + public int getRecoLimit() { return recoLimit; } + public void setRecoLimit(int recoLimit) { this.recoLimit = recoLimit; } + + public String getRecoGoto() { return recoGoto; } + public void setRecoGoto(String recoGoto) { this.recoGoto = (recoGoto != null) ? recoGoto : ""; } + + public int getRecoCount() { return recoCount; } + public void setRecoCount(int recoCount) { this.recoCount = recoCount; } + public void incrementRecoCount() { this.recoCount++; } + public void resetRecoCount() { this.recoCount = 0; } + + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + String ind = ""; + for (int i = 0; i < indent; i++) ind += " "; + + sb.append(ind).append("= 0) sb.append(" pausetime=\"").append(pauseTime).append("\""); + sb.append(">\n"); + + if (description != null) { + String descXml = description.getXML(); + for (String line : descXml.split("\n")) { + sb.append(ind).append(" ").append(line).append("\n"); + } + } + + if (actions != null) { + sb.append(actions.format(indent + 1)).append("\n"); + } + + if (nextScreens != null && !nextScreens.isEmpty()) { + sb.append(nextScreens.format(indent + 1)).append("\n"); + } + + if (recoLimit > 0 || (recoGoto != null && !recoGoto.isEmpty())) { + sb.append(ind).append(" \n"); + } + + sb.append(ind).append(""); + return sb.toString(); + } + + private static String escapeXml(String text) { + if (text == null) return ""; + return text.replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">"); + } + + @Override + public MacroScreen clone() { + MacroScreen clone = new MacroScreen(name); + clone.setEntryScreen(entryScreen); + clone.setExitScreen(exitScreen); + clone.setTransientScreen(transientScreen); + clone.setPauseTime(pauseTime); + clone.setDescription((ECLScreenDesc) description.clone()); + clone.setActions(actions.clone()); + clone.setNextScreens(nextScreens.clone()); + clone.setRecoLimit(recoLimit); + clone.setRecoGoto(recoGoto); + return clone; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreens.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreens.java new file mode 100644 index 0000000..6f50ee9 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroScreens.java @@ -0,0 +1,196 @@ +package haus.nightmare.lib3270j.macro; + +import java.io.Serializable; +import java.util.*; +import haus.nightmare.lib3270j.macro.variable.MacroVariables; + +/** + * Collection of screens and root HASCRIPT configuration matching IBM HoD MacroScreens. + */ +public class MacroScreens implements Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + private final List screens = new ArrayList<>(); + private MacroVariables variables = new MacroVariables(); + + private String name = ""; + private String description = ""; + private String author = ""; + private String creationDate = ""; + private int timeout = 10000; + private int pauseTime = 300; + private boolean promptAll = false; + private boolean blockInput = true; + private boolean useVars = false; + + public MacroScreens() {} + + public void add(MacroScreen screen) { + if (screen != null) { + screens.add(screen); + } + } + + public void add(int index, MacroScreen screen) { + if (screen != null) { + screens.add(index, screen); + } + } + + public MacroScreen get(int index) { + return screens.get(index); + } + + public MacroScreen getByName(String name) { + if (name == null) return null; + for (MacroScreen s : screens) { + if (name.equalsIgnoreCase(s.getName())) { + return s; + } + } + return null; + } + + public boolean contains(String name) { + return getByName(name) != null; + } + + public MacroScreen getStartScreen() { + for (MacroScreen s : screens) { + if (s.isEntryScreen()) { + return s; + } + } + return (!screens.isEmpty()) ? screens.get(0) : null; + } + + public MacroScreen getEntryScreen() { + return getStartScreen(); + } + + public List getScreens() { + return Collections.unmodifiableList(screens); + } + + public int size() { + return screens.size(); + } + + public boolean isEmpty() { + return screens.isEmpty(); + } + + public void clear() { + screens.clear(); + } + + public MacroVariables getVariables() { + return variables; + } + + public void setVariables(MacroVariables variables) { + this.variables = (variables != null) ? variables : new MacroVariables(); + } + + public String getName() { return name; } + public void setName(String name) { this.name = (name != null) ? name : ""; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = (description != null) ? description : ""; } + + public String getAuthor() { return author; } + public void setAuthor(String author) { this.author = (author != null) ? author : ""; } + + public String getCreationDate() { return creationDate; } + public void setCreationDate(String creationDate) { this.creationDate = (creationDate != null) ? creationDate : ""; } + + public int getTimeout() { return timeout; } + public void setTimeout(int timeout) { this.timeout = timeout; } + + public int getPauseTime() { return pauseTime; } + public void setPauseTime(int pauseTime) { this.pauseTime = pauseTime; } + + public boolean isPromptAll() { return promptAll; } + public void setPromptAll(boolean promptAll) { this.promptAll = promptAll; } + + public boolean isBlockInput() { return blockInput; } + public void setBlockInput(boolean blockInput) { this.blockInput = blockInput; } + + public boolean isUseVars() { return useVars; } + public void setUseVars(boolean useVars) { + this.useVars = useVars; + if (variables != null) { + variables.setUseVars(useVars); + } + } + + public String format() { + StringBuilder sb = new StringBuilder(); + sb.append(" 0) sb.append(" timeout=\"").append(timeout).append("\""); + if (pauseTime >= 0) sb.append(" pausetime=\"").append(pauseTime).append("\""); + if (promptAll) sb.append(" promptall=\"true\""); + if (!blockInput) sb.append(" blockinput=\"false\""); + if (useVars) sb.append(" usevars=\"true\""); + sb.append(">\n\n"); + + // Vars section + if (variables != null && variables.size() > 0) { + sb.append(" \n"); + for (String varName : variables.getVarNames()) { + MacroVariables.VariableEntry entry = variables.getEntry(varName); + if (entry != null) { + sb.append(" \n"); + } + } + sb.append(" \n\n"); + } + + // Screens + for (MacroScreen screen : screens) { + sb.append(screen.format(1)).append("\n\n"); + } + + sb.append(""); + return sb.toString(); + } + + public String getXML() { + return format(); + } + + private static String escapeXml(String text) { + if (text == null) return ""; + return text.replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">"); + } + + @Override + public MacroScreens clone() { + MacroScreens clone = new MacroScreens(); + clone.setName(name); + clone.setDescription(description); + clone.setAuthor(author); + clone.setCreationDate(creationDate); + clone.setTimeout(timeout); + clone.setPauseTime(pauseTime); + clone.setPromptAll(promptAll); + clone.setBlockInput(blockInput); + clone.setUseVars(useVars); + clone.setVariables(variables.clone()); + for (MacroScreen s : screens) { + clone.add(s.clone()); + } + return clone; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroXmlParser.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroXmlParser.java new file mode 100644 index 0000000..605fd5f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/MacroXmlParser.java @@ -0,0 +1,385 @@ +package haus.nightmare.lib3270j.macro; + +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.*; +import org.xml.sax.InputSource; + +import haus.nightmare.lib3270j.ecl.ECLScreenDesc; +import haus.nightmare.lib3270j.ecl.screenreco.*; +import haus.nightmare.lib3270j.macro.action.*; +import haus.nightmare.lib3270j.macro.variable.MacroVariables; + +/** + * Standard IBM Host On-Demand XML Macro Parser matching MacroXmlParser. + * Parses <HASCRIPT> XML documents into MacroScreens AST and provides serialization. + */ +public class MacroXmlParser { + + public static MacroScreens parse(String xml) { + if (xml == null || xml.trim().isEmpty()) { + return new MacroScreens(); + } + return parse(new InputSource(new StringReader(xml.trim()))); + } + + public static MacroScreens parse(InputStream is) { + return parse(new InputSource(is)); + } + + public static MacroScreens parse(Reader reader) { + return parse(new InputSource(reader)); + } + + public static MacroScreens parse(InputSource inputSource) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + try { + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } catch (Throwable ignored) {} + try { + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + } catch (Throwable ignored) {} + try { + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + } catch (Throwable ignored) {} + + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(inputSource); + Element root = doc.getDocumentElement(); + + if (!root.getTagName().equalsIgnoreCase("HASCRIPT")) { + throw new IllegalArgumentException("Root element must be , found: <" + root.getTagName() + ">"); + } + + MacroScreens macroScreens = new MacroScreens(); + macroScreens.setName(getAttr(root, "name", "")); + macroScreens.setDescription(getAttr(root, "description", "")); + macroScreens.setAuthor(getAttr(root, "author", "")); + macroScreens.setCreationDate(getAttr(root, "creationdate", "")); + macroScreens.setTimeout(getIntAttr(root, "timeout", 10000)); + macroScreens.setPauseTime(getIntAttr(root, "pausetime", 300)); + macroScreens.setPromptAll(getBooleanAttr(root, "promptall", false)); + macroScreens.setBlockInput(getBooleanAttr(root, "blockinput", true)); + macroScreens.setUseVars(getBooleanAttr(root, "usevars", false)); + + MacroVariables vars = macroScreens.getVariables(); + + NodeList children = root.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + Element elem = (Element) node; + String tag = elem.getTagName().toLowerCase(); + + if (tag.equals("vars")) { + parseVars(elem, vars); + } else if (tag.equals("screen")) { + MacroScreen screen = parseScreen(elem, vars); + macroScreens.add(screen); + } + } + + return macroScreens; + } catch (Exception e) { + throw new RuntimeException("Failed to parse macro XML: " + e.getMessage(), e); + } + } + + private static void parseVars(Element varsElem, MacroVariables vars) { + NodeList list = varsElem.getChildNodes(); + for (int i = 0; i < list.getLength(); i++) { + Node n = list.item(i); + if (n.getNodeType() != Node.ELEMENT_NODE) continue; + Element el = (Element) n; + if (el.getTagName().equalsIgnoreCase("create")) { + String name = getAttr(el, "name", ""); + String type = getAttr(el, "type", "string"); + String value = getAttr(el, "value", ""); + if (!name.isEmpty()) { + vars.createVariable(name, type, value); + } + } + } + } + + private static MacroScreen parseScreen(Element screenElem, MacroVariables vars) { + String screenName = getAttr(screenElem, "name", ""); + MacroScreen screen = new MacroScreen(screenName); + + boolean entry = getBooleanAttr(screenElem, "entryscreen", false) + || getBooleanAttr(screenElem, "startscreen", false); + boolean exit = getBooleanAttr(screenElem, "exitscreen", false) + || getBooleanAttr(screenElem, "stopscreen", false); + boolean trans = getBooleanAttr(screenElem, "transient", false); + int pause = getIntAttr(screenElem, "pausetime", -1); + + screen.setEntryScreen(entry); + screen.setExitScreen(exit); + screen.setTransientScreen(trans); + screen.setPauseTime(pause); + + NodeList children = screenElem.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + Element elem = (Element) node; + String tag = elem.getTagName().toLowerCase(); + + if (tag.equals("description")) { + ECLScreenDesc desc = parseDescription(elem); + desc.SetName(screenName); + screen.setDescription(desc); + } else if (tag.equals("actions")) { + MacroActions actions = parseActions(elem); + screen.setActions(actions); + } else if (tag.equals("nextscreens")) { + MacroNextScreens next = parseNextScreens(elem); + screen.setNextScreens(next); + } else if (tag.equals("recolimit")) { + screen.setRecoLimit(getIntAttr(elem, "value", 0)); + screen.setRecoGoto(getAttr(elem, "goto", "")); + } + } + + return screen; + } + + private static ECLScreenDesc parseDescription(Element descElem) { + ECLScreenDesc desc = new ECLScreenDesc(); + NodeList children = descElem.getChildNodes(); + + for (int i = 0; i < children.getLength(); i++) { + Node n = children.item(i); + if (n.getNodeType() != Node.ELEMENT_NODE) continue; + Element elem = (Element) n; + String tag = elem.getTagName().toLowerCase(); + + boolean optional = getBooleanAttr(elem, "optional", false); + boolean invertmatch = getBooleanAttr(elem, "invertmatch", false); + + if (tag.equals("string")) { + String val = getAttr(elem, "value", ""); + int row = getIntAttr(elem, "row", 0); + int col = getIntAttr(elem, "col", 0); + boolean casesense = getBooleanAttr(elem, "casesense", false); + ECLSDString s = new ECLSDString(val, row, col, casesense, optional); + s.setInvertMatch(invertmatch); + desc.AddDescriptor(s); + } else if (tag.equals("cursor")) { + int row = getIntAttr(elem, "row", 0); + int col = getIntAttr(elem, "col", 0); + int pos = getIntAttr(elem, "pos", 0); + ECLSDCursor c; + if (row > 0 && col > 0) { + c = new ECLSDCursor(row, col, optional); + } else { + c = new ECLSDCursor(pos, optional); + } + c.setInvertMatch(invertmatch); + desc.AddDescriptor(c); + } else if (tag.equals("numfields")) { + int count = getIntAttr(elem, "value", 0); + ECLSDFields f = new ECLSDFields(count, optional); + f.setInvertMatch(invertmatch); + desc.AddDescriptor(f); + } else if (tag.equals("numinputfields")) { + int count = getIntAttr(elem, "value", 0); + ECLSDInputFields inf = new ECLSDInputFields(count, optional); + inf.setInvertMatch(invertmatch); + desc.AddDescriptor(inf); + } else if (tag.equals("oia")) { + String status = getAttr(elem, "status", getAttr(elem, "input", "NOTINHIBITED")); + ECLSDOIA oia = new ECLSDOIA(status, optional); + oia.setInvertMatch(invertmatch); + desc.AddDescriptor(oia); + } else if (tag.equals("checksum")) { + int val = getIntAttr(elem, "value", 0); + ECLSDChecksum cs = new ECLSDChecksum(val, optional); + cs.setInvertMatch(invertmatch); + desc.AddDescriptor(cs); + } else if (tag.equals("attrib")) { + String valStr = getAttr(elem, "value", ""); + char val = ECLSDAttrib.StringToAttrib(valStr); + int row = getIntAttr(elem, "row", 1); + int col = getIntAttr(elem, "col", 1); + int plane = ECLSDAttrib.StringToPlane(getAttr(elem, "plane", "FIELD_PLANE")); + ECLSDAttrib attr = new ECLSDAttrib(val, row, col, plane, optional); + attr.setInvertMatch(invertmatch); + desc.AddDescriptor(attr); + } else if (tag.equals("block")) { + int row = getIntAttr(elem, "row", 1); + int col = getIntAttr(elem, "col", 1); + int erow = getIntAttr(elem, "erow", -1); + int ecol = getIntAttr(elem, "ecol", -1); + boolean casesense = getBooleanAttr(elem, "casesense", false); + + ECLSDBlock blk; + if (erow > 0 || ecol > 0) { + blk = new ECLSDBlock(row, col, erow, ecol, casesense, optional); + } else { + blk = new ECLSDBlock(row, col, casesense, optional); + } + blk.setInvertMatch(invertmatch); + + NodeList subList = elem.getElementsByTagName("string"); + for (int j = 0; j < subList.getLength(); j++) { + Element strElem = (Element) subList.item(j); + blk.addString(getAttr(strElem, "value", "")); + } + desc.AddDescriptor(blk); + } + } + return desc; + } + + private static MacroActions parseActions(Element actionsElem) { + MacroActions actions = new MacroActions(); + actions.setPromptAll(getBooleanAttr(actionsElem, "promptall", false)); + + NodeList children = actionsElem.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node n = children.item(i); + if (n.getNodeType() != Node.ELEMENT_NODE) continue; + MacroAction act = parseActionElement((Element) n); + if (act != null) { + actions.add(act); + } + } + return actions; + } + + private static MacroAction parseActionElement(Element elem) { + String tag = elem.getTagName().toLowerCase(); + + if (tag.equals("input")) { + MacroActionInput act = new MacroActionInput(); + act.setValue(getAttr(elem, "value", "")); + act.setRow(getIntAttr(elem, "row", 0)); + act.setCol(getIntAttr(elem, "col", 0)); + act.setMoveCursor(getBooleanAttr(elem, "movecursor", true)); + act.setXlateHostKeys(getBooleanAttr(elem, "xlatehostkeys", true)); + act.setEncrypted(getBooleanAttr(elem, "encrypted", false)); + return act; + } else if (tag.equals("prompt")) { + MacroActionPrompt act = new MacroActionPrompt(); + act.setName(getAttr(elem, "name", "")); + act.setDescription(getAttr(elem, "description", "")); + act.setTitle(getAttr(elem, "title", "Macro Prompt")); + act.setDefaultValue(getAttr(elem, "default", "")); + act.setRow(getIntAttr(elem, "row", 0)); + act.setCol(getIntAttr(elem, "col", 0)); + act.setLen(getIntAttr(elem, "len", 80)); + act.setClear(getBooleanAttr(elem, "clear", false)); + act.setEncrypted(getBooleanAttr(elem, "encrypted", false)); + act.setMoveCursor(getBooleanAttr(elem, "movecursor", true)); + act.setXlateHostKeys(getBooleanAttr(elem, "xlatehostkeys", true)); + act.setVariable(getAttr(elem, "variable", "")); + act.setRequired(getBooleanAttr(elem, "required", false)); + act.setDontSend(getBooleanAttr(elem, "dontsend", false)); + return act; + } else if (tag.equals("extract")) { + MacroActionExtract act = new MacroActionExtract(); + act.setName(getAttr(elem, "name", "")); + act.setRow(getIntAttr(elem, "row", 1)); + act.setCol(getIntAttr(elem, "col", 1)); + act.setErow(getIntAttr(elem, "erow", -1)); + act.setEcol(getIntAttr(elem, "ecol", -1)); + act.setPlane(getAttr(elem, "plane", "TEXT_PLANE")); + act.setContinuous(getBooleanAttr(elem, "continuous", false)); + act.setFieldLevel(getBooleanAttr(elem, "fieldlevel", false)); + act.setVariable(getAttr(elem, "variable", "")); + return act; + } else if (tag.equals("pause")) { + MacroActionPause act = new MacroActionPause(); + String val = getAttr(elem, "value", "10000"); + try { + act.setTimeout(Integer.parseInt(val.trim())); + } catch (NumberFormatException e) { + act.setTimeoutExpr(val); + } + return act; + } else if (tag.equals("message")) { + MacroActionMessage act = new MacroActionMessage(); + act.setTitle(getAttr(elem, "title", "")); + act.setValue(getAttr(elem, "value", "")); + return act; + } else if (tag.equals("playmacro")) { + MacroActionPlayMacro act = new MacroActionPlayMacro(); + act.setName(getAttr(elem, "name", "")); + act.setStartScreen(getAttr(elem, "startscreen", "*DEFAULT*")); + return act; + } else if (tag.equals("commwait")) { + MacroActionCommWait act = new MacroActionCommWait(); + act.setValue(getAttr(elem, "value", "CONNECTION_READY")); + act.setTimeout(getIntAttr(elem, "timeout", 10000)); + return act; + } else if (tag.equals("varupdate")) { + MacroActionVarUpdate act = new MacroActionVarUpdate(); + act.setName(getAttr(elem, "name", "")); + act.setValue(getAttr(elem, "value", "")); + return act; + } else if (tag.equals("if")) { + MacroActionIf act = new MacroActionIf(getAttr(elem, "condition", "true")); + NodeList ifChildren = elem.getChildNodes(); + for (int j = 0; j < ifChildren.getLength(); j++) { + Node ifChild = ifChildren.item(j); + if (ifChild.getNodeType() != Node.ELEMENT_NODE) continue; + Element ifElem = (Element) ifChild; + if (ifElem.getTagName().equalsIgnoreCase("else")) { + NodeList elseChildren = ifElem.getChildNodes(); + for (int k = 0; k < elseChildren.getLength(); k++) { + Node elseChild = elseChildren.item(k); + if (elseChild.getNodeType() != Node.ELEMENT_NODE) continue; + MacroAction elseAct = parseActionElement((Element) elseChild); + if (elseAct != null) act.addElseAction(elseAct); + } + } else { + MacroAction thenAct = parseActionElement(ifElem); + if (thenAct != null) act.addIfAction(thenAct); + } + } + return act; + } + return null; + } + + private static MacroNextScreens parseNextScreens(Element nextElem) { + MacroNextScreens next = new MacroNextScreens(); + next.setTimeout(getIntAttr(nextElem, "timeout", 0)); + + NodeList list = nextElem.getElementsByTagName("nextscreen"); + for (int i = 0; i < list.getLength(); i++) { + Element el = (Element) list.item(i); + String name = getAttr(el, "name", ""); + if (!name.isEmpty()) { + next.addNextScreen(name); + } + } + return next; + } + + private static String getAttr(Element elem, String name, String defaultVal) { + if (elem == null || !elem.hasAttribute(name)) return defaultVal; + return elem.getAttribute(name); + } + + private static int getIntAttr(Element elem, String name, int defaultVal) { + if (elem == null || !elem.hasAttribute(name)) return defaultVal; + try { + return Integer.parseInt(elem.getAttribute(name).trim()); + } catch (NumberFormatException e) { + return defaultVal; + } + } + + private static boolean getBooleanAttr(Element elem, String name, boolean defaultVal) { + if (elem == null || !elem.hasAttribute(name)) return defaultVal; + String val = elem.getAttribute(name).trim(); + return val.equalsIgnoreCase("true") || val.equalsIgnoreCase("yes") || val.equals("1"); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/PlayThread.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/PlayThread.java new file mode 100644 index 0000000..c7c794d --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/PlayThread.java @@ -0,0 +1,237 @@ +package haus.nightmare.lib3270j.macro; + +import java.util.ArrayList; +import java.util.List; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.action.MacroAction; +import haus.nightmare.lib3270j.macro.action.MacroActionPrompt; + +/** + * Worker thread executing the HoD Macro finite state machine matching PlayThread. + */ +public class PlayThread extends Thread { + + private final Macro macro; + private final MacroScreens screens; + private final ECLSession session; + private final String initialScreenName; + + private volatile boolean running = true; + private volatile boolean paused = false; + private final Object pauseLock = new Object(); + + public PlayThread(Macro macro, MacroScreens screens, ECLSession session) { + this(macro, screens, session, null); + } + + public PlayThread(Macro macro, MacroScreens screens, ECLSession session, String initialScreenName) { + super("j3270-MacroPlayThread"); + setDaemon(true); + this.macro = macro; + this.screens = screens; + this.session = session; + this.initialScreenName = initialScreenName; + } + + public void halt() { + running = false; + resumePlay(); + interrupt(); + } + + public void pausePlay() { + paused = true; + } + + public void resumePlay() { + synchronized (pauseLock) { + paused = false; + pauseLock.notifyAll(); + } + } + + public boolean isPaused() { + return paused; + } + + @Override + public void run() { + try { + macro.setState(Macro.STATE_PLAYING); + + if (screens == null || screens.isEmpty()) { + macro.logError("Macro is empty - no screens to execute."); + macro.setState(Macro.STATE_STOPPED); + return; + } + + MacroScreen start = null; + if (initialScreenName != null && !initialScreenName.trim().isEmpty()) { + start = screens.getByName(initialScreenName); + } + if (start == null) { + start = screens.getStartScreen(); + } + + if (start == null) { + macro.logError("No valid starting screen found in macro."); + macro.setState(Macro.STATE_STOPPED); + return; + } + + List currentCandidates = new ArrayList<>(); + currentCandidates.add(start); + + long screenWaitStart = System.currentTimeMillis(); + int currentTimeout = screens.getTimeout() > 0 ? screens.getTimeout() : 10000; + + while (running && !isInterrupted()) { + // Handle pause + checkPause(); + if (!running) break; + + // 1. Try to find a matching screen among current candidates + MacroScreen matchedScreen = null; + ECLPS ps = (session != null) ? session.GetPS() : null; + + if (ps != null) { + for (MacroScreen cand : currentCandidates) { + if (cand.getDescription() != null && cand.getDescription().Matches(session)) { + matchedScreen = cand; + break; + } + } + } + + if (matchedScreen == null) { + // Check timeout + long elapsed = System.currentTimeMillis() - screenWaitStart; + if (elapsed > currentTimeout) { + StringBuilder names = new StringBuilder(); + for (MacroScreen c : currentCandidates) { + if (names.length() > 0) names.append(", "); + names.append(c.getName()); + } + macro.logError("Macro timed out waiting for screen(s): [" + names + "] after " + elapsed + "ms"); + macro.setState(Macro.STATE_ERROR); + return; + } + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + break; + } + continue; + } + + // 2. Screen matched! + matchedScreen.incrementRecoCount(); + if (matchedScreen.getRecoLimit() > 0 && matchedScreen.getRecoCount() > matchedScreen.getRecoLimit()) { + String gotoScreen = matchedScreen.getRecoGoto(); + if (gotoScreen != null && !gotoScreen.trim().isEmpty() && screens.contains(gotoScreen)) { + matchedScreen.resetRecoCount(); + currentCandidates.clear(); + currentCandidates.add(screens.getByName(gotoScreen)); + screenWaitStart = System.currentTimeMillis(); + continue; + } else { + macro.logError("Recognition limit (" + matchedScreen.getRecoLimit() + ") exceeded on screen: " + matchedScreen.getName()); + macro.setState(Macro.STATE_ERROR); + return; + } + } + + macro.notifyScreenMatching(matchedScreen); + + // Pause time before actions if configured + int pauseTime = matchedScreen.getPauseTime() >= 0 ? matchedScreen.getPauseTime() : screens.getPauseTime(); + if (pauseTime > 0) { + try { + Thread.sleep(pauseTime); + } catch (InterruptedException e) { + break; + } + } + + // 3. Execute actions + MacroActions actions = matchedScreen.getActions(); + if (actions != null) { + for (MacroAction act : actions.getActions()) { + checkPause(); + if (!running) break; + + macro.notifyActionExecuting(act); + act.execute(macro, session); + + if (act instanceof MacroActionPrompt && ((MacroActionPrompt) act).isCancelled()) { + running = false; + break; + } + } + } + + if (!running) break; + + // 4. Check if screen is exit screen + if (matchedScreen.isExitScreen()) { + macro.setState(Macro.STATE_STOPPED); + return; + } + + // 5. Transition to nextscreens + MacroNextScreens next = matchedScreen.getNextScreens(); + if (next == null || next.isEmpty()) { + // No next screens -> macro successfully completed + macro.setState(Macro.STATE_STOPPED); + return; + } + + List nextCandidates = new ArrayList<>(); + for (String nextName : next.getNextScreenNames()) { + MacroScreen s = screens.getByName(nextName); + if (s != null) { + nextCandidates.add(s); + } + } + + if (nextCandidates.isEmpty()) { + macro.logError("Next screens configured but none exist in macro: " + next.getNextScreenNames()); + macro.setState(Macro.STATE_ERROR); + return; + } + + currentCandidates = nextCandidates; + screenWaitStart = System.currentTimeMillis(); + currentTimeout = (next.getTimeout() > 0) ? next.getTimeout() : (screens.getTimeout() > 0 ? screens.getTimeout() : 10000); + } + } catch (Exception e) { + macro.logError("Macro execution error: " + e.getMessage()); + macro.setState(Macro.STATE_ERROR); + } finally { + if (macro.getState() == Macro.STATE_PLAYING) { + macro.setState(Macro.STATE_STOPPED); + } + // Reset reco counters + if (screens != null) { + for (MacroScreen s : screens.getScreens()) { + s.resetRecoCount(); + } + } + } + } + + private void checkPause() { + while (paused && running) { + synchronized (pauseLock) { + try { + pauseLock.wait(100); + } catch (InterruptedException e) { + running = false; + break; + } + } + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroAction.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroAction.java new file mode 100644 index 0000000..880f0f8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroAction.java @@ -0,0 +1,108 @@ +package haus.nightmare.lib3270j.macro.action; + +import java.io.Serializable; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; +import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser; +import haus.nightmare.lib3270j.macro.variable.MacroVariables; + +/** + * Abstract base class for all macro actions matching IBM Host On-Demand MacroAction. + */ +public abstract class MacroAction implements Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + protected Macro mMacro; + protected ECLSession mECLSession; + protected int mLineNum = -1; + protected String comments = ""; + + public MacroAction() {} + + public void setOwner(Macro macro) { + this.mMacro = macro; + } + + public Macro getOwner() { + return mMacro; + } + + public void setECLSession(ECLSession session) { + this.mECLSession = session; + } + + public ECLSession getECLSession() { + return mECLSession; + } + + public int getLineNum() { + return mLineNum; + } + + public void setLineNum(int lineNum) { + this.mLineNum = lineNum; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + /** + * Executes this macro action against the given macro engine and host session. + */ + public abstract void execute(Macro macro, ECLSession session); + + /** + * Convenience execute method invoking the bound owner and session. + */ + public void execute() { + if (mMacro != null) { + execute(mMacro, mECLSession != null ? mECLSession : mMacro.getECLSession()); + } + } + + public void halt() {} + + /** + * Serializes this action to its XML representation. + */ + public abstract String format(int indent); + + public String getXML() { + return format(0); + } + + protected String resolve(String expression, Macro macro) { + if (expression == null) return ""; + if (macro == null) return expression; + MacroVariables vars = macro.getVariables(); + if (vars == null || !macro.isUseVars()) { + return expression; + } + return MacroExpressionParser.evaluateString(vars, expression); + } + + protected String indentSpaces(int indent) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < indent; i++) { + sb.append(" "); + } + return sb.toString(); + } + + public static String escapeXml(String text) { + if (text == null) return ""; + return text.replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">"); + } + + @Override + public abstract MacroAction clone(); +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionCommWait.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionCommWait.java new file mode 100644 index 0000000..640206b --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionCommWait.java @@ -0,0 +1,69 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLConnection; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; + +/** + * Macro action matching IBM HoD <commwait>. + * Waits for the host session connection to reach a desired state (e.g. CONNECTION_READY). + */ +public class MacroActionCommWait extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String value = "CONNECTION_READY"; + private int timeout = 10000; + + public MacroActionCommWait() {} + + public MacroActionCommWait(String value, int timeout) { + this.value = (value != null) ? value : "CONNECTION_READY"; + this.timeout = timeout; + } + + public String getValue() { return value; } + public void setValue(String value) { this.value = (value != null) ? value : "CONNECTION_READY"; } + + public int getTimeout() { return timeout; } + public void setTimeout(int timeout) { this.timeout = timeout; } + + @Override + public void execute(Macro macro, ECLSession session) { + if (session == null) return; + ECLConnection conn = session.GetConnection(); + if (conn == null) return; + + long start = System.currentTimeMillis(); + long maxWait = (timeout > 0) ? timeout : 10000; + + while (System.currentTimeMillis() - start < maxWait) { + if (conn.IsReady() || conn.IsConnected()) { + return; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(" 0) sb.append(" timeout=\"").append(timeout).append("\""); + sb.append(" />"); + return sb.toString(); + } + + @Override + public MacroActionCommWait clone() { + MacroActionCommWait c = new MacroActionCommWait(value, timeout); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionExtract.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionExtract.java new file mode 100644 index 0000000..89a6812 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionExtract.java @@ -0,0 +1,144 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; + +/** + * Macro action matching IBM HoD <extract>. + * Extracts text from the host screen into a macro variable. + */ +public class MacroActionExtract extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String name = ""; + private int row = 1; + private int col = 1; + private int erow = -1; + private int ecol = -1; + private String plane = "TEXT_PLANE"; + private boolean continuous = false; + private boolean fieldLevel = false; + private String variable = ""; + private String lastExtracted = ""; + + public MacroActionExtract() {} + + public String getName() { return name; } + public void setName(String name) { this.name = (name != null) ? name : ""; } + + public int getRow() { return row; } + public void setRow(int row) { this.row = row; } + + public int getCol() { return col; } + public void setCol(int col) { this.col = col; } + + public int getErow() { return erow; } + public void setErow(int erow) { this.erow = erow; } + + public int getEcol() { return ecol; } + public void setEcol(int ecol) { this.ecol = ecol; } + + public String getPlane() { return plane; } + public void setPlane(String plane) { this.plane = (plane != null) ? plane : "TEXT_PLANE"; } + + public boolean isContinuous() { return continuous; } + public void setContinuous(boolean continuous) { this.continuous = continuous; } + + public boolean isFieldLevel() { return fieldLevel; } + public void setFieldLevel(boolean fieldLevel) { this.fieldLevel = fieldLevel; } + + public String getVariable() { return variable; } + public void setVariable(String variable) { this.variable = (variable != null) ? variable : ""; } + + public String getLastExtracted() { return lastExtracted; } + + @Override + public void execute(Macro macro, ECLSession session) { + if (session == null) return; + ECLPS ps = session.GetPS(); + if (ps == null) return; + + try { + int r = Math.max(1, row); + int c = Math.max(1, col); + int er = (erow > 0) ? erow : r; + int ec = (ecol > 0) ? ecol : (col > 0 && erow <= 0 ? c : ps.GetSizeCols()); + + int planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_TEXT; + if ("COLOR_PLANE".equalsIgnoreCase(plane) || "PLANE_COLOR".equalsIgnoreCase(plane)) { + planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_COLOR; + } else if ("FIELD_PLANE".equalsIgnoreCase(plane) || "PLANE_FIELD".equalsIgnoreCase(plane)) { + planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_FIELD; + } else if ("HILITE_PLANE".equalsIgnoreCase(plane) || "PLANE_HILITE".equalsIgnoreCase(plane)) { + planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_HILITE; + } + + String extracted; + if (continuous) { + int startPos = ps.ConvertRowColToPos(r, c); + int endPos = ps.ConvertRowColToPos(er, ec); + int len = Math.max(0, endPos - startPos + 1); + char[] buf = new char[len]; + if (planeVal == haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_TEXT) { + ps.GetString(buf, len, startPos); + } else { + ps.getPlane(planeVal, buf, startPos - 1, len); + } + extracted = new String(buf); + } else { + int height = er - r + 1; + int width = ec - c + 1; + int len = height * width; + char[] buf = new char[len]; + ps.GetScreenRect(buf, len, r, c, er, ec, planeVal); + extracted = new String(buf); + } + + this.lastExtracted = extracted; + + if (variable != null && !variable.trim().isEmpty() && macro != null && macro.getVariables() != null) { + macro.getVariables().setVariable(variable, extracted); + } + } catch (Exception e) { + if (macro != null) { + macro.logError("MacroActionExtract error: " + e.getMessage()); + } + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(" 0) sb.append(" erow=\"").append(erow).append("\""); + if (ecol > 0) sb.append(" ecol=\"").append(ecol).append("\""); + if (plane != null && !plane.equals("TEXT_PLANE")) sb.append(" plane=\"").append(escapeXml(plane)).append("\""); + if (continuous) sb.append(" continuous=\"true\""); + if (fieldLevel) sb.append(" fieldlevel=\"true\""); + if (!variable.isEmpty()) sb.append(" variable=\"").append(escapeXml(variable)).append("\""); + sb.append(" />"); + return sb.toString(); + } + + @Override + public MacroActionExtract clone() { + MacroActionExtract c = new MacroActionExtract(); + c.setName(name); + c.setRow(row); + c.setCol(col); + c.setErow(erow); + c.setEcol(ecol); + c.setPlane(plane); + c.setContinuous(continuous); + c.setFieldLevel(fieldLevel); + c.setVariable(variable); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionIf.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionIf.java new file mode 100644 index 0000000..da0fcf8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionIf.java @@ -0,0 +1,87 @@ +package haus.nightmare.lib3270j.macro.action; + +import java.util.ArrayList; +import java.util.List; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; +import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser; + +/** + * Macro action matching IBM HoD <if>. + * Evaluates a conditional expression and branches to then-actions or else-actions. + */ +public class MacroActionIf extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String condition = "true"; + private final List ifActions = new ArrayList<>(); + private final List elseActions = new ArrayList<>(); + + public MacroActionIf() {} + + public MacroActionIf(String condition) { + this.condition = (condition != null) ? condition : "true"; + } + + public String getCondition() { return condition; } + public void setCondition(String condition) { this.condition = (condition != null) ? condition : "true"; } + + public List getIfActions() { return ifActions; } + public void addIfAction(MacroAction action) { if (action != null) ifActions.add(action); } + + public List getElseActions() { return elseActions; } + public void addElseAction(MacroAction action) { if (action != null) elseActions.add(action); } + + public List getActiveBranchActions(Macro macro) { + boolean match = evaluateCondition(macro); + return match ? ifActions : elseActions; + } + + public boolean evaluateCondition(Macro macro) { + if (condition == null || condition.trim().isEmpty() || condition.equalsIgnoreCase("true")) { + return true; + } + if (macro != null && macro.getVariables() != null) { + return MacroExpressionParser.evaluateBoolean(macro.getVariables(), condition); + } + return false; + } + + @Override + public void execute(Macro macro, ECLSession session) { + List branch = getActiveBranchActions(macro); + for (MacroAction act : branch) { + act.execute(macro, session); + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + String ind = indentSpaces(indent); + sb.append(ind).append("\n"); + for (MacroAction act : ifActions) { + sb.append(act.format(indent + 1)).append("\n"); + } + if (!elseActions.isEmpty()) { + sb.append(ind).append(" \n"); + for (MacroAction act : elseActions) { + sb.append(act.format(indent + 2)).append("\n"); + } + sb.append(ind).append(" \n"); + } + sb.append(ind).append(""); + return sb.toString(); + } + + @Override + public MacroActionIf clone() { + MacroActionIf c = new MacroActionIf(condition); + for (MacroAction a : ifActions) c.addIfAction(a.clone()); + for (MacroAction a : elseActions) c.addElseAction(a.clone()); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionInput.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionInput.java new file mode 100644 index 0000000..d2358a8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionInput.java @@ -0,0 +1,114 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; +import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser; + +/** + * Macro action matching IBM HoD <input>. + * Inputs text and keystroke mnemonics into the host presentation space. + */ +public class MacroActionInput extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String value = ""; + private int row = 0; + private int col = 0; + private boolean moveCursor = true; + private boolean xlateHostKeys = true; + private boolean encrypted = false; + + public MacroActionInput() {} + + public MacroActionInput(String value) { + this.value = (value != null) ? value : ""; + } + + public MacroActionInput(String value, int row, int col, boolean moveCursor, boolean xlateHostKeys) { + this.value = (value != null) ? value : ""; + this.row = row; + this.col = col; + this.moveCursor = moveCursor; + this.xlateHostKeys = xlateHostKeys; + } + + public String getValue() { return value; } + public void setValue(String value) { this.value = (value != null) ? value : ""; } + + public int getRow() { return row; } + public void setRow(int row) { this.row = row; } + + public int getCol() { return col; } + public void setCol(int col) { this.col = col; } + + public boolean isMoveCursor() { return moveCursor; } + public void setMoveCursor(boolean moveCursor) { this.moveCursor = moveCursor; } + + public boolean isXlateHostKeys() { return xlateHostKeys; } + public void setXlateHostKeys(boolean xlateHostKeys) { this.xlateHostKeys = xlateHostKeys; } + + public boolean isEncrypted() { return encrypted; } + public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; } + + @Override + public void execute(Macro macro, ECLSession session) { + if (session == null) return; + ECLPS ps = session.GetPS(); + if (ps == null) return; + + String text = value; + if (macro != null && macro.isUseVars()) { + text = MacroExpressionParser.resolveText(macro.getVariables(), value); + if (text.equals(value) && (value.contains("+") || value.contains("-") || value.startsWith("$"))) { + text = MacroExpressionParser.evaluateString(macro.getVariables(), value); + } + } + + try { + if (row > 0 && col > 0) { + if (moveCursor) { + ps.SetCursorPos(row, col); + } + if (xlateHostKeys) { + ps.SendKeys(text, row, col); + } else { + ps.SetText(text, row, col); + } + } else { + if (xlateHostKeys) { + ps.SendKeys(text); + } else { + ps.SetText(text); + } + } + } catch (Exception e) { + if (macro != null) { + macro.logError("MacroActionInput failed: " + e.getMessage()); + } + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(" 0) sb.append(" row=\"").append(row).append("\""); + if (col > 0) sb.append(" col=\"").append(col).append("\""); + if (!moveCursor) sb.append(" movecursor=\"false\""); + if (!xlateHostKeys) sb.append(" xlatehostkeys=\"false\""); + if (encrypted) sb.append(" encrypted=\"true\""); + sb.append(" />"); + return sb.toString(); + } + + @Override + public MacroActionInput clone() { + MacroActionInput c = new MacroActionInput(value, row, col, moveCursor, xlateHostKeys); + c.setEncrypted(encrypted); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionMessage.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionMessage.java new file mode 100644 index 0000000..2b2b4cd --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionMessage.java @@ -0,0 +1,60 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; +import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser; + +/** + * Macro action matching IBM HoD <message>. + * Displays a popup message or status notification. + */ +public class MacroActionMessage extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String title = ""; + private String value = ""; + + public MacroActionMessage() {} + + public MacroActionMessage(String title, String value) { + this.title = (title != null) ? title : ""; + this.value = (value != null) ? value : ""; + } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = (title != null) ? title : ""; } + + public String getValue() { return value; } + public void setValue(String value) { this.value = (value != null) ? value : ""; } + + @Override + public void execute(Macro macro, ECLSession session) { + String t = title; + String v = value; + if (macro != null && macro.isUseVars()) { + t = MacroExpressionParser.resolveText(macro.getVariables(), title); + v = MacroExpressionParser.resolveText(macro.getVariables(), value); + } + + if (macro != null) { + macro.notifyMessage(t, v); + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(""); + return sb.toString(); + } + + @Override + public MacroActionMessage clone() { + MacroActionMessage c = new MacroActionMessage(title, value); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPause.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPause.java new file mode 100644 index 0000000..c419484 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPause.java @@ -0,0 +1,62 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; +import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser; + +/** + * Macro action matching IBM HoD <pause>. + * Pauses macro execution for a specified duration in milliseconds. + */ +public class MacroActionPause extends MacroAction { + + private static final long serialVersionUID = 1L; + + private int timeout = 10000; + private String timeoutExpr = null; + + public MacroActionPause() {} + + public MacroActionPause(int timeout) { + this.timeout = timeout; + } + + public int getTimeout() { return timeout; } + public void setTimeout(int timeout) { this.timeout = timeout; } + + public String getTimeoutExpr() { return timeoutExpr; } + public void setTimeoutExpr(String timeoutExpr) { this.timeoutExpr = timeoutExpr; } + + @Override + public void execute(Macro macro, ECLSession session) { + int duration = timeout; + if (timeoutExpr != null && macro != null && macro.getVariables() != null) { + duration = MacroExpressionParser.evaluateInt(macro.getVariables(), timeoutExpr); + } + if (duration <= 0) return; + + try { + Thread.sleep(duration); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(""); + return sb.toString(); + } + + @Override + public MacroActionPause clone() { + MacroActionPause c = new MacroActionPause(timeout); + c.setTimeoutExpr(timeoutExpr); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPlayMacro.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPlayMacro.java new file mode 100644 index 0000000..92f64a9 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPlayMacro.java @@ -0,0 +1,61 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; + +/** + * Macro action matching IBM HoD <playmacro>. + * Chains execution to another macro. + */ +public class MacroActionPlayMacro extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String name = ""; + private String startScreen = "*DEFAULT*"; + + public MacroActionPlayMacro() {} + + public MacroActionPlayMacro(String name) { + this.name = (name != null) ? name : ""; + } + + public MacroActionPlayMacro(String name, String startScreen) { + this.name = (name != null) ? name : ""; + if (startScreen != null && !startScreen.trim().isEmpty()) { + this.startScreen = startScreen.trim(); + } + } + + public String getName() { return name; } + public void setName(String name) { this.name = (name != null) ? name : ""; } + + public String getStartScreen() { return startScreen; } + public void setStartScreen(String startScreen) { this.startScreen = (startScreen != null) ? startScreen : "*DEFAULT*"; } + + @Override + public void execute(Macro macro, ECLSession session) { + if (macro != null) { + macro.playChainedMacro(name, startScreen); + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(""); + return sb.toString(); + } + + @Override + public MacroActionPlayMacro clone() { + MacroActionPlayMacro c = new MacroActionPlayMacro(name, startScreen); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPrompt.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPrompt.java new file mode 100644 index 0000000..e1badab --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionPrompt.java @@ -0,0 +1,176 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; + +/** + * Macro action matching IBM HoD <prompt>. + * Requests user input via modal dialog, updates variable, and optionally types input to host. + */ +public class MacroActionPrompt extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String name = ""; + private String description = ""; + private String title = "Macro Prompt"; + private String defaultValue = ""; + private int row = 0; + private int col = 0; + private int len = 80; + private boolean clear = false; + private boolean encrypted = false; + private boolean moveCursor = true; + private boolean xlateHostKeys = true; + private String variable = ""; + private boolean required = false; + private boolean dontSend = false; + private boolean cancelled = false; + + public MacroActionPrompt() {} + + public String getName() { return name; } + public void setName(String name) { this.name = (name != null) ? name : ""; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = (description != null) ? description : ""; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = (title != null) ? title : "Macro Prompt"; } + + public String getDefaultValue() { return defaultValue; } + public void setDefaultValue(String defaultValue) { this.defaultValue = (defaultValue != null) ? defaultValue : ""; } + + public int getRow() { return row; } + public void setRow(int row) { this.row = row; } + + public int getCol() { return col; } + public void setCol(int col) { this.col = col; } + + public int getLen() { return len; } + public void setLen(int len) { this.len = len; } + + public boolean isClear() { return clear; } + public void setClear(boolean clear) { this.clear = clear; } + + public boolean isEncrypted() { return encrypted; } + public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; } + + public boolean isMoveCursor() { return moveCursor; } + public void setMoveCursor(boolean moveCursor) { this.moveCursor = moveCursor; } + + public boolean isXlateHostKeys() { return xlateHostKeys; } + public void setXlateHostKeys(boolean xlateHostKeys) { this.xlateHostKeys = xlateHostKeys; } + + public String getVariable() { return variable; } + public void setVariable(String variable) { this.variable = (variable != null) ? variable : ""; } + + public boolean isRequired() { return required; } + public void setRequired(boolean required) { this.required = required; } + + public boolean isDontSend() { return dontSend; } + public void setDontSend(boolean dontSend) { this.dontSend = dontSend; } + + public boolean isCancelled() { return cancelled; } + public void setCancelled(boolean cancelled) { this.cancelled = cancelled; } + + @Override + public void execute(Macro macro, ECLSession session) { + String response = defaultValue; + cancelled = false; + + if (macro != null && macro.getPromptHandler() != null) { + String prompted = macro.getPromptHandler().prompt(this); + if (prompted == null) { + cancelled = true; + macro.stop(); + return; + } + response = prompted; + } + + if (variable != null && !variable.trim().isEmpty() && macro != null && macro.getVariables() != null) { + macro.getVariables().setVariable(variable, response); + } + + if (!dontSend && session != null) { + ECLPS ps = session.GetPS(); + if (ps != null) { + try { + if (clear) { + if (row > 0 && col > 0) { + ps.SendKeys("[erasefld]", row, col); + } else { + ps.SendKeys("[erasefld]"); + } + } + if (row > 0 && col > 0) { + if (moveCursor) { + ps.SetCursorPos(row, col); + } + if (xlateHostKeys) { + ps.SendKeys(response, row, col); + } else { + ps.SetText(response, row, col); + } + } else { + if (xlateHostKeys) { + ps.SendKeys(response); + } else { + ps.SetText(response); + } + } + } catch (Exception e) { + if (macro != null) { + macro.logError("MacroActionPrompt typing error: " + e.getMessage()); + } + } + } + } + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(" 0) sb.append(" row=\"").append(row).append("\""); + if (col > 0) sb.append(" col=\"").append(col).append("\""); + if (len != 80) sb.append(" len=\"").append(len).append("\""); + if (clear) sb.append(" clear=\"true\""); + if (encrypted) sb.append(" encrypted=\"true\""); + if (!moveCursor) sb.append(" movecursor=\"false\""); + if (!xlateHostKeys) sb.append(" xlatehostkeys=\"false\""); + if (!variable.isEmpty()) sb.append(" variable=\"").append(escapeXml(variable)).append("\""); + if (required) sb.append(" required=\"true\""); + if (dontSend) sb.append(" dontsend=\"true\""); + sb.append(" />"); + return sb.toString(); + } + + @Override + public MacroActionPrompt clone() { + MacroActionPrompt c = new MacroActionPrompt(); + c.setName(name); + c.setDescription(description); + c.setTitle(title); + c.setDefaultValue(defaultValue); + c.setRow(row); + c.setCol(col); + c.setLen(len); + c.setClear(clear); + c.setEncrypted(encrypted); + c.setMoveCursor(moveCursor); + c.setXlateHostKeys(xlateHostKeys); + c.setVariable(variable); + c.setRequired(required); + c.setDontSend(dontSend); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionVarUpdate.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionVarUpdate.java new file mode 100644 index 0000000..2411115 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/action/MacroActionVarUpdate.java @@ -0,0 +1,57 @@ +package haus.nightmare.lib3270j.macro.action; + +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.macro.Macro; +import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser; +import haus.nightmare.lib3270j.macro.variable.MacroVariables; + +/** + * Macro action matching IBM HoD <varupdate>. + * Evaluates an expression and assigns the result to a macro variable. + */ +public class MacroActionVarUpdate extends MacroAction { + + private static final long serialVersionUID = 1L; + + private String name = ""; + private String value = ""; + + public MacroActionVarUpdate() {} + + public MacroActionVarUpdate(String name, String value) { + this.name = (name != null) ? name : ""; + this.value = (value != null) ? value : ""; + } + + public String getName() { return name; } + public void setName(String name) { this.name = (name != null) ? name : ""; } + + public String getValue() { return value; } + public void setValue(String value) { this.value = (value != null) ? value : ""; } + + @Override + public void execute(Macro macro, ECLSession session) { + if (macro == null || name == null || name.trim().isEmpty()) return; + MacroVariables vars = macro.getVariables(); + if (vars == null) return; + + Object evalRes = MacroExpressionParser.evaluate(vars, value); + vars.setVariable(name, evalRes); + } + + @Override + public String format(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(indentSpaces(indent)).append(""); + return sb.toString(); + } + + @Override + public MacroActionVarUpdate clone() { + MacroActionVarUpdate c = new MacroActionVarUpdate(name, value); + c.setLineNum(mLineNum); + c.setComments(comments); + return c; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroExpressionParser.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroExpressionParser.java new file mode 100644 index 0000000..a332f8f --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroExpressionParser.java @@ -0,0 +1,490 @@ +package haus.nightmare.lib3270j.macro.variable; + +import java.io.Serializable; +import java.util.*; + +/** + * Expression evaluator for IBM Host On-Demand macros. + * Evaluates arithmetic (+, -, *, /, %), string concatenation, comparisons (==, !=, <, >, <=, >=), + * and logical operations (&&, ||, !, AND, OR, NOT) with variable substitution. + */ +public class MacroExpressionParser implements Serializable { + + private static final long serialVersionUID = 1L; + + public static Object evaluate(MacroVariables vars, String expression) { + if (expression == null) return ""; + String trimmed = expression.trim(); + if (trimmed.isEmpty() || trimmed.equals("''") || trimmed.equals("\"\"")) { + return ""; + } + + try { + Parser parser = new Parser(vars, trimmed); + return parser.parseExpression(); + } catch (Exception e) { + // If expression parsing fails, try simple variable resolution or return raw string + if (vars != null) { + return resolveText(vars, expression); + } + return expression; + } + } + + public static String evaluateString(MacroVariables vars, String expression) { + Object val = evaluate(vars, expression); + return (val != null) ? val.toString() : ""; + } + + public static int evaluateInt(MacroVariables vars, String expression) { + Object val = evaluate(vars, expression); + if (val instanceof Number) { + return ((Number) val).intValue(); + } else if (val != null) { + try { + return Integer.parseInt(val.toString().trim()); + } catch (Exception e) { + try { + return (int) Double.parseDouble(val.toString().trim()); + } catch (Exception ignored) {} + } + } + return 0; + } + + public static double evaluateDouble(MacroVariables vars, String expression) { + Object val = evaluate(vars, expression); + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } else if (val != null) { + try { + return Double.parseDouble(val.toString().trim()); + } catch (Exception ignored) {} + } + return 0.0; + } + + public static boolean evaluateBoolean(MacroVariables vars, String expression) { + Object val = evaluate(vars, expression); + if (val instanceof Boolean) { + return (Boolean) val; + } else if (val instanceof Number) { + return ((Number) val).doubleValue() != 0.0; + } else if (val != null) { + String s = val.toString().trim(); + if (s.equalsIgnoreCase("true") || s.equals("1")) return true; + if (s.equalsIgnoreCase("false") || s.equals("0")) return false; + return !s.isEmpty(); + } + return false; + } + + public static String resolveText(MacroVariables vars, String text) { + if (text == null || vars == null || !text.contains("$")) { + return text; + } + + StringBuilder sb = new StringBuilder(); + int len = text.length(); + int i = 0; + while (i < len) { + char c = text.charAt(i); + if (c == '$') { + int nextDollar = text.indexOf('$', i + 1); + if (nextDollar > i + 1) { + String candidate = text.substring(i, nextDollar + 1); + if (vars.containsKey(candidate)) { + sb.append(vars.getString(candidate)); + i = nextDollar + 1; + continue; + } + } + // Check if variable without closing dollar: $varName + int j = i + 1; + while (j < len && (Character.isLetterOrDigit(text.charAt(j)) || text.charAt(j) == '_' || text.charAt(j) == '-')) { + j++; + } + if (j > i + 1) { + String varName = text.substring(i, j); + if (vars.containsKey(varName)) { + sb.append(vars.getString(varName)); + i = j; + continue; + } + } + } + sb.append(c); + i++; + } + return sb.toString(); + } + + // ========================================== + // Parser Implementation + // ========================================== + + private static class Parser { + private final MacroVariables vars; + private final String input; + private int pos = 0; + private final int length; + + Parser(MacroVariables vars, String input) { + this.vars = vars; + this.input = input; + this.length = input.length(); + } + + Object parseExpression() { + Object res = parseLogicalOr(); + skipWhitespace(); + return res; + } + + private Object parseLogicalOr() { + Object left = parseLogicalAnd(); + while (true) { + skipWhitespace(); + if (match("||") || matchKeyword("OR")) { + Object right = parseLogicalAnd(); + left = toBooleanVal(left) || toBooleanVal(right); + } else { + break; + } + } + return left; + } + + private Object parseLogicalAnd() { + Object left = parseEquality(); + while (true) { + skipWhitespace(); + if (match("&&") || matchKeyword("AND")) { + Object right = parseEquality(); + left = toBooleanVal(left) && toBooleanVal(right); + } else { + break; + } + } + return left; + } + + private Object parseEquality() { + Object left = parseRelational(); + while (true) { + skipWhitespace(); + if (match("==") || matchKeyword("EQ")) { + Object right = parseRelational(); + left = compareEqual(left, right); + } else if (match("!=") || match("<>") || matchKeyword("NE")) { + Object right = parseRelational(); + left = !compareEqual(left, right); + } else { + break; + } + } + return left; + } + + private Object parseRelational() { + Object left = parseAdditive(); + while (true) { + skipWhitespace(); + if (match("<=") || matchKeyword("LE")) { + Object right = parseAdditive(); + left = compareRelational(left, right) <= 0; + } else if (match(">=") || matchKeyword("GE")) { + Object right = parseAdditive(); + left = compareRelational(left, right) >= 0; + } else if (match("<") || matchKeyword("LT")) { + Object right = parseAdditive(); + left = compareRelational(left, right) < 0; + } else if (match(">") || matchKeyword("GT")) { + Object right = parseAdditive(); + left = compareRelational(left, right) > 0; + } else { + break; + } + } + return left; + } + + private Object parseAdditive() { + Object left = parseMultiplicative(); + while (true) { + skipWhitespace(); + if (pos < length && input.charAt(pos) == '+' && (pos + 1 >= length || input.charAt(pos + 1) != '+')) { + pos++; + Object right = parseMultiplicative(); + if (left instanceof String || right instanceof String) { + left = String.valueOf(left) + String.valueOf(right); + } else if (isDouble(left) || isDouble(right)) { + left = toDoubleVal(left) + toDoubleVal(right); + } else { + left = toIntVal(left) + toIntVal(right); + } + } else if (pos < length && input.charAt(pos) == '-' && (pos + 1 >= length || input.charAt(pos + 1) != '-')) { + pos++; + Object right = parseMultiplicative(); + if (isDouble(left) || isDouble(right)) { + left = toDoubleVal(left) - toDoubleVal(right); + } else { + left = toIntVal(left) - toIntVal(right); + } + } else { + break; + } + } + return left; + } + + private Object parseMultiplicative() { + Object left = parseUnary(); + while (true) { + skipWhitespace(); + if (pos < length && input.charAt(pos) == '*') { + pos++; + Object right = parseUnary(); + if (isDouble(left) || isDouble(right)) { + left = toDoubleVal(left) * toDoubleVal(right); + } else { + left = toIntVal(left) * toIntVal(right); + } + } else if (pos < length && input.charAt(pos) == '/') { + pos++; + Object right = parseUnary(); + double r = toDoubleVal(right); + if (r == 0.0) throw new ArithmeticException("Division by zero"); + if (isDouble(left) || isDouble(right)) { + left = toDoubleVal(left) / r; + } else { + left = toIntVal(left) / toIntVal(right); + } + } else if (pos < length && input.charAt(pos) == '%') { + pos++; + Object right = parseUnary(); + left = toIntVal(left) % toIntVal(right); + } else { + break; + } + } + return left; + } + + private Object parseUnary() { + skipWhitespace(); + if (match("!") || matchKeyword("NOT")) { + Object val = parseUnary(); + return !toBooleanVal(val); + } + if (pos < length && input.charAt(pos) == '+') { + pos++; + return parseUnary(); + } + if (pos < length && input.charAt(pos) == '-') { + pos++; + Object val = parseUnary(); + if (isDouble(val)) { + return -toDoubleVal(val); + } else { + return -toIntVal(val); + } + } + return parsePrimary(); + } + + private Object parsePrimary() { + skipWhitespace(); + if (pos >= length) return ""; + + char c = input.charAt(pos); + + // Parentheses + if (c == '(') { + pos++; + Object val = parseExpression(); + skipWhitespace(); + if (pos < length && input.charAt(pos) == ')') { + pos++; + } + return val; + } + + // String literals: 'string' or "string" + if (c == '\'' || c == '"') { + char quote = c; + pos++; + StringBuilder sb = new StringBuilder(); + while (pos < length) { + char ch = input.charAt(pos); + if (ch == '\\' && pos + 1 < length) { + pos++; + sb.append(input.charAt(pos)); + } else if (ch == quote) { + pos++; + break; + } else { + sb.append(ch); + } + pos++; + } + return sb.toString(); + } + + // Variable reference: $var$ or $var + if (c == '$') { + pos++; + StringBuilder varName = new StringBuilder("$"); + while (pos < length) { + char ch = input.charAt(pos); + if (Character.isLetterOrDigit(ch) || ch == '_' || ch == '-') { + varName.append(ch); + pos++; + } else if (ch == '$') { + varName.append('$'); + pos++; + break; + } else { + break; + } + } + String name = varName.toString(); + if (vars != null) { + if (vars.containsKey(name)) { + return vars.get(name); + } + } + return name; + } + + // Number literal + if (Character.isDigit(c) || (c == '.' && pos + 1 < length && Character.isDigit(input.charAt(pos + 1)))) { + int start = pos; + boolean isDec = false; + while (pos < length && (Character.isDigit(input.charAt(pos)) || input.charAt(pos) == '.')) { + if (input.charAt(pos) == '.') { + if (isDec) break; + isDec = true; + } + pos++; + } + String numStr = input.substring(start, pos); + if (isDec) { + return Double.parseDouble(numStr); + } else { + try { + return Integer.parseInt(numStr); + } catch (NumberFormatException e) { + return Long.parseLong(numStr); + } + } + } + + // Identifiers: true, false, null, or words + if (Character.isLetter(c) || c == '_') { + int start = pos; + while (pos < length && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) { + pos++; + } + String word = input.substring(start, pos); + if (word.equalsIgnoreCase("true")) return Boolean.TRUE; + if (word.equalsIgnoreCase("false")) return Boolean.FALSE; + if (word.equalsIgnoreCase("null")) return null; + + // Check if it matches variable without dollar + if (vars != null && vars.containsKey(word)) { + return vars.get(word); + } + return word; + } + + // Fallback character + pos++; + return String.valueOf(c); + } + + private void skipWhitespace() { + while (pos < length && Character.isWhitespace(input.charAt(pos))) { + pos++; + } + } + + private boolean match(String prefix) { + if (input.startsWith(prefix, pos)) { + pos += prefix.length(); + return true; + } + return false; + } + + private boolean matchKeyword(String keyword) { + if (pos + keyword.length() <= length) { + String sub = input.substring(pos, pos + keyword.length()); + if (sub.equalsIgnoreCase(keyword)) { + int end = pos + keyword.length(); + if (end == length || !Character.isLetterOrDigit(input.charAt(end))) { + pos = end; + return true; + } + } + } + return false; + } + + private static boolean isDouble(Object obj) { + return obj instanceof Double || obj instanceof Float; + } + + private static int toIntVal(Object obj) { + if (obj instanceof Number) return ((Number) obj).intValue(); + if (obj != null) { + try { + return Integer.parseInt(obj.toString().trim()); + } catch (Exception ignored) {} + } + return 0; + } + + private static double toDoubleVal(Object obj) { + if (obj instanceof Number) return ((Number) obj).doubleValue(); + if (obj != null) { + try { + return Double.parseDouble(obj.toString().trim()); + } catch (Exception ignored) {} + } + return 0.0; + } + + private static boolean toBooleanVal(Object obj) { + if (obj instanceof Boolean) return (Boolean) obj; + if (obj instanceof Number) return ((Number) obj).doubleValue() != 0.0; + if (obj != null) { + String s = obj.toString().trim(); + if (s.equalsIgnoreCase("true") || s.equals("1")) return true; + if (s.equalsIgnoreCase("false") || s.equals("0")) return false; + return !s.isEmpty(); + } + return false; + } + + private static boolean compareEqual(Object a, Object b) { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + + if (a instanceof Number && b instanceof Number) { + return Double.compare(((Number) a).doubleValue(), ((Number) b).doubleValue()) == 0; + } + if (a instanceof Boolean || b instanceof Boolean) { + return toBooleanVal(a) == toBooleanVal(b); + } + return a.toString().equalsIgnoreCase(b.toString()); + } + + private static int compareRelational(Object a, Object b) { + if (a instanceof Number || b instanceof Number) { + return Double.compare(toDoubleVal(a), toDoubleVal(b)); + } + return String.valueOf(a).compareToIgnoreCase(String.valueOf(b)); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroVariables.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroVariables.java new file mode 100644 index 0000000..7364c80 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/macro/variable/MacroVariables.java @@ -0,0 +1,262 @@ +package haus.nightmare.lib3270j.macro.variable; + +import java.io.Serializable; +import java.util.*; + +/** + * Variable management system matching IBM Host On-Demand MacroVariables. + * Manages macro variables ($var, $var$) supporting string, integer, double, + * boolean, and field coordinate types. + */ +public class MacroVariables implements Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + public static class VariableEntry implements Serializable, Cloneable { + private static final long serialVersionUID = 1L; + private final String name; + private final String type; + private Object value; + private final String initialValue; + + public VariableEntry(String name, String type, Object value, String initialValue) { + this.name = name; + this.type = type.toLowerCase(); + this.value = value; + this.initialValue = initialValue; + } + + public String getName() { return name; } + public String getType() { return type; } + public Object getValue() { return value; } + public void setValue(Object value) { this.value = value; } + public String getInitialValue() { return initialValue; } + + @Override + public VariableEntry clone() { + return new VariableEntry(name, type, value, initialValue); + } + } + + private final Map variables = new LinkedHashMap<>(); + private boolean useVars = false; + + public MacroVariables() {} + + public static String canonicalName(String name) { + if (name == null) return ""; + String trimmed = name.trim(); + if (trimmed.isEmpty()) return ""; + if (!trimmed.startsWith("$")) { + trimmed = "$" + trimmed; + } + if (!trimmed.endsWith("$")) { + trimmed = trimmed + "$"; + } + return trimmed; + } + + public static boolean isValidName(String name) { + if (name == null || name.length() < 2) return false; + String s = name.trim(); + if (!s.startsWith("$")) return false; + String inner = s.endsWith("$") && s.length() > 2 ? s.substring(1, s.length() - 1) : s.substring(1); + if (inner.isEmpty()) return false; + for (int i = 0; i < inner.length(); i++) { + char c = inner.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '_' && c != '-') { + return false; + } + } + return true; + } + + public static boolean isValidType(String type) { + if (type == null) return false; + String t = type.trim().toLowerCase(); + return t.equals("string") || t.equals("int") || t.equals("integer") + || t.equals("double") || t.equals("boolean") || t.equals("field"); + } + + public boolean isUseVars() { + return useVars; + } + + public void setUseVars(boolean useVars) { + this.useVars = useVars; + } + + public void createVariable(String name, String type, String initialValue) { + if (!isValidName(name)) { + throw new IllegalArgumentException("Invalid macro variable name: " + name); + } + if (!isValidType(type)) { + throw new IllegalArgumentException("Invalid macro variable type: " + type); + } + + String cName = canonicalName(name); + String t = type.trim().toLowerCase(); + if (t.equals("int")) t = "integer"; + + Object parsedVal = parseValueForType(t, initialValue); + variables.put(cName, new VariableEntry(cName, t, parsedVal, initialValue)); + this.useVars = true; + } + + private Object parseValueForType(String type, String value) { + if (value == null) { + switch (type) { + case "integer": return 0; + case "double": return 0.0; + case "boolean": return Boolean.FALSE; + default: return ""; + } + } + + String v = value.trim(); + // Remove single/double quotes if string + if (type.equals("string")) { + if ((v.startsWith("'") && v.endsWith("'") && v.length() >= 2) || + (v.startsWith("\"") && v.endsWith("\"") && v.length() >= 2)) { + return v.substring(1, v.length() - 1); + } + return v; + } else if (type.equals("integer")) { + if (v.isEmpty()) return 0; + try { + return Integer.parseInt(v); + } catch (NumberFormatException e) { + try { + return (int) Double.parseDouble(v); + } catch (NumberFormatException ex) { + return 0; + } + } + } else if (type.equals("double")) { + if (v.isEmpty()) return 0.0; + try { + return Double.parseDouble(v); + } catch (NumberFormatException e) { + return 0.0; + } + } else if (type.equals("boolean")) { + return Boolean.parseBoolean(v); + } else { + // field coordinate or object + return v; + } + } + + public boolean containsKey(String name) { + return variables.containsKey(canonicalName(name)); + } + + public VariableEntry getEntry(String name) { + return variables.get(canonicalName(name)); + } + + public Object get(String name) { + VariableEntry entry = getEntry(name); + return (entry != null) ? entry.getValue() : null; + } + + public String getString(String name) { + Object val = get(name); + return (val != null) ? val.toString() : ""; + } + + public int getInt(String name) { + Object val = get(name); + if (val instanceof Number) { + return ((Number) val).intValue(); + } else if (val != null) { + try { + return Integer.parseInt(val.toString().trim()); + } catch (Exception e) { + return 0; + } + } + return 0; + } + + public double getDouble(String name) { + Object val = get(name); + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } else if (val != null) { + try { + return Double.parseDouble(val.toString().trim()); + } catch (Exception e) { + return 0.0; + } + } + return 0.0; + } + + public boolean getBoolean(String name) { + Object val = get(name); + if (val instanceof Boolean) { + return (Boolean) val; + } else if (val != null) { + return Boolean.parseBoolean(val.toString().trim()); + } + return false; + } + + public void setVariable(String name, Object value) { + String cName = canonicalName(name); + VariableEntry entry = variables.get(cName); + if (entry != null) { + if (value instanceof String && !entry.getType().equals("string")) { + entry.setValue(parseValueForType(entry.getType(), (String) value)); + } else { + entry.setValue(value); + } + } else { + // Implicit declaration + String inferredType = "string"; + if (value instanceof Integer || value instanceof Long) inferredType = "integer"; + else if (value instanceof Double || value instanceof Float) inferredType = "double"; + else if (value instanceof Boolean) inferredType = "boolean"; + + variables.put(cName, new VariableEntry(cName, inferredType, value, String.valueOf(value))); + } + this.useVars = true; + } + + public void updateVariable(String name, Object value) { + setVariable(name, value); + } + + public void removeVariable(String name) { + variables.remove(canonicalName(name)); + } + + public List getVarNames() { + return new ArrayList<>(variables.keySet()); + } + + public int size() { + return variables.size(); + } + + public void clear() { + variables.clear(); + } + + public void reset() { + for (VariableEntry entry : variables.values()) { + entry.setValue(parseValueForType(entry.getType(), entry.getInitialValue())); + } + } + + @Override + public MacroVariables clone() { + MacroVariables clone = new MacroVariables(); + clone.useVars = this.useVars; + for (Map.Entry e : this.variables.entrySet()) { + clone.variables.put(e.getKey(), e.getValue().clone()); + } + return clone; + } +} 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 231f7fd..4829586 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java @@ -1600,6 +1600,7 @@ public class TelnetFSM { } public boolean isTn3270eNegotiated() { return tn3270eNegotiated; } + public void setTn3270eNegotiated(boolean neg) { this.tn3270eNegotiated = neg; } public String getConnectedLu() { return connectedLu; } public String getConnectedType() { return connectedType; } public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; } @@ -1869,6 +1870,41 @@ public class TelnetFSM { handleSysReq(); } + public void processAttn() { + handleAttn(); + } + + public void handleAttn() { + if (connection == null || !connection.isConnected()) { + return; + } + if (tn3270eNegotiated && (tn3270eBound || connectionState == ConnectionState.CONNECTED_TN3270E)) { + // RFC 2355 10.1 & HoD parity: Send raw Telnet IAC IP + byte[] ip = new byte[] { (byte) IAC, (byte) IP }; + sendBytes(ip); + log.info("SENT Telnet IP (ATTN)"); + } else if (connectionState == ConnectionState.CONNECTED_SSCP) { + // In SSCP-LU mode, host does not receive ATTN; clear pending and unlock + if (dsProcessor != null && dsProcessor.getInputProcessor() != null) { + dsProcessor.getInputProcessor().setKeyboardLocked(false); + dsProcessor.getInputProcessor().reset(); + } + } else if (connectionState == ConnectionState.CONNECTED_3270) { + // RFC 1576 & HoD parity: Send PA1 record followed by Telnet IAC BREAK + byte[] attnBytes = new byte[] { + (byte) DS3270Constants.AID_PA1, + (byte) IAC, (byte) EOR, + (byte) IAC, (byte) BREAK + }; + sendBytes(attnBytes); + log.info("SENT TN3270 ATTN (PA1 + IAC BREAK)"); + } else { + byte[] ip = new byte[] { (byte) IAC, (byte) IP }; + sendBytes(ip); + log.info("SENT Telnet IP"); + } + } + public void handleSysReq() { if (tn3270eNegotiated) { byte[] ao = new byte[] { (byte) IAC, (byte) AO }; diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/macro/Phase24MacroRuntimeTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/macro/Phase24MacroRuntimeTest.java new file mode 100644 index 0000000..2461115 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/macro/Phase24MacroRuntimeTest.java @@ -0,0 +1,359 @@ +package haus.nightmare.lib3270j.macro; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLOIA; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.ecl.ECLScreenDesc; +import haus.nightmare.lib3270j.ecl.ECLSession; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.macro.action.*; +import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser; +import haus.nightmare.lib3270j.macro.variable.MacroVariables; +import haus.nightmare.lib3270j.screen.ScreenBuffer; + +/** + * Test suite for Phase 24: IBM HoD XML Macro Runtime () & Expression Evaluator. + */ +public class Phase24MacroRuntimeTest { + + private ScreenBuffer screen; + private InputProcessor inputProcessor; + private EbcdicTranslator translator; + private ECLPS ps; + private ECLOIA oia; + private ECLSession session; + + @BeforeEach + public void setUp() { + session = new ECLSession(); + ps = session.GetPS(); + oia = session.GetOIA(); + screen = ps.getScreenBuffer(); + translator = ps.getTranslator(); + inputProcessor = ps.getInputProcessor(); + } + + @Test + @DisplayName("24.2: MacroVariables variable creation, types, and canonical resolution") + public void testMacroVariables() { + MacroVariables vars = new MacroVariables(); + vars.createVariable("$username", "string", "IBMUSER"); + vars.createVariable("$counter$", "integer", "10"); + vars.createVariable("$rate", "double", "3.14159"); + vars.createVariable("$flag", "boolean", "true"); + vars.createVariable("$pos", "field", "5,10"); + + assertTrue(vars.isUseVars()); + assertTrue(vars.containsKey("$username")); + assertTrue(vars.containsKey("$username$")); + assertTrue(vars.containsKey("username")); + + assertEquals("IBMUSER", vars.getString("$username")); + assertEquals(10, vars.getInt("$counter")); + assertEquals(3.14159, vars.getDouble("$rate"), 0.0001); + assertTrue(vars.getBoolean("$flag")); + assertEquals("5,10", vars.getString("$pos")); + + // Update variable + vars.setVariable("$counter", 25); + assertEquals(25, vars.getInt("$counter")); + + // Reset restores initial values + vars.reset(); + assertEquals(10, vars.getInt("$counter")); + + // Validation + assertTrue(MacroVariables.isValidName("$myVar")); + assertTrue(MacroVariables.isValidName("$my_var$")); + assertFalse(MacroVariables.isValidName("invalidVar")); + assertTrue(MacroVariables.isValidType("string")); + assertTrue(MacroVariables.isValidType("integer")); + assertFalse(MacroVariables.isValidType("unknown")); + } + + @Test + @DisplayName("24.2: MacroExpressionParser arithmetic, comparisons, logic, and concatenation") + public void testMacroExpressionParser() { + MacroVariables vars = new MacroVariables(); + vars.createVariable("$a", "integer", "15"); + vars.createVariable("$b", "integer", "5"); + vars.createVariable("$name", "string", "World"); + + // Arithmetic + assertEquals(20, MacroExpressionParser.evaluateInt(vars, "$a + $b")); + assertEquals(10, MacroExpressionParser.evaluateInt(vars, "$a - $b")); + assertEquals(75, MacroExpressionParser.evaluateInt(vars, "$a * $b")); + assertEquals(3, MacroExpressionParser.evaluateInt(vars, "$a / $b")); + assertEquals(1, MacroExpressionParser.evaluateInt(vars, "$a % 2")); + assertEquals(35, MacroExpressionParser.evaluateInt(vars, "$a + $b * 4")); // Precedence 15 + (5*4) = 35 + assertEquals(80, MacroExpressionParser.evaluateInt(vars, "($a + $b) * 4")); // Parentheses (15+5)*4 = 80 + + // String Concatenation + assertEquals("Hello World", MacroExpressionParser.evaluateString(vars, "'Hello ' + $name")); + assertEquals("Count: 15", MacroExpressionParser.evaluateString(vars, "'Count: ' + $a")); + + // Comparisons + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a > $b")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a >= 15")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$b < 10")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$b <= 5")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a == 15")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$name == 'world'")); // Case-insensitive per HoD + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a != 99")); + + // Logical Operators + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a > 10 && $b < 10")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a > 20 || $b == 5")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "!($a < 10)")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a == 15 AND $b == 5")); + assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a == 99 OR $b == 5")); + + // resolveText + assertEquals("Logon for World with ID 15", MacroExpressionParser.resolveText(vars, "Logon for $name with ID $a")); + } + + @Test + @DisplayName("24.1: MacroXmlParser parsing AST and roundtrip XML formatting") + public void testMacroXmlParser() { + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + MacroScreens screens = MacroXmlParser.parse(xml); + + assertNotNull(screens); + assertEquals("TestMacro", screens.getName()); + assertEquals("A unit test macro", screens.getDescription()); + assertEquals(8000, screens.getTimeout()); + assertEquals(200, screens.getPauseTime()); + assertTrue(screens.isUseVars()); + + // Check variables + MacroVariables vars = screens.getVariables(); + assertNotNull(vars); + assertEquals("12345", vars.getString("$acct")); + assertEquals(0, vars.getInt("$retry")); + + // Check screens + assertEquals(2, screens.size()); + MacroScreen s1 = screens.getByName("Screen1"); + assertNotNull(s1); + assertTrue(s1.isEntryScreen()); + assertEquals(150, s1.getPauseTime()); + assertEquals(3, s1.getRecoLimit()); + assertEquals("ScreenError", s1.getRecoGoto()); + + // Descriptors + assertEquals(2, s1.getDescription().size()); + + // Actions + assertEquals(2, s1.getActions().size()); + assertTrue(s1.getActions().get(0) instanceof MacroActionInput); + MacroActionInput inputAct = (MacroActionInput) s1.getActions().get(0); + assertEquals("$acct + '[enter]'", inputAct.getValue()); + assertEquals(5, inputAct.getRow()); + assertEquals(10, inputAct.getCol()); + + assertTrue(s1.getActions().get(1) instanceof MacroActionVarUpdate); + MacroActionVarUpdate vuAct = (MacroActionVarUpdate) s1.getActions().get(1); + assertEquals("$retry", vuAct.getName()); + assertEquals("$retry + 1", vuAct.getValue()); + + // Next screens + assertEquals(1, s1.getNextScreens().size()); + assertTrue(s1.getNextScreens().containsName("Screen2")); + assertEquals(5000, s1.getNextScreens().getTimeout()); + + // Screen 2 + MacroScreen s2 = screens.getByName("Screen2"); + assertNotNull(s2); + assertTrue(s2.isExitScreen()); + assertEquals(1, s2.getActions().size()); + assertTrue(s2.getActions().get(0) instanceof MacroActionMessage); + + // Roundtrip serialization + String formatted = screens.format(); + assertTrue(formatted.contains("")); + + MacroScreens reparsed = MacroXmlParser.parse(formatted); + assertEquals("TestMacro", reparsed.getName()); + assertEquals(2, reparsed.size()); + } + + @Test + @DisplayName("24.3: PlayThread finite state machine loop execution") + public void testPlayThreadStateMachineExecution() throws Exception { + // Setup initial screen in PS + ps.SetText("TSO/E LOGON", 1, 35); + ps.SetCursorPos(5, 10); // row 5, col 10 (1-based) + + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + Macro macro = new Macro(session); + macro.load(xml); + + AtomicReference matched = new AtomicReference<>(""); + macro.addMacroListener(new Macro.MacroListener() { + @Override public void macroStateChanged(Macro m, int oldState, int newState) {} + @Override public void macroScreenMatching(MacroScreen screen) { matched.set(screen.getName()); } + @Override public void macroActionExecuting(MacroAction action) {} + @Override public void macroMessage(String title, String message) {} + @Override public void macroError(String error) {} + }); + + // Launch macro in background thread + macro.play(); + + // Wait for Screen1 to match and execute actions + Thread.sleep(200); + assertEquals(1, macro.getVariables().getInt("$step")); + assertEquals("Logon", matched.get()); + + // Now simulate host transition to second screen + ps.SetText("READY TO WORK", 2, 10); + + // Wait for Welcome screen to match and complete + long start = System.currentTimeMillis(); + while (macro.getState() == Macro.STATE_PLAYING && System.currentTimeMillis() - start < 2000) { + Thread.sleep(50); + } + + assertEquals(Macro.STATE_STOPPED, macro.getState()); + assertEquals("Welcome", matched.get()); + assertEquals(2, macro.getVariables().getInt("$step")); + assertEquals("READY TO WORK", macro.getVariables().getString("$extracted").trim()); + } + + @Test + @DisplayName("24.3: MacroActionIf conditional branching during macro execution") + public void testMacroActionIfBranching() { + Macro macro = new Macro(session); + MacroVariables vars = new MacroVariables(); + vars.createVariable("$flag", "boolean", "true"); + vars.createVariable("$result", "string", ""); + macro.getScreens().setVariables(vars); + macro.getScreens().setUseVars(true); + + MacroActionIf ifAct = new MacroActionIf("$flag == true"); + ifAct.addIfAction(new MacroActionVarUpdate("$result", "'THEN_BRANCH'")); + ifAct.addElseAction(new MacroActionVarUpdate("$result", "'ELSE_BRANCH'")); + + ifAct.execute(macro, session); + assertEquals("THEN_BRANCH", vars.getString("$result")); + + // Switch flag and re-execute + vars.setVariable("$flag", false); + ifAct.execute(macro, session); + assertEquals("ELSE_BRANCH", vars.getString("$result")); + } + + @Test + @DisplayName("24.3: MacroActionPrompt modal input provider") + public void testMacroActionPrompt() { + Macro macro = new Macro(session); + MacroVariables vars = new MacroVariables(); + vars.createVariable("$secret", "string", ""); + macro.getScreens().setVariables(vars); + macro.getScreens().setUseVars(true); + + // Set mock prompt handler + macro.setPromptHandler(prompt -> { + assertEquals("Password Prompt", prompt.getTitle()); + return "Pass123"; + }); + + MacroActionPrompt promptAct = new MacroActionPrompt(); + promptAct.setTitle("Password Prompt"); + promptAct.setDescription("Enter Password:"); + promptAct.setEncrypted(true); + promptAct.setVariable("$secret"); + promptAct.setDontSend(true); // Don't write to PS, only store variable + + promptAct.execute(macro, session); + assertEquals("Pass123", vars.getString("$secret")); + } + + @Test + @DisplayName("24.1 & 24.2: com.ibm.eNetwork HOD facades compatibility") + public void testHodFacades() { + com.ibm.eNetwork.beans.HOD.Macro hodMacro = new com.ibm.eNetwork.beans.HOD.Macro(session); + com.ibm.eNetwork.beans.HOD.MacroScreens hodScreens = new com.ibm.eNetwork.beans.HOD.MacroScreens(); + com.ibm.eNetwork.beans.HOD.MacroScreen hodScreen = new com.ibm.eNetwork.beans.HOD.MacroScreen("TestScreen"); + hodScreens.add(hodScreen); + hodMacro.setScreens(hodScreens); + + assertEquals(1, hodMacro.getScreens().size()); + assertEquals("TestScreen", hodMacro.getScreens().get(0).getName()); + + com.ibm.eNetwork.ECL.macrovariable.MacroVariables hodVars = new com.ibm.eNetwork.ECL.macrovariable.MacroVariables(); + hodVars.createVariable("$test", "integer", "42"); + assertEquals(42, hodVars.getInt("$test")); + + int res = com.ibm.eNetwork.ECL.macrovariable.MacroExpressionParser.evaluateInt(hodVars, "$test * 2"); + assertEquals(84, res); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/AttnTransmissionTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/AttnTransmissionTest.java new file mode 100644 index 0000000..325d334 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/AttnTransmissionTest.java @@ -0,0 +1,160 @@ +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.ConnectionState; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.protocol.DS3270Constants; +import haus.nightmare.lib3270j.protocol.TelnetConstants; +import haus.nightmare.lib3270j.screen.ScreenBuffer; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static haus.nightmare.lib3270j.protocol.TelnetConstants.*; + +/** + * Tests verifying proper transmission of the ATTN key in accordance with + * RFC 2355, RFC 1576, and IBM Host On-Demand parity. + */ +public class AttnTransmissionTest { + + private ConnectionConfig config; + private ScreenBuffer screenBuffer; + private DataStreamProcessor dsProcessor; + private InputProcessor inputProcessor; + private TelnetFSM fsm; + private MockConnection connection; + private EbcdicTranslator translator; + + private static class MockConnection extends TelnetConnection { + final List sentData = new ArrayList<>(); + boolean connected = true; + + MockConnection(ConnectionConfig config, TelnetFSM fsm) { + super(config, fsm); + } + + @Override + public boolean isConnected() { + return connected; + } + + @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() { + translator = new EbcdicTranslator(); + config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4); + screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + dsProcessor = new DataStreamProcessor(screenBuffer, translator); + fsm = new TelnetFSM(config, screenBuffer, dsProcessor); + inputProcessor = new InputProcessor(screenBuffer, translator, fsm); + dsProcessor.setInputProcessor(inputProcessor); + connection = new MockConnection(config, fsm); + fsm.setConnection(connection); + } + + @Test + public void testAttnInTn3270eBoundModeSendsRawIacIp() { + fsm.setConnectionState(ConnectionState.CONNECTED_TN3270E); + fsm.setTn3270eNegotiated(true); + connection.sentData.clear(); + + inputProcessor.processAttn(); + + // Must send exactly 1 packet with 2 bytes: IAC IP (0xFF, 0xF4) + // Must NOT escape IAC into IAC IAC (0xFF 0xFF) or add IAC EOR (0xFF 0xEF) + assertEquals(1, connection.sentData.size(), "Should have transmitted exactly one packet"); + byte[] packet = connection.sentData.get(0); + assertEquals(2, packet.length, "ATTN packet in TN3270E bound mode must be exactly 2 bytes (IAC IP)"); + assertArrayEquals(new byte[] { (byte) IAC, (byte) IP }, packet); + } + + @Test + public void testAttnInStandardTn3270ModeSendsPa1AndBreak() { + fsm.setConnectionState(ConnectionState.CONNECTED_3270); + fsm.setTn3270eNegotiated(false); + connection.sentData.clear(); + + inputProcessor.processAttn(); + + // HoD parity: In standard TN3270 mode, sends PA1 record + IAC BREAK: + // { 108, -1, -17, -1, -13 } -> { 0x6C, IAC, EOR, IAC, BREAK } + assertEquals(1, connection.sentData.size(), "Should have transmitted exactly one packet"); + byte[] packet = connection.sentData.get(0); + assertEquals(5, packet.length, "ATTN packet in TN3270 mode must be 5 bytes (PA1 + IAC EOR + IAC BREAK)"); + assertArrayEquals(new byte[] { + (byte) DS3270Constants.AID_PA1, + (byte) IAC, (byte) EOR, + (byte) IAC, (byte) BREAK + }, packet); + } + + @Test + public void testAttnInSscpModeDoesNotTransmitAndResetsState() { + fsm.setConnectionState(ConnectionState.CONNECTED_SSCP); + fsm.setTn3270eNegotiated(true); + inputProcessor.setKeyboardLocked(true); + connection.sentData.clear(); + + inputProcessor.processAttn(); + + // In SSCP-LU mode, host does not receive ATTN; client resets and unlocks + assertTrue(connection.sentData.isEmpty(), "ATTN in SSCP-LU mode must not transmit to host"); + assertFalse(inputProcessor.isKeyboardLocked(), "Keyboard should be unlocked in SSCP-LU mode on ATTN"); + } + + @Test + public void testAttnInNvtModeSendsRawIacIp() { + fsm.setConnectionState(ConnectionState.CONNECTED_NVT); + connection.sentData.clear(); + + inputProcessor.processAttn(); + + assertEquals(1, connection.sentData.size(), "Should transmit one packet in NVT mode"); + byte[] packet = connection.sentData.get(0); + assertArrayEquals(new byte[] { (byte) IAC, (byte) IP }, packet); + } + + @Test + public void testAttnResetsInsertModeIfInsertOffOnAid() { + fsm.setConnectionState(ConnectionState.CONNECTED_TN3270E); + fsm.setTn3270eNegotiated(true); + inputProcessor.setInsertOffOnAid(true); + inputProcessor.setInsertMode(true); + assertTrue(inputProcessor.isInsertMode()); + + inputProcessor.processAttn(); + + assertFalse(inputProcessor.isInsertMode(), "ATTN should reset insert mode when insertOffOnAid is enabled"); + } + + @Test + public void testDirectFsmHandleAttn() { + fsm.setConnectionState(ConnectionState.CONNECTED_TN3270E); + fsm.setTn3270eNegotiated(true); + connection.sentData.clear(); + + fsm.processAttn(); + + assertEquals(1, connection.sentData.size()); + assertArrayEquals(new byte[] { (byte) IAC, (byte) IP }, connection.sentData.get(0)); + } +}