Add bugfix for ATTN
This commit is contained in:
@@ -42,6 +42,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
// Dialog instances
|
// Dialog instances
|
||||||
private FindDialog findDialog;
|
private FindDialog findDialog;
|
||||||
private ScriptDialog scriptDialog;
|
private ScriptDialog scriptDialog;
|
||||||
|
private MacroDialog macroDialog;
|
||||||
private FieldInspectorDialog fieldInspectorDialog;
|
private FieldInspectorDialog fieldInspectorDialog;
|
||||||
private PrinterSessionDialog printerSessionDialog;
|
private PrinterSessionDialog printerSessionDialog;
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
if (findDialog != null && findDialog.isVisible()) return true;
|
if (findDialog != null && findDialog.isVisible()) return true;
|
||||||
if (scriptDialog != null && scriptDialog.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 (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
|
||||||
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
|
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
|
||||||
for (Window owned : getOwnedWindows()) {
|
for (Window owned : getOwnedWindows()) {
|
||||||
@@ -453,6 +455,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}, true));
|
}, true));
|
||||||
actionsMenu.addSeparator();
|
actionsMenu.addSeparator();
|
||||||
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
||||||
|
actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true));
|
||||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
|
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
|
||||||
menuBar.add(actionsMenu);
|
menuBar.add(actionsMenu);
|
||||||
|
|
||||||
@@ -559,6 +562,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
scriptDialog.setVisible(true);
|
scriptDialog.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showMacroDialog() {
|
||||||
|
if (macroDialog == null) {
|
||||||
|
macroDialog = new MacroDialog(this, client, terminalPanel);
|
||||||
|
}
|
||||||
|
macroDialog.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
private void showFieldInspectorDialog() {
|
private void showFieldInspectorDialog() {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
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.cancel();
|
||||||
fileTransfer = null;
|
fileTransfer = null;
|
||||||
}
|
}
|
||||||
|
if (macroDialog != null) {
|
||||||
|
macroDialog.stopMacro();
|
||||||
|
}
|
||||||
Telnet3270Client c = client;
|
Telnet3270Client c = client;
|
||||||
client = null;
|
client = null;
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
|
|||||||
@@ -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 (<HASCRIPT> / .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 (<HASCRIPT>)", 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("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code><HASCRIPT></code>).<br>"
|
||||||
|
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
|
||||||
|
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 (<HASCRIPT>)"));
|
||||||
|
|
||||||
|
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<String> 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 "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
|
||||||
|
+ " <vars>\n"
|
||||||
|
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
|
||||||
|
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
|
||||||
|
+ " </vars>\n\n"
|
||||||
|
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
|
||||||
|
+ " <description>\n"
|
||||||
|
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||||
|
+ " </description>\n"
|
||||||
|
+ " <actions>\n"
|
||||||
|
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
|
||||||
|
+ " </actions>\n"
|
||||||
|
+ " <nextscreens timeout=\"10000\">\n"
|
||||||
|
+ " <nextscreen name=\"ReadyScreen\" />\n"
|
||||||
|
+ " </nextscreens>\n"
|
||||||
|
+ " </screen>\n\n"
|
||||||
|
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
|
||||||
|
+ " <description>\n"
|
||||||
|
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||||
|
+ " </description>\n"
|
||||||
|
+ " <actions>\n"
|
||||||
|
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
|
||||||
|
+ " </actions>\n"
|
||||||
|
+ " </screen>\n"
|
||||||
|
+ "</HASCRIPT>";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -448,6 +448,16 @@ public class ECLPS implements ECLConstants {
|
|||||||
setText(text, p);
|
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 {
|
public synchronized void SetString(String text, int pos) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"text\"", "null");
|
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"text\"", "null");
|
||||||
|
|||||||
@@ -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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getXML();
|
return getXML();
|
||||||
|
|||||||
@@ -1528,14 +1528,13 @@ public class InputProcessor {
|
|||||||
screen.updateDisplaySnapshot();
|
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() {
|
public void processAttn() {
|
||||||
|
if (insertOffOnAid && insertMode) {
|
||||||
|
setInsertMode(false);
|
||||||
|
}
|
||||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
|
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
|
||||||
byte[] ipEor = new byte[] {
|
fsm.handleAttn();
|
||||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.IP,
|
|
||||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
|
|
||||||
};
|
|
||||||
fsm.sendRecord(ipEor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<MacroListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
private final Map<String, Macro> 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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MacroAction> actions = new ArrayList<>();
|
||||||
|
private boolean promptAll = false;
|
||||||
|
|
||||||
|
public MacroActions() {}
|
||||||
|
|
||||||
|
public MacroActions(List<MacroAction> 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<MacroAction> 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("<actions");
|
||||||
|
if (promptAll) sb.append(" promptall=\"true\"");
|
||||||
|
sb.append(">\n");
|
||||||
|
|
||||||
|
for (MacroAction act : actions) {
|
||||||
|
sb.append(act.format(indent + 1)).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(ind).append("</actions>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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<String> 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("<nextscreens");
|
||||||
|
if (timeout > 0) sb.append(" timeout=\"").append(timeout).append("\"");
|
||||||
|
sb.append(">\n");
|
||||||
|
|
||||||
|
for (String name : nextScreens) {
|
||||||
|
sb.append(ind).append(" <nextscreen name=\"").append(escapeXml(name)).append("\" />\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(ind).append("</nextscreens>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<screen name=\"").append(escapeXml(name)).append("\"");
|
||||||
|
if (entryScreen) sb.append(" entryscreen=\"true\"");
|
||||||
|
if (exitScreen) sb.append(" exitscreen=\"true\"");
|
||||||
|
if (transientScreen) sb.append(" transient=\"true\"");
|
||||||
|
if (pauseTime >= 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(" <recolimit value=\"").append(recoLimit).append("\"");
|
||||||
|
if (recoGoto != null && !recoGoto.isEmpty()) {
|
||||||
|
sb.append(" goto=\"").append(escapeXml(recoGoto)).append("\"");
|
||||||
|
}
|
||||||
|
sb.append(" />\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(ind).append("</screen>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MacroScreen> 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<MacroScreen> 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("<HASCRIPT");
|
||||||
|
if (!name.isEmpty()) sb.append(" name=\"").append(escapeXml(name)).append("\"");
|
||||||
|
if (!description.isEmpty()) sb.append(" description=\"").append(escapeXml(description)).append("\"");
|
||||||
|
if (!author.isEmpty()) sb.append(" author=\"").append(escapeXml(author)).append("\"");
|
||||||
|
if (!creationDate.isEmpty()) sb.append(" creationdate=\"").append(escapeXml(creationDate)).append("\"");
|
||||||
|
if (timeout > 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(" <vars>\n");
|
||||||
|
for (String varName : variables.getVarNames()) {
|
||||||
|
MacroVariables.VariableEntry entry = variables.getEntry(varName);
|
||||||
|
if (entry != null) {
|
||||||
|
sb.append(" <create name=\"").append(escapeXml(entry.getName()))
|
||||||
|
.append("\" type=\"").append(escapeXml(entry.getType()))
|
||||||
|
.append("\" value=\"").append(escapeXml(String.valueOf(entry.getValue())))
|
||||||
|
.append("\" />\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(" </vars>\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screens
|
||||||
|
for (MacroScreen screen : screens) {
|
||||||
|
sb.append(screen.format(1)).append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("</HASCRIPT>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <HASCRIPT>, 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 <HASCRIPT> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MacroScreen> 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<MacroScreen> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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("<commwait value=\"").append(escapeXml(value)).append("\"");
|
||||||
|
if (timeout > 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<extract");
|
||||||
|
if (!name.isEmpty()) sb.append(" name=\"").append(escapeXml(name)).append("\"");
|
||||||
|
sb.append(" row=\"").append(row).append("\"");
|
||||||
|
sb.append(" col=\"").append(col).append("\"");
|
||||||
|
if (erow > 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MacroAction> ifActions = new ArrayList<>();
|
||||||
|
private final List<MacroAction> 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<MacroAction> getIfActions() { return ifActions; }
|
||||||
|
public void addIfAction(MacroAction action) { if (action != null) ifActions.add(action); }
|
||||||
|
|
||||||
|
public List<MacroAction> getElseActions() { return elseActions; }
|
||||||
|
public void addElseAction(MacroAction action) { if (action != null) elseActions.add(action); }
|
||||||
|
|
||||||
|
public List<MacroAction> 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<MacroAction> 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("<if condition=\"").append(escapeXml(condition)).append("\">\n");
|
||||||
|
for (MacroAction act : ifActions) {
|
||||||
|
sb.append(act.format(indent + 1)).append("\n");
|
||||||
|
}
|
||||||
|
if (!elseActions.isEmpty()) {
|
||||||
|
sb.append(ind).append(" <else>\n");
|
||||||
|
for (MacroAction act : elseActions) {
|
||||||
|
sb.append(act.format(indent + 2)).append("\n");
|
||||||
|
}
|
||||||
|
sb.append(ind).append(" </else>\n");
|
||||||
|
}
|
||||||
|
sb.append(ind).append("</if>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<input value=\"").append(escapeXml(value)).append("\"");
|
||||||
|
if (row > 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<message title=\"").append(escapeXml(title))
|
||||||
|
.append("\" value=\"").append(escapeXml(value)).append("\" />");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MacroActionMessage clone() {
|
||||||
|
MacroActionMessage c = new MacroActionMessage(title, value);
|
||||||
|
c.setLineNum(mLineNum);
|
||||||
|
c.setComments(comments);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<pause value=\"")
|
||||||
|
.append(timeoutExpr != null ? escapeXml(timeoutExpr) : String.valueOf(timeout))
|
||||||
|
.append("\" />");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MacroActionPause clone() {
|
||||||
|
MacroActionPause c = new MacroActionPause(timeout);
|
||||||
|
c.setTimeoutExpr(timeoutExpr);
|
||||||
|
c.setLineNum(mLineNum);
|
||||||
|
c.setComments(comments);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<playmacro name=\"").append(escapeXml(name)).append("\"");
|
||||||
|
if (startScreen != null && !startScreen.equals("*DEFAULT*")) {
|
||||||
|
sb.append(" startscreen=\"").append(escapeXml(startScreen)).append("\"");
|
||||||
|
}
|
||||||
|
sb.append(" />");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MacroActionPlayMacro clone() {
|
||||||
|
MacroActionPlayMacro c = new MacroActionPlayMacro(name, startScreen);
|
||||||
|
c.setLineNum(mLineNum);
|
||||||
|
c.setComments(comments);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<prompt");
|
||||||
|
if (!name.isEmpty()) sb.append(" name=\"").append(escapeXml(name)).append("\"");
|
||||||
|
if (!description.isEmpty()) sb.append(" description=\"").append(escapeXml(description)).append("\"");
|
||||||
|
if (!title.isEmpty() && !title.equals("Macro Prompt")) sb.append(" title=\"").append(escapeXml(title)).append("\"");
|
||||||
|
if (!defaultValue.isEmpty()) sb.append(" default=\"").append(escapeXml(defaultValue)).append("\"");
|
||||||
|
if (row > 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<varupdate name=\"").append(escapeXml(name))
|
||||||
|
.append("\" value=\"").append(escapeXml(value)).append("\" />");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MacroActionVarUpdate clone() {
|
||||||
|
MacroActionVarUpdate c = new MacroActionVarUpdate(name, value);
|
||||||
|
c.setLineNum(mLineNum);
|
||||||
|
c.setComments(comments);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
+490
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, VariableEntry> 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<String> 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<String, VariableEntry> e : this.variables.entrySet()) {
|
||||||
|
clone.variables.put(e.getKey(), e.getValue().clone());
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1600,6 +1600,7 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
|
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
|
||||||
|
public void setTn3270eNegotiated(boolean neg) { this.tn3270eNegotiated = neg; }
|
||||||
public String getConnectedLu() { return connectedLu; }
|
public String getConnectedLu() { return connectedLu; }
|
||||||
public String getConnectedType() { return connectedType; }
|
public String getConnectedType() { return connectedType; }
|
||||||
public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; }
|
public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; }
|
||||||
@@ -1869,6 +1870,41 @@ public class TelnetFSM {
|
|||||||
handleSysReq();
|
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() {
|
public void handleSysReq() {
|
||||||
if (tn3270eNegotiated) {
|
if (tn3270eNegotiated) {
|
||||||
byte[] ao = new byte[] { (byte) IAC, (byte) AO };
|
byte[] ao = new byte[] { (byte) IAC, (byte) AO };
|
||||||
|
|||||||
@@ -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 (<HASCRIPT>) & 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 <HASCRIPT> AST and roundtrip XML formatting")
|
||||||
|
public void testMacroXmlParser() {
|
||||||
|
String xml = "<HASCRIPT name=\"TestMacro\" description=\"A unit test macro\" timeout=\"8000\" pausetime=\"200\" usevars=\"true\">\n"
|
||||||
|
+ " <vars>\n"
|
||||||
|
+ " <create name=\"$acct\" type=\"string\" value=\"12345\" />\n"
|
||||||
|
+ " <create name=\"$retry\" type=\"integer\" value=\"0\" />\n"
|
||||||
|
+ " </vars>\n"
|
||||||
|
+ " <screen name=\"Screen1\" entryscreen=\"true\" pausetime=\"150\">\n"
|
||||||
|
+ " <description>\n"
|
||||||
|
+ " <string value=\"TSO/E LOGON\" row=\"1\" col=\"35\" />\n"
|
||||||
|
+ " <cursor row=\"5\" col=\"10\" />\n"
|
||||||
|
+ " </description>\n"
|
||||||
|
+ " <actions>\n"
|
||||||
|
+ " <input value=\"$acct + '[enter]'\" row=\"5\" col=\"10\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
|
||||||
|
+ " <varupdate name=\"$retry\" value=\"$retry + 1\" />\n"
|
||||||
|
+ " </actions>\n"
|
||||||
|
+ " <nextscreens timeout=\"5000\">\n"
|
||||||
|
+ " <nextscreen name=\"Screen2\" />\n"
|
||||||
|
+ " </nextscreens>\n"
|
||||||
|
+ " <recolimit value=\"3\" goto=\"ScreenError\" />\n"
|
||||||
|
+ " </screen>\n"
|
||||||
|
+ " <screen name=\"Screen2\" exitscreen=\"true\">\n"
|
||||||
|
+ " <description>\n"
|
||||||
|
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||||
|
+ " </description>\n"
|
||||||
|
+ " <actions>\n"
|
||||||
|
+ " <message title=\"Done\" value=\"Success!\" />\n"
|
||||||
|
+ " </actions>\n"
|
||||||
|
+ " </screen>\n"
|
||||||
|
+ "</HASCRIPT>";
|
||||||
|
|
||||||
|
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("<HASCRIPT"));
|
||||||
|
assertTrue(formatted.contains("name=\"TestMacro\""));
|
||||||
|
assertTrue(formatted.contains("<screen name=\"Screen1\""));
|
||||||
|
assertTrue(formatted.contains("<actions>"));
|
||||||
|
|
||||||
|
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 = "<HASCRIPT name=\"FlowTest\" timeout=\"5000\" pausetime=\"50\" usevars=\"true\">\n"
|
||||||
|
+ " <vars>\n"
|
||||||
|
+ " <create name=\"$step\" type=\"integer\" value=\"0\" />\n"
|
||||||
|
+ " <create name=\"$extracted\" type=\"string\" value=\"\" />\n"
|
||||||
|
+ " </vars>\n"
|
||||||
|
+ " <screen name=\"Logon\" entryscreen=\"true\">\n"
|
||||||
|
+ " <description>\n"
|
||||||
|
+ " <string value=\"TSO/E LOGON\" row=\"1\" col=\"35\" />\n"
|
||||||
|
+ " </description>\n"
|
||||||
|
+ " <actions>\n"
|
||||||
|
+ " <varupdate name=\"$step\" value=\"1\" />\n"
|
||||||
|
+ " <input value=\"USER01[enter]\" />\n"
|
||||||
|
+ " </actions>\n"
|
||||||
|
+ " <nextscreens timeout=\"3000\">\n"
|
||||||
|
+ " <nextscreen name=\"Welcome\" />\n"
|
||||||
|
+ " </nextscreens>\n"
|
||||||
|
+ " </screen>\n"
|
||||||
|
+ " <screen name=\"Welcome\" exitscreen=\"true\">\n"
|
||||||
|
+ " <description>\n"
|
||||||
|
+ " <string value=\"READY TO WORK\" row=\"2\" col=\"10\" />\n"
|
||||||
|
+ " </description>\n"
|
||||||
|
+ " <actions>\n"
|
||||||
|
+ " <extract row=\"2\" col=\"10\" erow=\"2\" ecol=\"22\" variable=\"$extracted\" />\n"
|
||||||
|
+ " <varupdate name=\"$step\" value=\"2\" />\n"
|
||||||
|
+ " </actions>\n"
|
||||||
|
+ " </screen>\n"
|
||||||
|
+ "</HASCRIPT>";
|
||||||
|
|
||||||
|
Macro macro = new Macro(session);
|
||||||
|
macro.load(xml);
|
||||||
|
|
||||||
|
AtomicReference<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<byte[]> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user