Add bugfix for ATTN
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 35s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s

This commit is contained in:
2026-09-18 17:33:58 +00:00
parent 5049d8df74
commit 8a30efe9c0
35 changed files with 4201 additions and 6 deletions
@@ -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);
}
/**
* Insert text at current cursor position.
*/
public void SetText(String text) {
SetText(text, GetCursorPos());
}
public void setText(String text) {
SetText(text);
}
public synchronized void SetString(String text, int pos) throws com.ibm.eNetwork.ECL.ECLErr {
if (text == null) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"text\"", "null");
@@ -460,6 +460,26 @@ public class ECLScreenDesc implements Serializable, Cloneable {
}
}
@Override
public Object clone() {
try {
ECLScreenDesc copy = (ECLScreenDesc) super.clone();
copy.descriptors.clear();
for (ECLScreenDescriptor d : descriptors) {
copy.descriptors.add((ECLScreenDescriptor) d.clone());
}
return copy;
} catch (CloneNotSupportedException e) {
ECLScreenDesc copy = new ECLScreenDesc();
copy.SetName(this.name);
copy.SetActive(this.active);
for (ECLScreenDescriptor d : descriptors) {
copy.AddDescriptor((ECLScreenDescriptor) d.clone());
}
return copy;
}
}
@Override
public String toString() {
return getXML();
@@ -1528,14 +1528,13 @@ public class InputProcessor {
screen.updateDisplaySnapshot();
}
/** Attention key (sends Telnet Interrupt Process + IAC EOR break signal). */
/** Attention key (sends Telnet Interrupt Process or PA1/BREAK signal). */
public void processAttn() {
if (insertOffOnAid && insertMode) {
setInsertMode(false);
}
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ipEor = new byte[] {
(byte) TelnetConstants.IAC, (byte) TelnetConstants.IP,
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
fsm.sendRecord(ipEor);
fsm.handleAttn();
}
}
@@ -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("&", "&amp;")
.replace("\"", "&quot;")
.replace("<", "&lt;")
.replace(">", "&gt;");
}
@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("&", "&amp;")
.replace("\"", "&quot;")
.replace("<", "&lt;")
.replace(">", "&gt;");
}
@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("&", "&amp;")
.replace("\"", "&quot;")
.replace("<", "&lt;")
.replace(">", "&gt;");
}
@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 &lt;HASCRIPT&gt; 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("&", "&amp;")
.replace("\"", "&quot;")
.replace("<", "&lt;")
.replace(">", "&gt;");
}
@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 &lt;commwait&gt;.
* 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 &lt;extract&gt;.
* 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 &lt;if&gt;.
* 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 &lt;input&gt;.
* 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 &lt;message&gt;.
* 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 &lt;pause&gt;.
* 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 &lt;playmacro&gt;.
* 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 &lt;prompt&gt;.
* 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 &lt;varupdate&gt;.
* 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;
}
}
@@ -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 void setTn3270eNegotiated(boolean neg) { this.tn3270eNegotiated = neg; }
public String getConnectedLu() { return connectedLu; }
public String getConnectedType() { return connectedType; }
public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; }
@@ -1869,6 +1870,41 @@ public class TelnetFSM {
handleSysReq();
}
public void processAttn() {
handleAttn();
}
public void handleAttn() {
if (connection == null || !connection.isConnected()) {
return;
}
if (tn3270eNegotiated && (tn3270eBound || connectionState == ConnectionState.CONNECTED_TN3270E)) {
// RFC 2355 10.1 & HoD parity: Send raw Telnet IAC IP
byte[] ip = new byte[] { (byte) IAC, (byte) IP };
sendBytes(ip);
log.info("SENT Telnet IP (ATTN)");
} else if (connectionState == ConnectionState.CONNECTED_SSCP) {
// In SSCP-LU mode, host does not receive ATTN; clear pending and unlock
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
dsProcessor.getInputProcessor().reset();
}
} else if (connectionState == ConnectionState.CONNECTED_3270) {
// RFC 1576 & HoD parity: Send PA1 record followed by Telnet IAC BREAK
byte[] attnBytes = new byte[] {
(byte) DS3270Constants.AID_PA1,
(byte) IAC, (byte) EOR,
(byte) IAC, (byte) BREAK
};
sendBytes(attnBytes);
log.info("SENT TN3270 ATTN (PA1 + IAC BREAK)");
} else {
byte[] ip = new byte[] { (byte) IAC, (byte) IP };
sendBytes(ip);
log.info("SENT Telnet IP");
}
}
public void handleSysReq() {
if (tn3270eNegotiated) {
byte[] ao = new byte[] { (byte) IAC, (byte) AO };
@@ -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));
}
}