Add bugfix for ATTN
This commit is contained in:
@@ -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("&", "&")
|
||||
.replace("\"", """)
|
||||
.replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroNextScreens clone() {
|
||||
MacroNextScreens clone = new MacroNextScreens(timeout);
|
||||
for (String s : nextScreens) {
|
||||
clone.addNextScreen(s);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package haus.nightmare.lib3270j.macro;
|
||||
|
||||
import java.io.Serializable;
|
||||
import haus.nightmare.lib3270j.ecl.ECLScreenDesc;
|
||||
|
||||
/**
|
||||
* Screen state machine representation matching IBM Host On-Demand MacroScreen.
|
||||
*/
|
||||
public class MacroScreen implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String name = "";
|
||||
private boolean entryScreen = false;
|
||||
private boolean exitScreen = false;
|
||||
private boolean transientScreen = false;
|
||||
private int pauseTime = -1;
|
||||
|
||||
private ECLScreenDesc description = new ECLScreenDesc();
|
||||
private MacroActions actions = new MacroActions();
|
||||
private MacroNextScreens nextScreens = new MacroNextScreens();
|
||||
|
||||
private int recoLimit = 0;
|
||||
private String recoGoto = "";
|
||||
private int recoCount = 0;
|
||||
|
||||
public MacroScreen() {}
|
||||
|
||||
public MacroScreen(String name) {
|
||||
this.name = (name != null) ? name : "";
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) {
|
||||
this.name = (name != null) ? name : "";
|
||||
if (description != null) {
|
||||
description.SetName(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEntryScreen() { return entryScreen; }
|
||||
public void setEntryScreen(boolean entryScreen) { this.entryScreen = entryScreen; }
|
||||
public boolean isStartScreen() { return isEntryScreen(); }
|
||||
public void setStartScreen(boolean startScreen) { setEntryScreen(startScreen); }
|
||||
|
||||
public boolean isExitScreen() { return exitScreen; }
|
||||
public void setExitScreen(boolean exitScreen) { this.exitScreen = exitScreen; }
|
||||
public boolean isStopScreen() { return isExitScreen(); }
|
||||
public void setStopScreen(boolean stopScreen) { setExitScreen(stopScreen); }
|
||||
|
||||
public boolean isTransientScreen() { return transientScreen; }
|
||||
public void setTransientScreen(boolean transientScreen) { this.transientScreen = transientScreen; }
|
||||
public boolean isTransient() { return isTransientScreen(); }
|
||||
public void setTransient(boolean transientScreen) { setTransientScreen(transientScreen); }
|
||||
|
||||
public int getPauseTime() { return pauseTime; }
|
||||
public void setPauseTime(int pauseTime) { this.pauseTime = pauseTime; }
|
||||
|
||||
public ECLScreenDesc getDescription() { return description; }
|
||||
public void setDescription(ECLScreenDesc description) {
|
||||
this.description = (description != null) ? description : new ECLScreenDesc();
|
||||
if (this.description != null && !this.name.isEmpty()) {
|
||||
this.description.SetName(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
public MacroActions getActions() { return actions; }
|
||||
public void setActions(MacroActions actions) {
|
||||
this.actions = (actions != null) ? actions : new MacroActions();
|
||||
}
|
||||
|
||||
public MacroNextScreens getNextScreens() { return nextScreens; }
|
||||
public void setNextScreens(MacroNextScreens nextScreens) {
|
||||
this.nextScreens = (nextScreens != null) ? nextScreens : new MacroNextScreens();
|
||||
}
|
||||
|
||||
public int getRecoLimit() { return recoLimit; }
|
||||
public void setRecoLimit(int recoLimit) { this.recoLimit = recoLimit; }
|
||||
|
||||
public String getRecoGoto() { return recoGoto; }
|
||||
public void setRecoGoto(String recoGoto) { this.recoGoto = (recoGoto != null) ? recoGoto : ""; }
|
||||
|
||||
public int getRecoCount() { return recoCount; }
|
||||
public void setRecoCount(int recoCount) { this.recoCount = recoCount; }
|
||||
public void incrementRecoCount() { this.recoCount++; }
|
||||
public void resetRecoCount() { this.recoCount = 0; }
|
||||
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String ind = "";
|
||||
for (int i = 0; i < indent; i++) ind += " ";
|
||||
|
||||
sb.append(ind).append("<screen name=\"").append(escapeXml(name)).append("\"");
|
||||
if (entryScreen) sb.append(" entryscreen=\"true\"");
|
||||
if (exitScreen) sb.append(" exitscreen=\"true\"");
|
||||
if (transientScreen) sb.append(" transient=\"true\"");
|
||||
if (pauseTime >= 0) sb.append(" pausetime=\"").append(pauseTime).append("\"");
|
||||
sb.append(">\n");
|
||||
|
||||
if (description != null) {
|
||||
String descXml = description.getXML();
|
||||
for (String line : descXml.split("\n")) {
|
||||
sb.append(ind).append(" ").append(line).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (actions != null) {
|
||||
sb.append(actions.format(indent + 1)).append("\n");
|
||||
}
|
||||
|
||||
if (nextScreens != null && !nextScreens.isEmpty()) {
|
||||
sb.append(nextScreens.format(indent + 1)).append("\n");
|
||||
}
|
||||
|
||||
if (recoLimit > 0 || (recoGoto != null && !recoGoto.isEmpty())) {
|
||||
sb.append(ind).append(" <recolimit value=\"").append(recoLimit).append("\"");
|
||||
if (recoGoto != null && !recoGoto.isEmpty()) {
|
||||
sb.append(" goto=\"").append(escapeXml(recoGoto)).append("\"");
|
||||
}
|
||||
sb.append(" />\n");
|
||||
}
|
||||
|
||||
sb.append(ind).append("</screen>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String escapeXml(String text) {
|
||||
if (text == null) return "";
|
||||
return text.replace("&", "&")
|
||||
.replace("\"", """)
|
||||
.replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroScreen clone() {
|
||||
MacroScreen clone = new MacroScreen(name);
|
||||
clone.setEntryScreen(entryScreen);
|
||||
clone.setExitScreen(exitScreen);
|
||||
clone.setTransientScreen(transientScreen);
|
||||
clone.setPauseTime(pauseTime);
|
||||
clone.setDescription((ECLScreenDesc) description.clone());
|
||||
clone.setActions(actions.clone());
|
||||
clone.setNextScreens(nextScreens.clone());
|
||||
clone.setRecoLimit(recoLimit);
|
||||
clone.setRecoGoto(recoGoto);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package haus.nightmare.lib3270j.macro;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroVariables;
|
||||
|
||||
/**
|
||||
* Collection of screens and root HASCRIPT configuration matching IBM HoD MacroScreens.
|
||||
*/
|
||||
public class MacroScreens implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final List<MacroScreen> screens = new ArrayList<>();
|
||||
private MacroVariables variables = new MacroVariables();
|
||||
|
||||
private String name = "";
|
||||
private String description = "";
|
||||
private String author = "";
|
||||
private String creationDate = "";
|
||||
private int timeout = 10000;
|
||||
private int pauseTime = 300;
|
||||
private boolean promptAll = false;
|
||||
private boolean blockInput = true;
|
||||
private boolean useVars = false;
|
||||
|
||||
public MacroScreens() {}
|
||||
|
||||
public void add(MacroScreen screen) {
|
||||
if (screen != null) {
|
||||
screens.add(screen);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(int index, MacroScreen screen) {
|
||||
if (screen != null) {
|
||||
screens.add(index, screen);
|
||||
}
|
||||
}
|
||||
|
||||
public MacroScreen get(int index) {
|
||||
return screens.get(index);
|
||||
}
|
||||
|
||||
public MacroScreen getByName(String name) {
|
||||
if (name == null) return null;
|
||||
for (MacroScreen s : screens) {
|
||||
if (name.equalsIgnoreCase(s.getName())) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean contains(String name) {
|
||||
return getByName(name) != null;
|
||||
}
|
||||
|
||||
public MacroScreen getStartScreen() {
|
||||
for (MacroScreen s : screens) {
|
||||
if (s.isEntryScreen()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return (!screens.isEmpty()) ? screens.get(0) : null;
|
||||
}
|
||||
|
||||
public MacroScreen getEntryScreen() {
|
||||
return getStartScreen();
|
||||
}
|
||||
|
||||
public List<MacroScreen> getScreens() {
|
||||
return Collections.unmodifiableList(screens);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return screens.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return screens.isEmpty();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
screens.clear();
|
||||
}
|
||||
|
||||
public MacroVariables getVariables() {
|
||||
return variables;
|
||||
}
|
||||
|
||||
public void setVariables(MacroVariables variables) {
|
||||
this.variables = (variables != null) ? variables : new MacroVariables();
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = (name != null) ? name : ""; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = (description != null) ? description : ""; }
|
||||
|
||||
public String getAuthor() { return author; }
|
||||
public void setAuthor(String author) { this.author = (author != null) ? author : ""; }
|
||||
|
||||
public String getCreationDate() { return creationDate; }
|
||||
public void setCreationDate(String creationDate) { this.creationDate = (creationDate != null) ? creationDate : ""; }
|
||||
|
||||
public int getTimeout() { return timeout; }
|
||||
public void setTimeout(int timeout) { this.timeout = timeout; }
|
||||
|
||||
public int getPauseTime() { return pauseTime; }
|
||||
public void setPauseTime(int pauseTime) { this.pauseTime = pauseTime; }
|
||||
|
||||
public boolean isPromptAll() { return promptAll; }
|
||||
public void setPromptAll(boolean promptAll) { this.promptAll = promptAll; }
|
||||
|
||||
public boolean isBlockInput() { return blockInput; }
|
||||
public void setBlockInput(boolean blockInput) { this.blockInput = blockInput; }
|
||||
|
||||
public boolean isUseVars() { return useVars; }
|
||||
public void setUseVars(boolean useVars) {
|
||||
this.useVars = useVars;
|
||||
if (variables != null) {
|
||||
variables.setUseVars(useVars);
|
||||
}
|
||||
}
|
||||
|
||||
public String format() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<HASCRIPT");
|
||||
if (!name.isEmpty()) sb.append(" name=\"").append(escapeXml(name)).append("\"");
|
||||
if (!description.isEmpty()) sb.append(" description=\"").append(escapeXml(description)).append("\"");
|
||||
if (!author.isEmpty()) sb.append(" author=\"").append(escapeXml(author)).append("\"");
|
||||
if (!creationDate.isEmpty()) sb.append(" creationdate=\"").append(escapeXml(creationDate)).append("\"");
|
||||
if (timeout > 0) sb.append(" timeout=\"").append(timeout).append("\"");
|
||||
if (pauseTime >= 0) sb.append(" pausetime=\"").append(pauseTime).append("\"");
|
||||
if (promptAll) sb.append(" promptall=\"true\"");
|
||||
if (!blockInput) sb.append(" blockinput=\"false\"");
|
||||
if (useVars) sb.append(" usevars=\"true\"");
|
||||
sb.append(">\n\n");
|
||||
|
||||
// Vars section
|
||||
if (variables != null && variables.size() > 0) {
|
||||
sb.append(" <vars>\n");
|
||||
for (String varName : variables.getVarNames()) {
|
||||
MacroVariables.VariableEntry entry = variables.getEntry(varName);
|
||||
if (entry != null) {
|
||||
sb.append(" <create name=\"").append(escapeXml(entry.getName()))
|
||||
.append("\" type=\"").append(escapeXml(entry.getType()))
|
||||
.append("\" value=\"").append(escapeXml(String.valueOf(entry.getValue())))
|
||||
.append("\" />\n");
|
||||
}
|
||||
}
|
||||
sb.append(" </vars>\n\n");
|
||||
}
|
||||
|
||||
// Screens
|
||||
for (MacroScreen screen : screens) {
|
||||
sb.append(screen.format(1)).append("\n\n");
|
||||
}
|
||||
|
||||
sb.append("</HASCRIPT>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String getXML() {
|
||||
return format();
|
||||
}
|
||||
|
||||
private static String escapeXml(String text) {
|
||||
if (text == null) return "";
|
||||
return text.replace("&", "&")
|
||||
.replace("\"", """)
|
||||
.replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroScreens clone() {
|
||||
MacroScreens clone = new MacroScreens();
|
||||
clone.setName(name);
|
||||
clone.setDescription(description);
|
||||
clone.setAuthor(author);
|
||||
clone.setCreationDate(creationDate);
|
||||
clone.setTimeout(timeout);
|
||||
clone.setPauseTime(pauseTime);
|
||||
clone.setPromptAll(promptAll);
|
||||
clone.setBlockInput(blockInput);
|
||||
clone.setUseVars(useVars);
|
||||
clone.setVariables(variables.clone());
|
||||
for (MacroScreen s : screens) {
|
||||
clone.add(s.clone());
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
package haus.nightmare.lib3270j.macro;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import org.w3c.dom.*;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLScreenDesc;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.*;
|
||||
import haus.nightmare.lib3270j.macro.action.*;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroVariables;
|
||||
|
||||
/**
|
||||
* Standard IBM Host On-Demand XML Macro Parser matching MacroXmlParser.
|
||||
* Parses <HASCRIPT> XML documents into MacroScreens AST and provides serialization.
|
||||
*/
|
||||
public class MacroXmlParser {
|
||||
|
||||
public static MacroScreens parse(String xml) {
|
||||
if (xml == null || xml.trim().isEmpty()) {
|
||||
return new MacroScreens();
|
||||
}
|
||||
return parse(new InputSource(new StringReader(xml.trim())));
|
||||
}
|
||||
|
||||
public static MacroScreens parse(InputStream is) {
|
||||
return parse(new InputSource(is));
|
||||
}
|
||||
|
||||
public static MacroScreens parse(Reader reader) {
|
||||
return parse(new InputSource(reader));
|
||||
}
|
||||
|
||||
public static MacroScreens parse(InputSource inputSource) {
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
try {
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(inputSource);
|
||||
Element root = doc.getDocumentElement();
|
||||
|
||||
if (!root.getTagName().equalsIgnoreCase("HASCRIPT")) {
|
||||
throw new IllegalArgumentException("Root element must be <HASCRIPT>, found: <" + root.getTagName() + ">");
|
||||
}
|
||||
|
||||
MacroScreens macroScreens = new MacroScreens();
|
||||
macroScreens.setName(getAttr(root, "name", ""));
|
||||
macroScreens.setDescription(getAttr(root, "description", ""));
|
||||
macroScreens.setAuthor(getAttr(root, "author", ""));
|
||||
macroScreens.setCreationDate(getAttr(root, "creationdate", ""));
|
||||
macroScreens.setTimeout(getIntAttr(root, "timeout", 10000));
|
||||
macroScreens.setPauseTime(getIntAttr(root, "pausetime", 300));
|
||||
macroScreens.setPromptAll(getBooleanAttr(root, "promptall", false));
|
||||
macroScreens.setBlockInput(getBooleanAttr(root, "blockinput", true));
|
||||
macroScreens.setUseVars(getBooleanAttr(root, "usevars", false));
|
||||
|
||||
MacroVariables vars = macroScreens.getVariables();
|
||||
|
||||
NodeList children = root.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node node = children.item(i);
|
||||
if (node.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
Element elem = (Element) node;
|
||||
String tag = elem.getTagName().toLowerCase();
|
||||
|
||||
if (tag.equals("vars")) {
|
||||
parseVars(elem, vars);
|
||||
} else if (tag.equals("screen")) {
|
||||
MacroScreen screen = parseScreen(elem, vars);
|
||||
macroScreens.add(screen);
|
||||
}
|
||||
}
|
||||
|
||||
return macroScreens;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse <HASCRIPT> macro XML: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseVars(Element varsElem, MacroVariables vars) {
|
||||
NodeList list = varsElem.getChildNodes();
|
||||
for (int i = 0; i < list.getLength(); i++) {
|
||||
Node n = list.item(i);
|
||||
if (n.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
Element el = (Element) n;
|
||||
if (el.getTagName().equalsIgnoreCase("create")) {
|
||||
String name = getAttr(el, "name", "");
|
||||
String type = getAttr(el, "type", "string");
|
||||
String value = getAttr(el, "value", "");
|
||||
if (!name.isEmpty()) {
|
||||
vars.createVariable(name, type, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MacroScreen parseScreen(Element screenElem, MacroVariables vars) {
|
||||
String screenName = getAttr(screenElem, "name", "");
|
||||
MacroScreen screen = new MacroScreen(screenName);
|
||||
|
||||
boolean entry = getBooleanAttr(screenElem, "entryscreen", false)
|
||||
|| getBooleanAttr(screenElem, "startscreen", false);
|
||||
boolean exit = getBooleanAttr(screenElem, "exitscreen", false)
|
||||
|| getBooleanAttr(screenElem, "stopscreen", false);
|
||||
boolean trans = getBooleanAttr(screenElem, "transient", false);
|
||||
int pause = getIntAttr(screenElem, "pausetime", -1);
|
||||
|
||||
screen.setEntryScreen(entry);
|
||||
screen.setExitScreen(exit);
|
||||
screen.setTransientScreen(trans);
|
||||
screen.setPauseTime(pause);
|
||||
|
||||
NodeList children = screenElem.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node node = children.item(i);
|
||||
if (node.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
Element elem = (Element) node;
|
||||
String tag = elem.getTagName().toLowerCase();
|
||||
|
||||
if (tag.equals("description")) {
|
||||
ECLScreenDesc desc = parseDescription(elem);
|
||||
desc.SetName(screenName);
|
||||
screen.setDescription(desc);
|
||||
} else if (tag.equals("actions")) {
|
||||
MacroActions actions = parseActions(elem);
|
||||
screen.setActions(actions);
|
||||
} else if (tag.equals("nextscreens")) {
|
||||
MacroNextScreens next = parseNextScreens(elem);
|
||||
screen.setNextScreens(next);
|
||||
} else if (tag.equals("recolimit")) {
|
||||
screen.setRecoLimit(getIntAttr(elem, "value", 0));
|
||||
screen.setRecoGoto(getAttr(elem, "goto", ""));
|
||||
}
|
||||
}
|
||||
|
||||
return screen;
|
||||
}
|
||||
|
||||
private static ECLScreenDesc parseDescription(Element descElem) {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
NodeList children = descElem.getChildNodes();
|
||||
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node n = children.item(i);
|
||||
if (n.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
Element elem = (Element) n;
|
||||
String tag = elem.getTagName().toLowerCase();
|
||||
|
||||
boolean optional = getBooleanAttr(elem, "optional", false);
|
||||
boolean invertmatch = getBooleanAttr(elem, "invertmatch", false);
|
||||
|
||||
if (tag.equals("string")) {
|
||||
String val = getAttr(elem, "value", "");
|
||||
int row = getIntAttr(elem, "row", 0);
|
||||
int col = getIntAttr(elem, "col", 0);
|
||||
boolean casesense = getBooleanAttr(elem, "casesense", false);
|
||||
ECLSDString s = new ECLSDString(val, row, col, casesense, optional);
|
||||
s.setInvertMatch(invertmatch);
|
||||
desc.AddDescriptor(s);
|
||||
} else if (tag.equals("cursor")) {
|
||||
int row = getIntAttr(elem, "row", 0);
|
||||
int col = getIntAttr(elem, "col", 0);
|
||||
int pos = getIntAttr(elem, "pos", 0);
|
||||
ECLSDCursor c;
|
||||
if (row > 0 && col > 0) {
|
||||
c = new ECLSDCursor(row, col, optional);
|
||||
} else {
|
||||
c = new ECLSDCursor(pos, optional);
|
||||
}
|
||||
c.setInvertMatch(invertmatch);
|
||||
desc.AddDescriptor(c);
|
||||
} else if (tag.equals("numfields")) {
|
||||
int count = getIntAttr(elem, "value", 0);
|
||||
ECLSDFields f = new ECLSDFields(count, optional);
|
||||
f.setInvertMatch(invertmatch);
|
||||
desc.AddDescriptor(f);
|
||||
} else if (tag.equals("numinputfields")) {
|
||||
int count = getIntAttr(elem, "value", 0);
|
||||
ECLSDInputFields inf = new ECLSDInputFields(count, optional);
|
||||
inf.setInvertMatch(invertmatch);
|
||||
desc.AddDescriptor(inf);
|
||||
} else if (tag.equals("oia")) {
|
||||
String status = getAttr(elem, "status", getAttr(elem, "input", "NOTINHIBITED"));
|
||||
ECLSDOIA oia = new ECLSDOIA(status, optional);
|
||||
oia.setInvertMatch(invertmatch);
|
||||
desc.AddDescriptor(oia);
|
||||
} else if (tag.equals("checksum")) {
|
||||
int val = getIntAttr(elem, "value", 0);
|
||||
ECLSDChecksum cs = new ECLSDChecksum(val, optional);
|
||||
cs.setInvertMatch(invertmatch);
|
||||
desc.AddDescriptor(cs);
|
||||
} else if (tag.equals("attrib")) {
|
||||
String valStr = getAttr(elem, "value", "");
|
||||
char val = ECLSDAttrib.StringToAttrib(valStr);
|
||||
int row = getIntAttr(elem, "row", 1);
|
||||
int col = getIntAttr(elem, "col", 1);
|
||||
int plane = ECLSDAttrib.StringToPlane(getAttr(elem, "plane", "FIELD_PLANE"));
|
||||
ECLSDAttrib attr = new ECLSDAttrib(val, row, col, plane, optional);
|
||||
attr.setInvertMatch(invertmatch);
|
||||
desc.AddDescriptor(attr);
|
||||
} else if (tag.equals("block")) {
|
||||
int row = getIntAttr(elem, "row", 1);
|
||||
int col = getIntAttr(elem, "col", 1);
|
||||
int erow = getIntAttr(elem, "erow", -1);
|
||||
int ecol = getIntAttr(elem, "ecol", -1);
|
||||
boolean casesense = getBooleanAttr(elem, "casesense", false);
|
||||
|
||||
ECLSDBlock blk;
|
||||
if (erow > 0 || ecol > 0) {
|
||||
blk = new ECLSDBlock(row, col, erow, ecol, casesense, optional);
|
||||
} else {
|
||||
blk = new ECLSDBlock(row, col, casesense, optional);
|
||||
}
|
||||
blk.setInvertMatch(invertmatch);
|
||||
|
||||
NodeList subList = elem.getElementsByTagName("string");
|
||||
for (int j = 0; j < subList.getLength(); j++) {
|
||||
Element strElem = (Element) subList.item(j);
|
||||
blk.addString(getAttr(strElem, "value", ""));
|
||||
}
|
||||
desc.AddDescriptor(blk);
|
||||
}
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
private static MacroActions parseActions(Element actionsElem) {
|
||||
MacroActions actions = new MacroActions();
|
||||
actions.setPromptAll(getBooleanAttr(actionsElem, "promptall", false));
|
||||
|
||||
NodeList children = actionsElem.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node n = children.item(i);
|
||||
if (n.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
MacroAction act = parseActionElement((Element) n);
|
||||
if (act != null) {
|
||||
actions.add(act);
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static MacroAction parseActionElement(Element elem) {
|
||||
String tag = elem.getTagName().toLowerCase();
|
||||
|
||||
if (tag.equals("input")) {
|
||||
MacroActionInput act = new MacroActionInput();
|
||||
act.setValue(getAttr(elem, "value", ""));
|
||||
act.setRow(getIntAttr(elem, "row", 0));
|
||||
act.setCol(getIntAttr(elem, "col", 0));
|
||||
act.setMoveCursor(getBooleanAttr(elem, "movecursor", true));
|
||||
act.setXlateHostKeys(getBooleanAttr(elem, "xlatehostkeys", true));
|
||||
act.setEncrypted(getBooleanAttr(elem, "encrypted", false));
|
||||
return act;
|
||||
} else if (tag.equals("prompt")) {
|
||||
MacroActionPrompt act = new MacroActionPrompt();
|
||||
act.setName(getAttr(elem, "name", ""));
|
||||
act.setDescription(getAttr(elem, "description", ""));
|
||||
act.setTitle(getAttr(elem, "title", "Macro Prompt"));
|
||||
act.setDefaultValue(getAttr(elem, "default", ""));
|
||||
act.setRow(getIntAttr(elem, "row", 0));
|
||||
act.setCol(getIntAttr(elem, "col", 0));
|
||||
act.setLen(getIntAttr(elem, "len", 80));
|
||||
act.setClear(getBooleanAttr(elem, "clear", false));
|
||||
act.setEncrypted(getBooleanAttr(elem, "encrypted", false));
|
||||
act.setMoveCursor(getBooleanAttr(elem, "movecursor", true));
|
||||
act.setXlateHostKeys(getBooleanAttr(elem, "xlatehostkeys", true));
|
||||
act.setVariable(getAttr(elem, "variable", ""));
|
||||
act.setRequired(getBooleanAttr(elem, "required", false));
|
||||
act.setDontSend(getBooleanAttr(elem, "dontsend", false));
|
||||
return act;
|
||||
} else if (tag.equals("extract")) {
|
||||
MacroActionExtract act = new MacroActionExtract();
|
||||
act.setName(getAttr(elem, "name", ""));
|
||||
act.setRow(getIntAttr(elem, "row", 1));
|
||||
act.setCol(getIntAttr(elem, "col", 1));
|
||||
act.setErow(getIntAttr(elem, "erow", -1));
|
||||
act.setEcol(getIntAttr(elem, "ecol", -1));
|
||||
act.setPlane(getAttr(elem, "plane", "TEXT_PLANE"));
|
||||
act.setContinuous(getBooleanAttr(elem, "continuous", false));
|
||||
act.setFieldLevel(getBooleanAttr(elem, "fieldlevel", false));
|
||||
act.setVariable(getAttr(elem, "variable", ""));
|
||||
return act;
|
||||
} else if (tag.equals("pause")) {
|
||||
MacroActionPause act = new MacroActionPause();
|
||||
String val = getAttr(elem, "value", "10000");
|
||||
try {
|
||||
act.setTimeout(Integer.parseInt(val.trim()));
|
||||
} catch (NumberFormatException e) {
|
||||
act.setTimeoutExpr(val);
|
||||
}
|
||||
return act;
|
||||
} else if (tag.equals("message")) {
|
||||
MacroActionMessage act = new MacroActionMessage();
|
||||
act.setTitle(getAttr(elem, "title", ""));
|
||||
act.setValue(getAttr(elem, "value", ""));
|
||||
return act;
|
||||
} else if (tag.equals("playmacro")) {
|
||||
MacroActionPlayMacro act = new MacroActionPlayMacro();
|
||||
act.setName(getAttr(elem, "name", ""));
|
||||
act.setStartScreen(getAttr(elem, "startscreen", "*DEFAULT*"));
|
||||
return act;
|
||||
} else if (tag.equals("commwait")) {
|
||||
MacroActionCommWait act = new MacroActionCommWait();
|
||||
act.setValue(getAttr(elem, "value", "CONNECTION_READY"));
|
||||
act.setTimeout(getIntAttr(elem, "timeout", 10000));
|
||||
return act;
|
||||
} else if (tag.equals("varupdate")) {
|
||||
MacroActionVarUpdate act = new MacroActionVarUpdate();
|
||||
act.setName(getAttr(elem, "name", ""));
|
||||
act.setValue(getAttr(elem, "value", ""));
|
||||
return act;
|
||||
} else if (tag.equals("if")) {
|
||||
MacroActionIf act = new MacroActionIf(getAttr(elem, "condition", "true"));
|
||||
NodeList ifChildren = elem.getChildNodes();
|
||||
for (int j = 0; j < ifChildren.getLength(); j++) {
|
||||
Node ifChild = ifChildren.item(j);
|
||||
if (ifChild.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
Element ifElem = (Element) ifChild;
|
||||
if (ifElem.getTagName().equalsIgnoreCase("else")) {
|
||||
NodeList elseChildren = ifElem.getChildNodes();
|
||||
for (int k = 0; k < elseChildren.getLength(); k++) {
|
||||
Node elseChild = elseChildren.item(k);
|
||||
if (elseChild.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
MacroAction elseAct = parseActionElement((Element) elseChild);
|
||||
if (elseAct != null) act.addElseAction(elseAct);
|
||||
}
|
||||
} else {
|
||||
MacroAction thenAct = parseActionElement(ifElem);
|
||||
if (thenAct != null) act.addIfAction(thenAct);
|
||||
}
|
||||
}
|
||||
return act;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MacroNextScreens parseNextScreens(Element nextElem) {
|
||||
MacroNextScreens next = new MacroNextScreens();
|
||||
next.setTimeout(getIntAttr(nextElem, "timeout", 0));
|
||||
|
||||
NodeList list = nextElem.getElementsByTagName("nextscreen");
|
||||
for (int i = 0; i < list.getLength(); i++) {
|
||||
Element el = (Element) list.item(i);
|
||||
String name = getAttr(el, "name", "");
|
||||
if (!name.isEmpty()) {
|
||||
next.addNextScreen(name);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private static String getAttr(Element elem, String name, String defaultVal) {
|
||||
if (elem == null || !elem.hasAttribute(name)) return defaultVal;
|
||||
return elem.getAttribute(name);
|
||||
}
|
||||
|
||||
private static int getIntAttr(Element elem, String name, int defaultVal) {
|
||||
if (elem == null || !elem.hasAttribute(name)) return defaultVal;
|
||||
try {
|
||||
return Integer.parseInt(elem.getAttribute(name).trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean getBooleanAttr(Element elem, String name, boolean defaultVal) {
|
||||
if (elem == null || !elem.hasAttribute(name)) return defaultVal;
|
||||
String val = elem.getAttribute(name).trim();
|
||||
return val.equalsIgnoreCase("true") || val.equalsIgnoreCase("yes") || val.equals("1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package haus.nightmare.lib3270j.macro;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroAction;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
|
||||
|
||||
/**
|
||||
* Worker thread executing the HoD Macro finite state machine matching PlayThread.
|
||||
*/
|
||||
public class PlayThread extends Thread {
|
||||
|
||||
private final Macro macro;
|
||||
private final MacroScreens screens;
|
||||
private final ECLSession session;
|
||||
private final String initialScreenName;
|
||||
|
||||
private volatile boolean running = true;
|
||||
private volatile boolean paused = false;
|
||||
private final Object pauseLock = new Object();
|
||||
|
||||
public PlayThread(Macro macro, MacroScreens screens, ECLSession session) {
|
||||
this(macro, screens, session, null);
|
||||
}
|
||||
|
||||
public PlayThread(Macro macro, MacroScreens screens, ECLSession session, String initialScreenName) {
|
||||
super("j3270-MacroPlayThread");
|
||||
setDaemon(true);
|
||||
this.macro = macro;
|
||||
this.screens = screens;
|
||||
this.session = session;
|
||||
this.initialScreenName = initialScreenName;
|
||||
}
|
||||
|
||||
public void halt() {
|
||||
running = false;
|
||||
resumePlay();
|
||||
interrupt();
|
||||
}
|
||||
|
||||
public void pausePlay() {
|
||||
paused = true;
|
||||
}
|
||||
|
||||
public void resumePlay() {
|
||||
synchronized (pauseLock) {
|
||||
paused = false;
|
||||
pauseLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
macro.setState(Macro.STATE_PLAYING);
|
||||
|
||||
if (screens == null || screens.isEmpty()) {
|
||||
macro.logError("Macro is empty - no screens to execute.");
|
||||
macro.setState(Macro.STATE_STOPPED);
|
||||
return;
|
||||
}
|
||||
|
||||
MacroScreen start = null;
|
||||
if (initialScreenName != null && !initialScreenName.trim().isEmpty()) {
|
||||
start = screens.getByName(initialScreenName);
|
||||
}
|
||||
if (start == null) {
|
||||
start = screens.getStartScreen();
|
||||
}
|
||||
|
||||
if (start == null) {
|
||||
macro.logError("No valid starting screen found in macro.");
|
||||
macro.setState(Macro.STATE_STOPPED);
|
||||
return;
|
||||
}
|
||||
|
||||
List<MacroScreen> currentCandidates = new ArrayList<>();
|
||||
currentCandidates.add(start);
|
||||
|
||||
long screenWaitStart = System.currentTimeMillis();
|
||||
int currentTimeout = screens.getTimeout() > 0 ? screens.getTimeout() : 10000;
|
||||
|
||||
while (running && !isInterrupted()) {
|
||||
// Handle pause
|
||||
checkPause();
|
||||
if (!running) break;
|
||||
|
||||
// 1. Try to find a matching screen among current candidates
|
||||
MacroScreen matchedScreen = null;
|
||||
ECLPS ps = (session != null) ? session.GetPS() : null;
|
||||
|
||||
if (ps != null) {
|
||||
for (MacroScreen cand : currentCandidates) {
|
||||
if (cand.getDescription() != null && cand.getDescription().Matches(session)) {
|
||||
matchedScreen = cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedScreen == null) {
|
||||
// Check timeout
|
||||
long elapsed = System.currentTimeMillis() - screenWaitStart;
|
||||
if (elapsed > currentTimeout) {
|
||||
StringBuilder names = new StringBuilder();
|
||||
for (MacroScreen c : currentCandidates) {
|
||||
if (names.length() > 0) names.append(", ");
|
||||
names.append(c.getName());
|
||||
}
|
||||
macro.logError("Macro timed out waiting for screen(s): [" + names + "] after " + elapsed + "ms");
|
||||
macro.setState(Macro.STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Screen matched!
|
||||
matchedScreen.incrementRecoCount();
|
||||
if (matchedScreen.getRecoLimit() > 0 && matchedScreen.getRecoCount() > matchedScreen.getRecoLimit()) {
|
||||
String gotoScreen = matchedScreen.getRecoGoto();
|
||||
if (gotoScreen != null && !gotoScreen.trim().isEmpty() && screens.contains(gotoScreen)) {
|
||||
matchedScreen.resetRecoCount();
|
||||
currentCandidates.clear();
|
||||
currentCandidates.add(screens.getByName(gotoScreen));
|
||||
screenWaitStart = System.currentTimeMillis();
|
||||
continue;
|
||||
} else {
|
||||
macro.logError("Recognition limit (" + matchedScreen.getRecoLimit() + ") exceeded on screen: " + matchedScreen.getName());
|
||||
macro.setState(Macro.STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
macro.notifyScreenMatching(matchedScreen);
|
||||
|
||||
// Pause time before actions if configured
|
||||
int pauseTime = matchedScreen.getPauseTime() >= 0 ? matchedScreen.getPauseTime() : screens.getPauseTime();
|
||||
if (pauseTime > 0) {
|
||||
try {
|
||||
Thread.sleep(pauseTime);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Execute actions
|
||||
MacroActions actions = matchedScreen.getActions();
|
||||
if (actions != null) {
|
||||
for (MacroAction act : actions.getActions()) {
|
||||
checkPause();
|
||||
if (!running) break;
|
||||
|
||||
macro.notifyActionExecuting(act);
|
||||
act.execute(macro, session);
|
||||
|
||||
if (act instanceof MacroActionPrompt && ((MacroActionPrompt) act).isCancelled()) {
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!running) break;
|
||||
|
||||
// 4. Check if screen is exit screen
|
||||
if (matchedScreen.isExitScreen()) {
|
||||
macro.setState(Macro.STATE_STOPPED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Transition to nextscreens
|
||||
MacroNextScreens next = matchedScreen.getNextScreens();
|
||||
if (next == null || next.isEmpty()) {
|
||||
// No next screens -> macro successfully completed
|
||||
macro.setState(Macro.STATE_STOPPED);
|
||||
return;
|
||||
}
|
||||
|
||||
List<MacroScreen> nextCandidates = new ArrayList<>();
|
||||
for (String nextName : next.getNextScreenNames()) {
|
||||
MacroScreen s = screens.getByName(nextName);
|
||||
if (s != null) {
|
||||
nextCandidates.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextCandidates.isEmpty()) {
|
||||
macro.logError("Next screens configured but none exist in macro: " + next.getNextScreenNames());
|
||||
macro.setState(Macro.STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
currentCandidates = nextCandidates;
|
||||
screenWaitStart = System.currentTimeMillis();
|
||||
currentTimeout = (next.getTimeout() > 0) ? next.getTimeout() : (screens.getTimeout() > 0 ? screens.getTimeout() : 10000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
macro.logError("Macro execution error: " + e.getMessage());
|
||||
macro.setState(Macro.STATE_ERROR);
|
||||
} finally {
|
||||
if (macro.getState() == Macro.STATE_PLAYING) {
|
||||
macro.setState(Macro.STATE_STOPPED);
|
||||
}
|
||||
// Reset reco counters
|
||||
if (screens != null) {
|
||||
for (MacroScreen s : screens.getScreens()) {
|
||||
s.resetRecoCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPause() {
|
||||
while (paused && running) {
|
||||
synchronized (pauseLock) {
|
||||
try {
|
||||
pauseLock.wait(100);
|
||||
} catch (InterruptedException e) {
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import java.io.Serializable;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroVariables;
|
||||
|
||||
/**
|
||||
* Abstract base class for all macro actions matching IBM Host On-Demand MacroAction.
|
||||
*/
|
||||
public abstract class MacroAction implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected Macro mMacro;
|
||||
protected ECLSession mECLSession;
|
||||
protected int mLineNum = -1;
|
||||
protected String comments = "";
|
||||
|
||||
public MacroAction() {}
|
||||
|
||||
public void setOwner(Macro macro) {
|
||||
this.mMacro = macro;
|
||||
}
|
||||
|
||||
public Macro getOwner() {
|
||||
return mMacro;
|
||||
}
|
||||
|
||||
public void setECLSession(ECLSession session) {
|
||||
this.mECLSession = session;
|
||||
}
|
||||
|
||||
public ECLSession getECLSession() {
|
||||
return mECLSession;
|
||||
}
|
||||
|
||||
public int getLineNum() {
|
||||
return mLineNum;
|
||||
}
|
||||
|
||||
public void setLineNum(int lineNum) {
|
||||
this.mLineNum = lineNum;
|
||||
}
|
||||
|
||||
public String getComments() {
|
||||
return comments;
|
||||
}
|
||||
|
||||
public void setComments(String comments) {
|
||||
this.comments = comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes this macro action against the given macro engine and host session.
|
||||
*/
|
||||
public abstract void execute(Macro macro, ECLSession session);
|
||||
|
||||
/**
|
||||
* Convenience execute method invoking the bound owner and session.
|
||||
*/
|
||||
public void execute() {
|
||||
if (mMacro != null) {
|
||||
execute(mMacro, mECLSession != null ? mECLSession : mMacro.getECLSession());
|
||||
}
|
||||
}
|
||||
|
||||
public void halt() {}
|
||||
|
||||
/**
|
||||
* Serializes this action to its XML representation.
|
||||
*/
|
||||
public abstract String format(int indent);
|
||||
|
||||
public String getXML() {
|
||||
return format(0);
|
||||
}
|
||||
|
||||
protected String resolve(String expression, Macro macro) {
|
||||
if (expression == null) return "";
|
||||
if (macro == null) return expression;
|
||||
MacroVariables vars = macro.getVariables();
|
||||
if (vars == null || !macro.isUseVars()) {
|
||||
return expression;
|
||||
}
|
||||
return MacroExpressionParser.evaluateString(vars, expression);
|
||||
}
|
||||
|
||||
protected String indentSpaces(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < indent; i++) {
|
||||
sb.append(" ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String escapeXml(String text) {
|
||||
if (text == null) return "";
|
||||
return text.replace("&", "&")
|
||||
.replace("\"", """)
|
||||
.replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract MacroAction clone();
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLConnection;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <commwait>.
|
||||
* Waits for the host session connection to reach a desired state (e.g. CONNECTION_READY).
|
||||
*/
|
||||
public class MacroActionCommWait extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String value = "CONNECTION_READY";
|
||||
private int timeout = 10000;
|
||||
|
||||
public MacroActionCommWait() {}
|
||||
|
||||
public MacroActionCommWait(String value, int timeout) {
|
||||
this.value = (value != null) ? value : "CONNECTION_READY";
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = (value != null) ? value : "CONNECTION_READY"; }
|
||||
|
||||
public int getTimeout() { return timeout; }
|
||||
public void setTimeout(int timeout) { this.timeout = timeout; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
if (session == null) return;
|
||||
ECLConnection conn = session.GetConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
long maxWait = (timeout > 0) ? timeout : 10000;
|
||||
|
||||
while (System.currentTimeMillis() - start < maxWait) {
|
||||
if (conn.IsReady() || conn.IsConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<commwait value=\"").append(escapeXml(value)).append("\"");
|
||||
if (timeout > 0) sb.append(" timeout=\"").append(timeout).append("\"");
|
||||
sb.append(" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionCommWait clone() {
|
||||
MacroActionCommWait c = new MacroActionCommWait(value, timeout);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <extract>.
|
||||
* Extracts text from the host screen into a macro variable.
|
||||
*/
|
||||
public class MacroActionExtract extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String name = "";
|
||||
private int row = 1;
|
||||
private int col = 1;
|
||||
private int erow = -1;
|
||||
private int ecol = -1;
|
||||
private String plane = "TEXT_PLANE";
|
||||
private boolean continuous = false;
|
||||
private boolean fieldLevel = false;
|
||||
private String variable = "";
|
||||
private String lastExtracted = "";
|
||||
|
||||
public MacroActionExtract() {}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = (name != null) ? name : ""; }
|
||||
|
||||
public int getRow() { return row; }
|
||||
public void setRow(int row) { this.row = row; }
|
||||
|
||||
public int getCol() { return col; }
|
||||
public void setCol(int col) { this.col = col; }
|
||||
|
||||
public int getErow() { return erow; }
|
||||
public void setErow(int erow) { this.erow = erow; }
|
||||
|
||||
public int getEcol() { return ecol; }
|
||||
public void setEcol(int ecol) { this.ecol = ecol; }
|
||||
|
||||
public String getPlane() { return plane; }
|
||||
public void setPlane(String plane) { this.plane = (plane != null) ? plane : "TEXT_PLANE"; }
|
||||
|
||||
public boolean isContinuous() { return continuous; }
|
||||
public void setContinuous(boolean continuous) { this.continuous = continuous; }
|
||||
|
||||
public boolean isFieldLevel() { return fieldLevel; }
|
||||
public void setFieldLevel(boolean fieldLevel) { this.fieldLevel = fieldLevel; }
|
||||
|
||||
public String getVariable() { return variable; }
|
||||
public void setVariable(String variable) { this.variable = (variable != null) ? variable : ""; }
|
||||
|
||||
public String getLastExtracted() { return lastExtracted; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
if (session == null) return;
|
||||
ECLPS ps = session.GetPS();
|
||||
if (ps == null) return;
|
||||
|
||||
try {
|
||||
int r = Math.max(1, row);
|
||||
int c = Math.max(1, col);
|
||||
int er = (erow > 0) ? erow : r;
|
||||
int ec = (ecol > 0) ? ecol : (col > 0 && erow <= 0 ? c : ps.GetSizeCols());
|
||||
|
||||
int planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_TEXT;
|
||||
if ("COLOR_PLANE".equalsIgnoreCase(plane) || "PLANE_COLOR".equalsIgnoreCase(plane)) {
|
||||
planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_COLOR;
|
||||
} else if ("FIELD_PLANE".equalsIgnoreCase(plane) || "PLANE_FIELD".equalsIgnoreCase(plane)) {
|
||||
planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_FIELD;
|
||||
} else if ("HILITE_PLANE".equalsIgnoreCase(plane) || "PLANE_HILITE".equalsIgnoreCase(plane)) {
|
||||
planeVal = haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_HILITE;
|
||||
}
|
||||
|
||||
String extracted;
|
||||
if (continuous) {
|
||||
int startPos = ps.ConvertRowColToPos(r, c);
|
||||
int endPos = ps.ConvertRowColToPos(er, ec);
|
||||
int len = Math.max(0, endPos - startPos + 1);
|
||||
char[] buf = new char[len];
|
||||
if (planeVal == haus.nightmare.lib3270j.ecl.ECLConstants.PLANE_TEXT) {
|
||||
ps.GetString(buf, len, startPos);
|
||||
} else {
|
||||
ps.getPlane(planeVal, buf, startPos - 1, len);
|
||||
}
|
||||
extracted = new String(buf);
|
||||
} else {
|
||||
int height = er - r + 1;
|
||||
int width = ec - c + 1;
|
||||
int len = height * width;
|
||||
char[] buf = new char[len];
|
||||
ps.GetScreenRect(buf, len, r, c, er, ec, planeVal);
|
||||
extracted = new String(buf);
|
||||
}
|
||||
|
||||
this.lastExtracted = extracted;
|
||||
|
||||
if (variable != null && !variable.trim().isEmpty() && macro != null && macro.getVariables() != null) {
|
||||
macro.getVariables().setVariable(variable, extracted);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (macro != null) {
|
||||
macro.logError("MacroActionExtract error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<extract");
|
||||
if (!name.isEmpty()) sb.append(" name=\"").append(escapeXml(name)).append("\"");
|
||||
sb.append(" row=\"").append(row).append("\"");
|
||||
sb.append(" col=\"").append(col).append("\"");
|
||||
if (erow > 0) sb.append(" erow=\"").append(erow).append("\"");
|
||||
if (ecol > 0) sb.append(" ecol=\"").append(ecol).append("\"");
|
||||
if (plane != null && !plane.equals("TEXT_PLANE")) sb.append(" plane=\"").append(escapeXml(plane)).append("\"");
|
||||
if (continuous) sb.append(" continuous=\"true\"");
|
||||
if (fieldLevel) sb.append(" fieldlevel=\"true\"");
|
||||
if (!variable.isEmpty()) sb.append(" variable=\"").append(escapeXml(variable)).append("\"");
|
||||
sb.append(" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionExtract clone() {
|
||||
MacroActionExtract c = new MacroActionExtract();
|
||||
c.setName(name);
|
||||
c.setRow(row);
|
||||
c.setCol(col);
|
||||
c.setErow(erow);
|
||||
c.setEcol(ecol);
|
||||
c.setPlane(plane);
|
||||
c.setContinuous(continuous);
|
||||
c.setFieldLevel(fieldLevel);
|
||||
c.setVariable(variable);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <if>.
|
||||
* Evaluates a conditional expression and branches to then-actions or else-actions.
|
||||
*/
|
||||
public class MacroActionIf extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String condition = "true";
|
||||
private final List<MacroAction> ifActions = new ArrayList<>();
|
||||
private final List<MacroAction> elseActions = new ArrayList<>();
|
||||
|
||||
public MacroActionIf() {}
|
||||
|
||||
public MacroActionIf(String condition) {
|
||||
this.condition = (condition != null) ? condition : "true";
|
||||
}
|
||||
|
||||
public String getCondition() { return condition; }
|
||||
public void setCondition(String condition) { this.condition = (condition != null) ? condition : "true"; }
|
||||
|
||||
public List<MacroAction> getIfActions() { return ifActions; }
|
||||
public void addIfAction(MacroAction action) { if (action != null) ifActions.add(action); }
|
||||
|
||||
public List<MacroAction> getElseActions() { return elseActions; }
|
||||
public void addElseAction(MacroAction action) { if (action != null) elseActions.add(action); }
|
||||
|
||||
public List<MacroAction> getActiveBranchActions(Macro macro) {
|
||||
boolean match = evaluateCondition(macro);
|
||||
return match ? ifActions : elseActions;
|
||||
}
|
||||
|
||||
public boolean evaluateCondition(Macro macro) {
|
||||
if (condition == null || condition.trim().isEmpty() || condition.equalsIgnoreCase("true")) {
|
||||
return true;
|
||||
}
|
||||
if (macro != null && macro.getVariables() != null) {
|
||||
return MacroExpressionParser.evaluateBoolean(macro.getVariables(), condition);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
List<MacroAction> branch = getActiveBranchActions(macro);
|
||||
for (MacroAction act : branch) {
|
||||
act.execute(macro, session);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String ind = indentSpaces(indent);
|
||||
sb.append(ind).append("<if condition=\"").append(escapeXml(condition)).append("\">\n");
|
||||
for (MacroAction act : ifActions) {
|
||||
sb.append(act.format(indent + 1)).append("\n");
|
||||
}
|
||||
if (!elseActions.isEmpty()) {
|
||||
sb.append(ind).append(" <else>\n");
|
||||
for (MacroAction act : elseActions) {
|
||||
sb.append(act.format(indent + 2)).append("\n");
|
||||
}
|
||||
sb.append(ind).append(" </else>\n");
|
||||
}
|
||||
sb.append(ind).append("</if>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionIf clone() {
|
||||
MacroActionIf c = new MacroActionIf(condition);
|
||||
for (MacroAction a : ifActions) c.addIfAction(a.clone());
|
||||
for (MacroAction a : elseActions) c.addElseAction(a.clone());
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <input>.
|
||||
* Inputs text and keystroke mnemonics into the host presentation space.
|
||||
*/
|
||||
public class MacroActionInput extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String value = "";
|
||||
private int row = 0;
|
||||
private int col = 0;
|
||||
private boolean moveCursor = true;
|
||||
private boolean xlateHostKeys = true;
|
||||
private boolean encrypted = false;
|
||||
|
||||
public MacroActionInput() {}
|
||||
|
||||
public MacroActionInput(String value) {
|
||||
this.value = (value != null) ? value : "";
|
||||
}
|
||||
|
||||
public MacroActionInput(String value, int row, int col, boolean moveCursor, boolean xlateHostKeys) {
|
||||
this.value = (value != null) ? value : "";
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
this.moveCursor = moveCursor;
|
||||
this.xlateHostKeys = xlateHostKeys;
|
||||
}
|
||||
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = (value != null) ? value : ""; }
|
||||
|
||||
public int getRow() { return row; }
|
||||
public void setRow(int row) { this.row = row; }
|
||||
|
||||
public int getCol() { return col; }
|
||||
public void setCol(int col) { this.col = col; }
|
||||
|
||||
public boolean isMoveCursor() { return moveCursor; }
|
||||
public void setMoveCursor(boolean moveCursor) { this.moveCursor = moveCursor; }
|
||||
|
||||
public boolean isXlateHostKeys() { return xlateHostKeys; }
|
||||
public void setXlateHostKeys(boolean xlateHostKeys) { this.xlateHostKeys = xlateHostKeys; }
|
||||
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
if (session == null) return;
|
||||
ECLPS ps = session.GetPS();
|
||||
if (ps == null) return;
|
||||
|
||||
String text = value;
|
||||
if (macro != null && macro.isUseVars()) {
|
||||
text = MacroExpressionParser.resolveText(macro.getVariables(), value);
|
||||
if (text.equals(value) && (value.contains("+") || value.contains("-") || value.startsWith("$"))) {
|
||||
text = MacroExpressionParser.evaluateString(macro.getVariables(), value);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (row > 0 && col > 0) {
|
||||
if (moveCursor) {
|
||||
ps.SetCursorPos(row, col);
|
||||
}
|
||||
if (xlateHostKeys) {
|
||||
ps.SendKeys(text, row, col);
|
||||
} else {
|
||||
ps.SetText(text, row, col);
|
||||
}
|
||||
} else {
|
||||
if (xlateHostKeys) {
|
||||
ps.SendKeys(text);
|
||||
} else {
|
||||
ps.SetText(text);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (macro != null) {
|
||||
macro.logError("MacroActionInput failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<input value=\"").append(escapeXml(value)).append("\"");
|
||||
if (row > 0) sb.append(" row=\"").append(row).append("\"");
|
||||
if (col > 0) sb.append(" col=\"").append(col).append("\"");
|
||||
if (!moveCursor) sb.append(" movecursor=\"false\"");
|
||||
if (!xlateHostKeys) sb.append(" xlatehostkeys=\"false\"");
|
||||
if (encrypted) sb.append(" encrypted=\"true\"");
|
||||
sb.append(" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionInput clone() {
|
||||
MacroActionInput c = new MacroActionInput(value, row, col, moveCursor, xlateHostKeys);
|
||||
c.setEncrypted(encrypted);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <message>.
|
||||
* Displays a popup message or status notification.
|
||||
*/
|
||||
public class MacroActionMessage extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String title = "";
|
||||
private String value = "";
|
||||
|
||||
public MacroActionMessage() {}
|
||||
|
||||
public MacroActionMessage(String title, String value) {
|
||||
this.title = (title != null) ? title : "";
|
||||
this.value = (value != null) ? value : "";
|
||||
}
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = (title != null) ? title : ""; }
|
||||
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = (value != null) ? value : ""; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
String t = title;
|
||||
String v = value;
|
||||
if (macro != null && macro.isUseVars()) {
|
||||
t = MacroExpressionParser.resolveText(macro.getVariables(), title);
|
||||
v = MacroExpressionParser.resolveText(macro.getVariables(), value);
|
||||
}
|
||||
|
||||
if (macro != null) {
|
||||
macro.notifyMessage(t, v);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<message title=\"").append(escapeXml(title))
|
||||
.append("\" value=\"").append(escapeXml(value)).append("\" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionMessage clone() {
|
||||
MacroActionMessage c = new MacroActionMessage(title, value);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <pause>.
|
||||
* Pauses macro execution for a specified duration in milliseconds.
|
||||
*/
|
||||
public class MacroActionPause extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int timeout = 10000;
|
||||
private String timeoutExpr = null;
|
||||
|
||||
public MacroActionPause() {}
|
||||
|
||||
public MacroActionPause(int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public int getTimeout() { return timeout; }
|
||||
public void setTimeout(int timeout) { this.timeout = timeout; }
|
||||
|
||||
public String getTimeoutExpr() { return timeoutExpr; }
|
||||
public void setTimeoutExpr(String timeoutExpr) { this.timeoutExpr = timeoutExpr; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
int duration = timeout;
|
||||
if (timeoutExpr != null && macro != null && macro.getVariables() != null) {
|
||||
duration = MacroExpressionParser.evaluateInt(macro.getVariables(), timeoutExpr);
|
||||
}
|
||||
if (duration <= 0) return;
|
||||
|
||||
try {
|
||||
Thread.sleep(duration);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<pause value=\"")
|
||||
.append(timeoutExpr != null ? escapeXml(timeoutExpr) : String.valueOf(timeout))
|
||||
.append("\" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionPause clone() {
|
||||
MacroActionPause c = new MacroActionPause(timeout);
|
||||
c.setTimeoutExpr(timeoutExpr);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <playmacro>.
|
||||
* Chains execution to another macro.
|
||||
*/
|
||||
public class MacroActionPlayMacro extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String name = "";
|
||||
private String startScreen = "*DEFAULT*";
|
||||
|
||||
public MacroActionPlayMacro() {}
|
||||
|
||||
public MacroActionPlayMacro(String name) {
|
||||
this.name = (name != null) ? name : "";
|
||||
}
|
||||
|
||||
public MacroActionPlayMacro(String name, String startScreen) {
|
||||
this.name = (name != null) ? name : "";
|
||||
if (startScreen != null && !startScreen.trim().isEmpty()) {
|
||||
this.startScreen = startScreen.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = (name != null) ? name : ""; }
|
||||
|
||||
public String getStartScreen() { return startScreen; }
|
||||
public void setStartScreen(String startScreen) { this.startScreen = (startScreen != null) ? startScreen : "*DEFAULT*"; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
if (macro != null) {
|
||||
macro.playChainedMacro(name, startScreen);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<playmacro name=\"").append(escapeXml(name)).append("\"");
|
||||
if (startScreen != null && !startScreen.equals("*DEFAULT*")) {
|
||||
sb.append(" startscreen=\"").append(escapeXml(startScreen)).append("\"");
|
||||
}
|
||||
sb.append(" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionPlayMacro clone() {
|
||||
MacroActionPlayMacro c = new MacroActionPlayMacro(name, startScreen);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <prompt>.
|
||||
* Requests user input via modal dialog, updates variable, and optionally types input to host.
|
||||
*/
|
||||
public class MacroActionPrompt extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String name = "";
|
||||
private String description = "";
|
||||
private String title = "Macro Prompt";
|
||||
private String defaultValue = "";
|
||||
private int row = 0;
|
||||
private int col = 0;
|
||||
private int len = 80;
|
||||
private boolean clear = false;
|
||||
private boolean encrypted = false;
|
||||
private boolean moveCursor = true;
|
||||
private boolean xlateHostKeys = true;
|
||||
private String variable = "";
|
||||
private boolean required = false;
|
||||
private boolean dontSend = false;
|
||||
private boolean cancelled = false;
|
||||
|
||||
public MacroActionPrompt() {}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = (name != null) ? name : ""; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = (description != null) ? description : ""; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = (title != null) ? title : "Macro Prompt"; }
|
||||
|
||||
public String getDefaultValue() { return defaultValue; }
|
||||
public void setDefaultValue(String defaultValue) { this.defaultValue = (defaultValue != null) ? defaultValue : ""; }
|
||||
|
||||
public int getRow() { return row; }
|
||||
public void setRow(int row) { this.row = row; }
|
||||
|
||||
public int getCol() { return col; }
|
||||
public void setCol(int col) { this.col = col; }
|
||||
|
||||
public int getLen() { return len; }
|
||||
public void setLen(int len) { this.len = len; }
|
||||
|
||||
public boolean isClear() { return clear; }
|
||||
public void setClear(boolean clear) { this.clear = clear; }
|
||||
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
|
||||
|
||||
public boolean isMoveCursor() { return moveCursor; }
|
||||
public void setMoveCursor(boolean moveCursor) { this.moveCursor = moveCursor; }
|
||||
|
||||
public boolean isXlateHostKeys() { return xlateHostKeys; }
|
||||
public void setXlateHostKeys(boolean xlateHostKeys) { this.xlateHostKeys = xlateHostKeys; }
|
||||
|
||||
public String getVariable() { return variable; }
|
||||
public void setVariable(String variable) { this.variable = (variable != null) ? variable : ""; }
|
||||
|
||||
public boolean isRequired() { return required; }
|
||||
public void setRequired(boolean required) { this.required = required; }
|
||||
|
||||
public boolean isDontSend() { return dontSend; }
|
||||
public void setDontSend(boolean dontSend) { this.dontSend = dontSend; }
|
||||
|
||||
public boolean isCancelled() { return cancelled; }
|
||||
public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
String response = defaultValue;
|
||||
cancelled = false;
|
||||
|
||||
if (macro != null && macro.getPromptHandler() != null) {
|
||||
String prompted = macro.getPromptHandler().prompt(this);
|
||||
if (prompted == null) {
|
||||
cancelled = true;
|
||||
macro.stop();
|
||||
return;
|
||||
}
|
||||
response = prompted;
|
||||
}
|
||||
|
||||
if (variable != null && !variable.trim().isEmpty() && macro != null && macro.getVariables() != null) {
|
||||
macro.getVariables().setVariable(variable, response);
|
||||
}
|
||||
|
||||
if (!dontSend && session != null) {
|
||||
ECLPS ps = session.GetPS();
|
||||
if (ps != null) {
|
||||
try {
|
||||
if (clear) {
|
||||
if (row > 0 && col > 0) {
|
||||
ps.SendKeys("[erasefld]", row, col);
|
||||
} else {
|
||||
ps.SendKeys("[erasefld]");
|
||||
}
|
||||
}
|
||||
if (row > 0 && col > 0) {
|
||||
if (moveCursor) {
|
||||
ps.SetCursorPos(row, col);
|
||||
}
|
||||
if (xlateHostKeys) {
|
||||
ps.SendKeys(response, row, col);
|
||||
} else {
|
||||
ps.SetText(response, row, col);
|
||||
}
|
||||
} else {
|
||||
if (xlateHostKeys) {
|
||||
ps.SendKeys(response);
|
||||
} else {
|
||||
ps.SetText(response);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (macro != null) {
|
||||
macro.logError("MacroActionPrompt typing error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<prompt");
|
||||
if (!name.isEmpty()) sb.append(" name=\"").append(escapeXml(name)).append("\"");
|
||||
if (!description.isEmpty()) sb.append(" description=\"").append(escapeXml(description)).append("\"");
|
||||
if (!title.isEmpty() && !title.equals("Macro Prompt")) sb.append(" title=\"").append(escapeXml(title)).append("\"");
|
||||
if (!defaultValue.isEmpty()) sb.append(" default=\"").append(escapeXml(defaultValue)).append("\"");
|
||||
if (row > 0) sb.append(" row=\"").append(row).append("\"");
|
||||
if (col > 0) sb.append(" col=\"").append(col).append("\"");
|
||||
if (len != 80) sb.append(" len=\"").append(len).append("\"");
|
||||
if (clear) sb.append(" clear=\"true\"");
|
||||
if (encrypted) sb.append(" encrypted=\"true\"");
|
||||
if (!moveCursor) sb.append(" movecursor=\"false\"");
|
||||
if (!xlateHostKeys) sb.append(" xlatehostkeys=\"false\"");
|
||||
if (!variable.isEmpty()) sb.append(" variable=\"").append(escapeXml(variable)).append("\"");
|
||||
if (required) sb.append(" required=\"true\"");
|
||||
if (dontSend) sb.append(" dontsend=\"true\"");
|
||||
sb.append(" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionPrompt clone() {
|
||||
MacroActionPrompt c = new MacroActionPrompt();
|
||||
c.setName(name);
|
||||
c.setDescription(description);
|
||||
c.setTitle(title);
|
||||
c.setDefaultValue(defaultValue);
|
||||
c.setRow(row);
|
||||
c.setCol(col);
|
||||
c.setLen(len);
|
||||
c.setClear(clear);
|
||||
c.setEncrypted(encrypted);
|
||||
c.setMoveCursor(moveCursor);
|
||||
c.setXlateHostKeys(xlateHostKeys);
|
||||
c.setVariable(variable);
|
||||
c.setRequired(required);
|
||||
c.setDontSend(dontSend);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package haus.nightmare.lib3270j.macro.action;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroVariables;
|
||||
|
||||
/**
|
||||
* Macro action matching IBM HoD <varupdate>.
|
||||
* Evaluates an expression and assigns the result to a macro variable.
|
||||
*/
|
||||
public class MacroActionVarUpdate extends MacroAction {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String name = "";
|
||||
private String value = "";
|
||||
|
||||
public MacroActionVarUpdate() {}
|
||||
|
||||
public MacroActionVarUpdate(String name, String value) {
|
||||
this.name = (name != null) ? name : "";
|
||||
this.value = (value != null) ? value : "";
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = (name != null) ? name : ""; }
|
||||
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = (value != null) ? value : ""; }
|
||||
|
||||
@Override
|
||||
public void execute(Macro macro, ECLSession session) {
|
||||
if (macro == null || name == null || name.trim().isEmpty()) return;
|
||||
MacroVariables vars = macro.getVariables();
|
||||
if (vars == null) return;
|
||||
|
||||
Object evalRes = MacroExpressionParser.evaluate(vars, value);
|
||||
vars.setVariable(name, evalRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(indentSpaces(indent)).append("<varupdate name=\"").append(escapeXml(name))
|
||||
.append("\" value=\"").append(escapeXml(value)).append("\" />");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActionVarUpdate clone() {
|
||||
MacroActionVarUpdate c = new MacroActionVarUpdate(name, value);
|
||||
c.setLineNum(mLineNum);
|
||||
c.setComments(comments);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
+490
@@ -0,0 +1,490 @@
|
||||
package haus.nightmare.lib3270j.macro.variable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Expression evaluator for IBM Host On-Demand macros.
|
||||
* Evaluates arithmetic (+, -, *, /, %), string concatenation, comparisons (==, !=, <, >, <=, >=),
|
||||
* and logical operations (&&, ||, !, AND, OR, NOT) with variable substitution.
|
||||
*/
|
||||
public class MacroExpressionParser implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Object evaluate(MacroVariables vars, String expression) {
|
||||
if (expression == null) return "";
|
||||
String trimmed = expression.trim();
|
||||
if (trimmed.isEmpty() || trimmed.equals("''") || trimmed.equals("\"\"")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
Parser parser = new Parser(vars, trimmed);
|
||||
return parser.parseExpression();
|
||||
} catch (Exception e) {
|
||||
// If expression parsing fails, try simple variable resolution or return raw string
|
||||
if (vars != null) {
|
||||
return resolveText(vars, expression);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
public static String evaluateString(MacroVariables vars, String expression) {
|
||||
Object val = evaluate(vars, expression);
|
||||
return (val != null) ? val.toString() : "";
|
||||
}
|
||||
|
||||
public static int evaluateInt(MacroVariables vars, String expression) {
|
||||
Object val = evaluate(vars, expression);
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val).intValue();
|
||||
} else if (val != null) {
|
||||
try {
|
||||
return Integer.parseInt(val.toString().trim());
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
return (int) Double.parseDouble(val.toString().trim());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static double evaluateDouble(MacroVariables vars, String expression) {
|
||||
Object val = evaluate(vars, expression);
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val).doubleValue();
|
||||
} else if (val != null) {
|
||||
try {
|
||||
return Double.parseDouble(val.toString().trim());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static boolean evaluateBoolean(MacroVariables vars, String expression) {
|
||||
Object val = evaluate(vars, expression);
|
||||
if (val instanceof Boolean) {
|
||||
return (Boolean) val;
|
||||
} else if (val instanceof Number) {
|
||||
return ((Number) val).doubleValue() != 0.0;
|
||||
} else if (val != null) {
|
||||
String s = val.toString().trim();
|
||||
if (s.equalsIgnoreCase("true") || s.equals("1")) return true;
|
||||
if (s.equalsIgnoreCase("false") || s.equals("0")) return false;
|
||||
return !s.isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String resolveText(MacroVariables vars, String text) {
|
||||
if (text == null || vars == null || !text.contains("$")) {
|
||||
return text;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int len = text.length();
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '$') {
|
||||
int nextDollar = text.indexOf('$', i + 1);
|
||||
if (nextDollar > i + 1) {
|
||||
String candidate = text.substring(i, nextDollar + 1);
|
||||
if (vars.containsKey(candidate)) {
|
||||
sb.append(vars.getString(candidate));
|
||||
i = nextDollar + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Check if variable without closing dollar: $varName
|
||||
int j = i + 1;
|
||||
while (j < len && (Character.isLetterOrDigit(text.charAt(j)) || text.charAt(j) == '_' || text.charAt(j) == '-')) {
|
||||
j++;
|
||||
}
|
||||
if (j > i + 1) {
|
||||
String varName = text.substring(i, j);
|
||||
if (vars.containsKey(varName)) {
|
||||
sb.append(vars.getString(varName));
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append(c);
|
||||
i++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Parser Implementation
|
||||
// ==========================================
|
||||
|
||||
private static class Parser {
|
||||
private final MacroVariables vars;
|
||||
private final String input;
|
||||
private int pos = 0;
|
||||
private final int length;
|
||||
|
||||
Parser(MacroVariables vars, String input) {
|
||||
this.vars = vars;
|
||||
this.input = input;
|
||||
this.length = input.length();
|
||||
}
|
||||
|
||||
Object parseExpression() {
|
||||
Object res = parseLogicalOr();
|
||||
skipWhitespace();
|
||||
return res;
|
||||
}
|
||||
|
||||
private Object parseLogicalOr() {
|
||||
Object left = parseLogicalAnd();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (match("||") || matchKeyword("OR")) {
|
||||
Object right = parseLogicalAnd();
|
||||
left = toBooleanVal(left) || toBooleanVal(right);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Object parseLogicalAnd() {
|
||||
Object left = parseEquality();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (match("&&") || matchKeyword("AND")) {
|
||||
Object right = parseEquality();
|
||||
left = toBooleanVal(left) && toBooleanVal(right);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Object parseEquality() {
|
||||
Object left = parseRelational();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (match("==") || matchKeyword("EQ")) {
|
||||
Object right = parseRelational();
|
||||
left = compareEqual(left, right);
|
||||
} else if (match("!=") || match("<>") || matchKeyword("NE")) {
|
||||
Object right = parseRelational();
|
||||
left = !compareEqual(left, right);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Object parseRelational() {
|
||||
Object left = parseAdditive();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (match("<=") || matchKeyword("LE")) {
|
||||
Object right = parseAdditive();
|
||||
left = compareRelational(left, right) <= 0;
|
||||
} else if (match(">=") || matchKeyword("GE")) {
|
||||
Object right = parseAdditive();
|
||||
left = compareRelational(left, right) >= 0;
|
||||
} else if (match("<") || matchKeyword("LT")) {
|
||||
Object right = parseAdditive();
|
||||
left = compareRelational(left, right) < 0;
|
||||
} else if (match(">") || matchKeyword("GT")) {
|
||||
Object right = parseAdditive();
|
||||
left = compareRelational(left, right) > 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Object parseAdditive() {
|
||||
Object left = parseMultiplicative();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (pos < length && input.charAt(pos) == '+' && (pos + 1 >= length || input.charAt(pos + 1) != '+')) {
|
||||
pos++;
|
||||
Object right = parseMultiplicative();
|
||||
if (left instanceof String || right instanceof String) {
|
||||
left = String.valueOf(left) + String.valueOf(right);
|
||||
} else if (isDouble(left) || isDouble(right)) {
|
||||
left = toDoubleVal(left) + toDoubleVal(right);
|
||||
} else {
|
||||
left = toIntVal(left) + toIntVal(right);
|
||||
}
|
||||
} else if (pos < length && input.charAt(pos) == '-' && (pos + 1 >= length || input.charAt(pos + 1) != '-')) {
|
||||
pos++;
|
||||
Object right = parseMultiplicative();
|
||||
if (isDouble(left) || isDouble(right)) {
|
||||
left = toDoubleVal(left) - toDoubleVal(right);
|
||||
} else {
|
||||
left = toIntVal(left) - toIntVal(right);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Object parseMultiplicative() {
|
||||
Object left = parseUnary();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (pos < length && input.charAt(pos) == '*') {
|
||||
pos++;
|
||||
Object right = parseUnary();
|
||||
if (isDouble(left) || isDouble(right)) {
|
||||
left = toDoubleVal(left) * toDoubleVal(right);
|
||||
} else {
|
||||
left = toIntVal(left) * toIntVal(right);
|
||||
}
|
||||
} else if (pos < length && input.charAt(pos) == '/') {
|
||||
pos++;
|
||||
Object right = parseUnary();
|
||||
double r = toDoubleVal(right);
|
||||
if (r == 0.0) throw new ArithmeticException("Division by zero");
|
||||
if (isDouble(left) || isDouble(right)) {
|
||||
left = toDoubleVal(left) / r;
|
||||
} else {
|
||||
left = toIntVal(left) / toIntVal(right);
|
||||
}
|
||||
} else if (pos < length && input.charAt(pos) == '%') {
|
||||
pos++;
|
||||
Object right = parseUnary();
|
||||
left = toIntVal(left) % toIntVal(right);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Object parseUnary() {
|
||||
skipWhitespace();
|
||||
if (match("!") || matchKeyword("NOT")) {
|
||||
Object val = parseUnary();
|
||||
return !toBooleanVal(val);
|
||||
}
|
||||
if (pos < length && input.charAt(pos) == '+') {
|
||||
pos++;
|
||||
return parseUnary();
|
||||
}
|
||||
if (pos < length && input.charAt(pos) == '-') {
|
||||
pos++;
|
||||
Object val = parseUnary();
|
||||
if (isDouble(val)) {
|
||||
return -toDoubleVal(val);
|
||||
} else {
|
||||
return -toIntVal(val);
|
||||
}
|
||||
}
|
||||
return parsePrimary();
|
||||
}
|
||||
|
||||
private Object parsePrimary() {
|
||||
skipWhitespace();
|
||||
if (pos >= length) return "";
|
||||
|
||||
char c = input.charAt(pos);
|
||||
|
||||
// Parentheses
|
||||
if (c == '(') {
|
||||
pos++;
|
||||
Object val = parseExpression();
|
||||
skipWhitespace();
|
||||
if (pos < length && input.charAt(pos) == ')') {
|
||||
pos++;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// String literals: 'string' or "string"
|
||||
if (c == '\'' || c == '"') {
|
||||
char quote = c;
|
||||
pos++;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (pos < length) {
|
||||
char ch = input.charAt(pos);
|
||||
if (ch == '\\' && pos + 1 < length) {
|
||||
pos++;
|
||||
sb.append(input.charAt(pos));
|
||||
} else if (ch == quote) {
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
sb.append(ch);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// Variable reference: $var$ or $var
|
||||
if (c == '$') {
|
||||
pos++;
|
||||
StringBuilder varName = new StringBuilder("$");
|
||||
while (pos < length) {
|
||||
char ch = input.charAt(pos);
|
||||
if (Character.isLetterOrDigit(ch) || ch == '_' || ch == '-') {
|
||||
varName.append(ch);
|
||||
pos++;
|
||||
} else if (ch == '$') {
|
||||
varName.append('$');
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String name = varName.toString();
|
||||
if (vars != null) {
|
||||
if (vars.containsKey(name)) {
|
||||
return vars.get(name);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Number literal
|
||||
if (Character.isDigit(c) || (c == '.' && pos + 1 < length && Character.isDigit(input.charAt(pos + 1)))) {
|
||||
int start = pos;
|
||||
boolean isDec = false;
|
||||
while (pos < length && (Character.isDigit(input.charAt(pos)) || input.charAt(pos) == '.')) {
|
||||
if (input.charAt(pos) == '.') {
|
||||
if (isDec) break;
|
||||
isDec = true;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
String numStr = input.substring(start, pos);
|
||||
if (isDec) {
|
||||
return Double.parseDouble(numStr);
|
||||
} else {
|
||||
try {
|
||||
return Integer.parseInt(numStr);
|
||||
} catch (NumberFormatException e) {
|
||||
return Long.parseLong(numStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Identifiers: true, false, null, or words
|
||||
if (Character.isLetter(c) || c == '_') {
|
||||
int start = pos;
|
||||
while (pos < length && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) {
|
||||
pos++;
|
||||
}
|
||||
String word = input.substring(start, pos);
|
||||
if (word.equalsIgnoreCase("true")) return Boolean.TRUE;
|
||||
if (word.equalsIgnoreCase("false")) return Boolean.FALSE;
|
||||
if (word.equalsIgnoreCase("null")) return null;
|
||||
|
||||
// Check if it matches variable without dollar
|
||||
if (vars != null && vars.containsKey(word)) {
|
||||
return vars.get(word);
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
// Fallback character
|
||||
pos++;
|
||||
return String.valueOf(c);
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (pos < length && Character.isWhitespace(input.charAt(pos))) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean match(String prefix) {
|
||||
if (input.startsWith(prefix, pos)) {
|
||||
pos += prefix.length();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchKeyword(String keyword) {
|
||||
if (pos + keyword.length() <= length) {
|
||||
String sub = input.substring(pos, pos + keyword.length());
|
||||
if (sub.equalsIgnoreCase(keyword)) {
|
||||
int end = pos + keyword.length();
|
||||
if (end == length || !Character.isLetterOrDigit(input.charAt(end))) {
|
||||
pos = end;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isDouble(Object obj) {
|
||||
return obj instanceof Double || obj instanceof Float;
|
||||
}
|
||||
|
||||
private static int toIntVal(Object obj) {
|
||||
if (obj instanceof Number) return ((Number) obj).intValue();
|
||||
if (obj != null) {
|
||||
try {
|
||||
return Integer.parseInt(obj.toString().trim());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static double toDoubleVal(Object obj) {
|
||||
if (obj instanceof Number) return ((Number) obj).doubleValue();
|
||||
if (obj != null) {
|
||||
try {
|
||||
return Double.parseDouble(obj.toString().trim());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
private static boolean toBooleanVal(Object obj) {
|
||||
if (obj instanceof Boolean) return (Boolean) obj;
|
||||
if (obj instanceof Number) return ((Number) obj).doubleValue() != 0.0;
|
||||
if (obj != null) {
|
||||
String s = obj.toString().trim();
|
||||
if (s.equalsIgnoreCase("true") || s.equals("1")) return true;
|
||||
if (s.equalsIgnoreCase("false") || s.equals("0")) return false;
|
||||
return !s.isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean compareEqual(Object a, Object b) {
|
||||
if (a == null && b == null) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
if (a instanceof Number && b instanceof Number) {
|
||||
return Double.compare(((Number) a).doubleValue(), ((Number) b).doubleValue()) == 0;
|
||||
}
|
||||
if (a instanceof Boolean || b instanceof Boolean) {
|
||||
return toBooleanVal(a) == toBooleanVal(b);
|
||||
}
|
||||
return a.toString().equalsIgnoreCase(b.toString());
|
||||
}
|
||||
|
||||
private static int compareRelational(Object a, Object b) {
|
||||
if (a instanceof Number || b instanceof Number) {
|
||||
return Double.compare(toDoubleVal(a), toDoubleVal(b));
|
||||
}
|
||||
return String.valueOf(a).compareToIgnoreCase(String.valueOf(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package haus.nightmare.lib3270j.macro.variable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Variable management system matching IBM Host On-Demand MacroVariables.
|
||||
* Manages macro variables ($var, $var$) supporting string, integer, double,
|
||||
* boolean, and field coordinate types.
|
||||
*/
|
||||
public class MacroVariables implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static class VariableEntry implements Serializable, Cloneable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final String name;
|
||||
private final String type;
|
||||
private Object value;
|
||||
private final String initialValue;
|
||||
|
||||
public VariableEntry(String name, String type, Object value, String initialValue) {
|
||||
this.name = name;
|
||||
this.type = type.toLowerCase();
|
||||
this.value = value;
|
||||
this.initialValue = initialValue;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getType() { return type; }
|
||||
public Object getValue() { return value; }
|
||||
public void setValue(Object value) { this.value = value; }
|
||||
public String getInitialValue() { return initialValue; }
|
||||
|
||||
@Override
|
||||
public VariableEntry clone() {
|
||||
return new VariableEntry(name, type, value, initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<String, VariableEntry> variables = new LinkedHashMap<>();
|
||||
private boolean useVars = false;
|
||||
|
||||
public MacroVariables() {}
|
||||
|
||||
public static String canonicalName(String name) {
|
||||
if (name == null) return "";
|
||||
String trimmed = name.trim();
|
||||
if (trimmed.isEmpty()) return "";
|
||||
if (!trimmed.startsWith("$")) {
|
||||
trimmed = "$" + trimmed;
|
||||
}
|
||||
if (!trimmed.endsWith("$")) {
|
||||
trimmed = trimmed + "$";
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
public static boolean isValidName(String name) {
|
||||
if (name == null || name.length() < 2) return false;
|
||||
String s = name.trim();
|
||||
if (!s.startsWith("$")) return false;
|
||||
String inner = s.endsWith("$") && s.length() > 2 ? s.substring(1, s.length() - 1) : s.substring(1);
|
||||
if (inner.isEmpty()) return false;
|
||||
for (int i = 0; i < inner.length(); i++) {
|
||||
char c = inner.charAt(i);
|
||||
if (!Character.isLetterOrDigit(c) && c != '_' && c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isValidType(String type) {
|
||||
if (type == null) return false;
|
||||
String t = type.trim().toLowerCase();
|
||||
return t.equals("string") || t.equals("int") || t.equals("integer")
|
||||
|| t.equals("double") || t.equals("boolean") || t.equals("field");
|
||||
}
|
||||
|
||||
public boolean isUseVars() {
|
||||
return useVars;
|
||||
}
|
||||
|
||||
public void setUseVars(boolean useVars) {
|
||||
this.useVars = useVars;
|
||||
}
|
||||
|
||||
public void createVariable(String name, String type, String initialValue) {
|
||||
if (!isValidName(name)) {
|
||||
throw new IllegalArgumentException("Invalid macro variable name: " + name);
|
||||
}
|
||||
if (!isValidType(type)) {
|
||||
throw new IllegalArgumentException("Invalid macro variable type: " + type);
|
||||
}
|
||||
|
||||
String cName = canonicalName(name);
|
||||
String t = type.trim().toLowerCase();
|
||||
if (t.equals("int")) t = "integer";
|
||||
|
||||
Object parsedVal = parseValueForType(t, initialValue);
|
||||
variables.put(cName, new VariableEntry(cName, t, parsedVal, initialValue));
|
||||
this.useVars = true;
|
||||
}
|
||||
|
||||
private Object parseValueForType(String type, String value) {
|
||||
if (value == null) {
|
||||
switch (type) {
|
||||
case "integer": return 0;
|
||||
case "double": return 0.0;
|
||||
case "boolean": return Boolean.FALSE;
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
String v = value.trim();
|
||||
// Remove single/double quotes if string
|
||||
if (type.equals("string")) {
|
||||
if ((v.startsWith("'") && v.endsWith("'") && v.length() >= 2) ||
|
||||
(v.startsWith("\"") && v.endsWith("\"") && v.length() >= 2)) {
|
||||
return v.substring(1, v.length() - 1);
|
||||
}
|
||||
return v;
|
||||
} else if (type.equals("integer")) {
|
||||
if (v.isEmpty()) return 0;
|
||||
try {
|
||||
return Integer.parseInt(v);
|
||||
} catch (NumberFormatException e) {
|
||||
try {
|
||||
return (int) Double.parseDouble(v);
|
||||
} catch (NumberFormatException ex) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} else if (type.equals("double")) {
|
||||
if (v.isEmpty()) return 0.0;
|
||||
try {
|
||||
return Double.parseDouble(v);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0.0;
|
||||
}
|
||||
} else if (type.equals("boolean")) {
|
||||
return Boolean.parseBoolean(v);
|
||||
} else {
|
||||
// field coordinate or object
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsKey(String name) {
|
||||
return variables.containsKey(canonicalName(name));
|
||||
}
|
||||
|
||||
public VariableEntry getEntry(String name) {
|
||||
return variables.get(canonicalName(name));
|
||||
}
|
||||
|
||||
public Object get(String name) {
|
||||
VariableEntry entry = getEntry(name);
|
||||
return (entry != null) ? entry.getValue() : null;
|
||||
}
|
||||
|
||||
public String getString(String name) {
|
||||
Object val = get(name);
|
||||
return (val != null) ? val.toString() : "";
|
||||
}
|
||||
|
||||
public int getInt(String name) {
|
||||
Object val = get(name);
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val).intValue();
|
||||
} else if (val != null) {
|
||||
try {
|
||||
return Integer.parseInt(val.toString().trim());
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double getDouble(String name) {
|
||||
Object val = get(name);
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val).doubleValue();
|
||||
} else if (val != null) {
|
||||
try {
|
||||
return Double.parseDouble(val.toString().trim());
|
||||
} catch (Exception e) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public boolean getBoolean(String name) {
|
||||
Object val = get(name);
|
||||
if (val instanceof Boolean) {
|
||||
return (Boolean) val;
|
||||
} else if (val != null) {
|
||||
return Boolean.parseBoolean(val.toString().trim());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setVariable(String name, Object value) {
|
||||
String cName = canonicalName(name);
|
||||
VariableEntry entry = variables.get(cName);
|
||||
if (entry != null) {
|
||||
if (value instanceof String && !entry.getType().equals("string")) {
|
||||
entry.setValue(parseValueForType(entry.getType(), (String) value));
|
||||
} else {
|
||||
entry.setValue(value);
|
||||
}
|
||||
} else {
|
||||
// Implicit declaration
|
||||
String inferredType = "string";
|
||||
if (value instanceof Integer || value instanceof Long) inferredType = "integer";
|
||||
else if (value instanceof Double || value instanceof Float) inferredType = "double";
|
||||
else if (value instanceof Boolean) inferredType = "boolean";
|
||||
|
||||
variables.put(cName, new VariableEntry(cName, inferredType, value, String.valueOf(value)));
|
||||
}
|
||||
this.useVars = true;
|
||||
}
|
||||
|
||||
public void updateVariable(String name, Object value) {
|
||||
setVariable(name, value);
|
||||
}
|
||||
|
||||
public void removeVariable(String name) {
|
||||
variables.remove(canonicalName(name));
|
||||
}
|
||||
|
||||
public List<String> getVarNames() {
|
||||
return new ArrayList<>(variables.keySet());
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return variables.size();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
variables.clear();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
for (VariableEntry entry : variables.values()) {
|
||||
entry.setValue(parseValueForType(entry.getType(), entry.getInitialValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroVariables clone() {
|
||||
MacroVariables clone = new MacroVariables();
|
||||
clone.useVars = this.useVars;
|
||||
for (Map.Entry<String, VariableEntry> e : this.variables.entrySet()) {
|
||||
clone.variables.put(e.getKey(), e.getValue().clone());
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -1600,6 +1600,7 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
|
||||
public 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user