Phase 1
This commit is contained in:
@@ -39,6 +39,8 @@ public class Telnet3270Client {
|
|||||||
private final DataStreamProcessor dsProcessor;
|
private final DataStreamProcessor dsProcessor;
|
||||||
private final TelnetFSM fsm;
|
private final TelnetFSM fsm;
|
||||||
private final InputProcessor inputProcessor;
|
private final InputProcessor inputProcessor;
|
||||||
|
private final haus.nightmare.lib3270j.ecl.ECLPS ps;
|
||||||
|
private final haus.nightmare.lib3270j.ecl.ECLOIA oia;
|
||||||
private TelnetConnection connection;
|
private TelnetConnection connection;
|
||||||
|
|
||||||
public Telnet3270Client(ConnectionConfig config) {
|
public Telnet3270Client(ConnectionConfig config) {
|
||||||
@@ -49,6 +51,8 @@ public class Telnet3270Client {
|
|||||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||||
|
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
|
||||||
|
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
|
||||||
|
|
||||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||||
@@ -125,6 +129,18 @@ public class Telnet3270Client {
|
|||||||
/** Get the connection config. */
|
/** Get the connection config. */
|
||||||
public ConnectionConfig getConfig() { return config; }
|
public ConnectionConfig getConfig() { return config; }
|
||||||
|
|
||||||
|
/** Get the ECL Presentation Space API. */
|
||||||
|
public haus.nightmare.lib3270j.ecl.ECLPS getPS() { return ps; }
|
||||||
|
|
||||||
|
/** Get the ECL Operator Information Area API. */
|
||||||
|
public haus.nightmare.lib3270j.ecl.ECLOIA getOIA() { return oia; }
|
||||||
|
|
||||||
|
/** Get the list of all fields currently on screen. */
|
||||||
|
public haus.nightmare.lib3270j.ecl.ECLFieldList getFieldList() { return ps.getFieldList(); }
|
||||||
|
|
||||||
|
/** Send IBM ECL bracketed mnemonic keystrokes (e.g. "USER[tab]PASS[enter]"). */
|
||||||
|
public void sendKeys(String keys) { inputProcessor.sendKeys(keys); }
|
||||||
|
|
||||||
/** Set a custom or interactive TLS certificate verifier callback. */
|
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||||
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||||
config.setCertificateVerifier(verifier);
|
config.setCertificateVerifier(verifier);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common constants for IBM Host On-Demand Emulator Class Library (ECL) emulation.
|
||||||
|
*/
|
||||||
|
public interface ECLConstants {
|
||||||
|
|
||||||
|
// Presentation space logical planes
|
||||||
|
int PLANE_TEXT = 1; // Unicode/EBCDIC characters
|
||||||
|
int PLANE_COLOR = 2; // Color attributes
|
||||||
|
int PLANE_HILITE = 3; // Extended highlighting
|
||||||
|
int PLANE_EXTENDED = 4; // Extended character sets / outlining
|
||||||
|
int PLANE_FIELD = 5; // Field attribute definition bytes
|
||||||
|
int PLANE_DBCS = 6; // Double-byte character plane
|
||||||
|
|
||||||
|
// Color definitions (IBM 3279 standard 16-color palette)
|
||||||
|
char COLOR_NEUTRAL_BLACK = 0;
|
||||||
|
char COLOR_BLUE = 1;
|
||||||
|
char COLOR_RED = 2;
|
||||||
|
char COLOR_PINK = 3;
|
||||||
|
char COLOR_GREEN = 4;
|
||||||
|
char COLOR_TURQUOISE = 5;
|
||||||
|
char COLOR_YELLOW = 6;
|
||||||
|
char COLOR_NEUTRAL_WHITE = 7;
|
||||||
|
char COLOR_BLACK = 8;
|
||||||
|
char COLOR_DEEP_BLUE = 9;
|
||||||
|
char COLOR_ORANGE = 10;
|
||||||
|
char COLOR_PURPLE = 11;
|
||||||
|
char COLOR_PALE_GREEN = 12;
|
||||||
|
char COLOR_PALE_TURQUOISE= 13;
|
||||||
|
char COLOR_GREY = 14;
|
||||||
|
char COLOR_WHITE = 15;
|
||||||
|
|
||||||
|
// Highlighting attributes
|
||||||
|
char HILITE_DEFAULT = 0x00;
|
||||||
|
char HILITE_BLINK = 0xF1;
|
||||||
|
char HILITE_REVERSE = 0xF2;
|
||||||
|
char HILITE_UNDERSCORE = 0xF4;
|
||||||
|
|
||||||
|
// Search directions
|
||||||
|
int SEARCH_FORWARD = 1;
|
||||||
|
int SEARCH_BACKWARD = 2;
|
||||||
|
|
||||||
|
// OIA Input Inhibited Reason Codes
|
||||||
|
int INHIBIT_NOT_INHIBITED = 0;
|
||||||
|
int INHIBIT_SYSTEM_LOCK = 1; // X SYSTEM (Waiting for host response)
|
||||||
|
int INHIBIT_NUMERIC_ONLY = 2; // X NUMERIC (Non-numeric in numeric field)
|
||||||
|
int INHIBIT_PROTECTED_FIELD = 3; // X PROT (Attempted write into protected field)
|
||||||
|
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
|
||||||
|
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
|
||||||
|
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a discrete 3270 field within an IBM Host On-Demand Presentation Space.
|
||||||
|
* Provides programmatic inspection and modification of field text and attributes.
|
||||||
|
*/
|
||||||
|
public class ECLField {
|
||||||
|
|
||||||
|
private final ECLPS ps;
|
||||||
|
private final int startPos; // Position of field attribute byte
|
||||||
|
private final int dataStart; // First data position (startPos + 1 wrapped)
|
||||||
|
private final int endPos; // Last data position inclusive
|
||||||
|
private final int length; // Usable data character length
|
||||||
|
private final byte attribute; // Field attribute byte (FA)
|
||||||
|
|
||||||
|
public ECLField(ECLPS ps, int startPos, int dataStart, int endPos, int length, byte attribute) {
|
||||||
|
this.ps = ps;
|
||||||
|
this.startPos = startPos;
|
||||||
|
this.dataStart = dataStart;
|
||||||
|
this.endPos = endPos;
|
||||||
|
this.length = length;
|
||||||
|
this.attribute = attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Buffer address of the field attribute character. */
|
||||||
|
public int getStart() { return startPos; }
|
||||||
|
|
||||||
|
/** First buffer address of the field data (start + 1). */
|
||||||
|
public int getDataStart() { return dataStart; }
|
||||||
|
|
||||||
|
/** Last buffer address of the field data inclusive. */
|
||||||
|
public int getEnd() { return endPos; }
|
||||||
|
|
||||||
|
/** Number of data characters in the field. */
|
||||||
|
public int getLength() { return length; }
|
||||||
|
|
||||||
|
public int getStartRow() {
|
||||||
|
int cols = ps.getCols();
|
||||||
|
return cols > 0 ? startPos / cols : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStartCol() {
|
||||||
|
int cols = ps.getCols();
|
||||||
|
return cols > 0 ? startPos % cols : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEndRow() {
|
||||||
|
int cols = ps.getCols();
|
||||||
|
return cols > 0 ? endPos / cols : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEndCol() {
|
||||||
|
int cols = ps.getCols();
|
||||||
|
return cols > 0 ? endPos % cols : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isModified() {
|
||||||
|
return faIsModified(attribute & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProtected() {
|
||||||
|
return faIsProtected(attribute & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNumeric() {
|
||||||
|
return faIsNumeric(attribute & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHighIntensity() {
|
||||||
|
return faIsHigh(attribute & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHidden() {
|
||||||
|
return faIsZero(attribute & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDisplay() {
|
||||||
|
return !isHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPenSelectable() {
|
||||||
|
return faIsSelectable(attribute & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getAttribute() {
|
||||||
|
return (short) (attribute & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text contents of this field as a String.
|
||||||
|
*/
|
||||||
|
public String getText() {
|
||||||
|
if (length <= 0) return "";
|
||||||
|
return ps.getString(dataStart, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the text contents of this field.
|
||||||
|
*/
|
||||||
|
public void setText(String text) {
|
||||||
|
if (isProtected() || length <= 0) return;
|
||||||
|
ps.setText(text, dataStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selector light pen type.
|
||||||
|
* Returns '?' for selectable, '>' for selected, ' ' for space/unselectable.
|
||||||
|
*/
|
||||||
|
public char getSelectorPenType() {
|
||||||
|
if (length <= 0) return ' ';
|
||||||
|
String t = getText();
|
||||||
|
if (!t.isEmpty()) {
|
||||||
|
char first = t.charAt(0);
|
||||||
|
if (first == '?' || first == '>') return first;
|
||||||
|
}
|
||||||
|
return ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actuate lightpen selection on this field ('?' -> '>').
|
||||||
|
*/
|
||||||
|
public void selectField() {
|
||||||
|
if (isProtected() || length <= 0) return;
|
||||||
|
String t = getText();
|
||||||
|
if (!t.isEmpty() && t.charAt(0) == '?') {
|
||||||
|
setText(">" + (t.length() > 1 ? t.substring(1) : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect lightpen selection on this field ('>' -> '?').
|
||||||
|
*/
|
||||||
|
public void deSelectField() {
|
||||||
|
if (isProtected() || length <= 0) return;
|
||||||
|
String t = getText();
|
||||||
|
if (!t.isEmpty() && t.charAt(0) == '>') {
|
||||||
|
setText("?" + (t.length() > 1 ? t.substring(1) : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
|
||||||
|
startPos, endPos, length, isProtected(), isModified(), getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the collection of all 3270 fields currently present in the Presentation Space.
|
||||||
|
* Conforms to IBM ECL ECLFieldList specification.
|
||||||
|
*/
|
||||||
|
public class ECLFieldList {
|
||||||
|
|
||||||
|
private final ECLPS ps;
|
||||||
|
private final ScreenBuffer screen;
|
||||||
|
private final List<ECLField> fields = new ArrayList<>();
|
||||||
|
|
||||||
|
public ECLFieldList(ECLPS ps, ScreenBuffer screen) {
|
||||||
|
this.ps = ps;
|
||||||
|
this.screen = screen;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the field list by scanning the presentation space buffer.
|
||||||
|
*/
|
||||||
|
public synchronized void refresh() {
|
||||||
|
fields.clear();
|
||||||
|
if (screen == null || !screen.isFormatted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rows = screen.getRows();
|
||||||
|
int cols = screen.getCols();
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return;
|
||||||
|
|
||||||
|
// Collect all field attribute positions
|
||||||
|
List<Integer> faPositions = new ArrayList<>();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
faPositions.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faPositions.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numFields = faPositions.size();
|
||||||
|
for (int i = 0; i < numFields; i++) {
|
||||||
|
int startPos = faPositions.get(i);
|
||||||
|
int nextFaPos = faPositions.get((i + 1) % numFields);
|
||||||
|
|
||||||
|
int dataStart = (startPos + 1) % size;
|
||||||
|
int endPos = (nextFaPos == 0) ? size - 1 : nextFaPos - 1;
|
||||||
|
|
||||||
|
int len;
|
||||||
|
if (nextFaPos > startPos) {
|
||||||
|
len = nextFaPos - startPos - 1;
|
||||||
|
} else {
|
||||||
|
len = (size - startPos - 1) + nextFaPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte faVal = screen.getCell(startPos).fa;
|
||||||
|
fields.add(new ECLField(ps, startPos, dataStart, endPos, len, faVal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getFieldCount() {
|
||||||
|
return fields.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<ECLField> getFields() {
|
||||||
|
return Collections.unmodifiableList(new ArrayList<>(fields));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized ECLField getFirstField() {
|
||||||
|
if (fields.isEmpty()) return null;
|
||||||
|
return fields.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized ECLField getNextField(ECLField prev) {
|
||||||
|
if (prev == null || fields.isEmpty()) return getFirstField();
|
||||||
|
int idx = fields.indexOf(prev);
|
||||||
|
if (idx >= 0 && idx + 1 < fields.size()) {
|
||||||
|
return fields.get(idx + 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field that contains the specified buffer position.
|
||||||
|
*/
|
||||||
|
public synchronized ECLField findField(int pos) {
|
||||||
|
if (fields.isEmpty() || screen == null) return null;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return null;
|
||||||
|
pos = ((pos % size) + size) % size;
|
||||||
|
|
||||||
|
for (ECLField f : fields) {
|
||||||
|
int start = f.getStart();
|
||||||
|
int end = f.getEnd();
|
||||||
|
if (start <= end) {
|
||||||
|
if (pos >= start && pos <= end) return f;
|
||||||
|
} else {
|
||||||
|
// Wrapped field
|
||||||
|
if (pos >= start || pos <= end) return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field at the specified 0-indexed row and column.
|
||||||
|
*/
|
||||||
|
public ECLField findField(int row, int col) {
|
||||||
|
if (screen == null) return null;
|
||||||
|
int cols = screen.getCols();
|
||||||
|
return findField(row * cols + col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find field containing the given text string.
|
||||||
|
*/
|
||||||
|
public synchronized ECLField findField(String text, int startPos) {
|
||||||
|
if (text == null || text.isEmpty() || fields.isEmpty()) return null;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return null;
|
||||||
|
startPos = ((startPos % size) + size) % size;
|
||||||
|
|
||||||
|
// Find starting index in field list
|
||||||
|
int startIdx = 0;
|
||||||
|
for (int i = 0; i < fields.size(); i++) {
|
||||||
|
if (fields.get(i).getStart() >= startPos) {
|
||||||
|
startIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = startIdx; i < fields.size(); i++) {
|
||||||
|
ECLField f = fields.get(i);
|
||||||
|
if (f.getText().contains(text)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator Information Area (OIA) status engine matching IBM Host On-Demand ECL specification.
|
||||||
|
* Provides synchronization primitives (waitForInput, waitForSysAvailable) and status inspection.
|
||||||
|
*/
|
||||||
|
public class ECLOIA implements ECLConstants {
|
||||||
|
|
||||||
|
private final ScreenBuffer screen;
|
||||||
|
private final InputProcessor inputProcessor;
|
||||||
|
private final TelnetFSM fsm;
|
||||||
|
private final List<ECLOIANotify> listeners = new ArrayList<>();
|
||||||
|
|
||||||
|
public interface ECLOIANotify {
|
||||||
|
void onOIAChanged(ECLOIA oia);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ECLOIA(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) {
|
||||||
|
this.screen = screen;
|
||||||
|
this.inputProcessor = inputProcessor;
|
||||||
|
this.fsm = fsm;
|
||||||
|
|
||||||
|
if (inputProcessor != null) {
|
||||||
|
inputProcessor.setLockStateListener(locked -> notifyOIAChanged());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void registerOIAEvent(ECLOIANotify listener) {
|
||||||
|
if (listener != null && !listeners.contains(listener)) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void unregisterOIAEvent(ECLOIANotify listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void notifyOIAChanged() {
|
||||||
|
for (ECLOIANotify l : listeners) {
|
||||||
|
try {
|
||||||
|
l.onOIAChanged(this);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInsertMode() {
|
||||||
|
return inputProcessor != null && inputProcessor.isInsertMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNumeric() {
|
||||||
|
if (screen == null || !screen.isFormatted()) return false;
|
||||||
|
byte fa = screen.getFieldAttributeAt(screen.getCursorAddress());
|
||||||
|
return faIsNumeric(fa & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAlphanumeric() {
|
||||||
|
return !isNumeric();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMessageWaiting() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCommError() {
|
||||||
|
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current Input Inhibited code.
|
||||||
|
* Returns one of INHIBIT_* constants from ECLConstants.
|
||||||
|
*/
|
||||||
|
public int getInputInhibited() {
|
||||||
|
if (isCommError()) {
|
||||||
|
return INHIBIT_COMM_CHECK;
|
||||||
|
}
|
||||||
|
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
|
||||||
|
return INHIBIT_SYSTEM_LOCK;
|
||||||
|
}
|
||||||
|
return INHIBIT_NOT_INHIBITED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
|
||||||
|
* @return true if keyboard unlocked, false if timeout occurred.
|
||||||
|
*/
|
||||||
|
public boolean waitForInput(long timeoutMs) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||||
|
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(20);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getInputInhibited() == INHIBIT_NOT_INHIBITED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block until the host system is available.
|
||||||
|
*/
|
||||||
|
public boolean waitForSysAvailable(long timeoutMs) {
|
||||||
|
return waitForInput(timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block until application is available.
|
||||||
|
*/
|
||||||
|
public boolean waitForAppAvailable(long timeoutMs) {
|
||||||
|
return waitForInput(timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block until any OIA transition occurs.
|
||||||
|
*/
|
||||||
|
public boolean waitForTransition(long timeoutMs) {
|
||||||
|
int initialInhibit = getInputInhibited();
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||||
|
if (getInputInhibited() != initialInhibit) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(20);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getInputInhibited() != initialInhibit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presentation Space (ECLPS) implementation conforming to IBM Host On-Demand ECL specification.
|
||||||
|
* Provides multi-plane presentation buffer access, string searches, formatted field navigation,
|
||||||
|
* and automated keystroke streaming.
|
||||||
|
*/
|
||||||
|
public class ECLPS implements ECLConstants {
|
||||||
|
|
||||||
|
private final ScreenBuffer screen;
|
||||||
|
private final InputProcessor inputProcessor;
|
||||||
|
private final EbcdicTranslator translator;
|
||||||
|
private final ECLFieldList fieldList;
|
||||||
|
|
||||||
|
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
|
||||||
|
this.screen = screen;
|
||||||
|
this.inputProcessor = inputProcessor;
|
||||||
|
this.translator = translator;
|
||||||
|
this.fieldList = new ECLFieldList(this, screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScreenBuffer getScreenBuffer() { return screen; }
|
||||||
|
public InputProcessor getInputProcessor() { return inputProcessor; }
|
||||||
|
public EbcdicTranslator getTranslator() { return translator; }
|
||||||
|
public ECLFieldList getFieldList() {
|
||||||
|
fieldList.refresh();
|
||||||
|
return fieldList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSize() { return screen.getRows() * screen.getCols(); }
|
||||||
|
public int getRows() { return screen.getRows(); }
|
||||||
|
public int getCols() { return screen.getCols(); }
|
||||||
|
public int getCursorPos(){ return screen.getCursorAddress(); }
|
||||||
|
public int getCursorRow(){ return screen.getCursorRow(); }
|
||||||
|
public int getCursorCol(){ return screen.getCursorCol(); }
|
||||||
|
|
||||||
|
public void setCursorPos(int pos) {
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCursorPos(int row, int col) {
|
||||||
|
int pos = screen.rowColToAddress(row, col);
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy presentation data from a specific plane into a destination buffer.
|
||||||
|
*/
|
||||||
|
public synchronized int getPlane(int planeType, char[] destBuffer, int start, int length) {
|
||||||
|
if (destBuffer == null || screen == null || length <= 0) return 0;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return 0;
|
||||||
|
|
||||||
|
start = ((start % size) + size) % size;
|
||||||
|
int copyLen = Math.min(length, destBuffer.length);
|
||||||
|
|
||||||
|
for (int i = 0; i < copyLen; i++) {
|
||||||
|
int addr = (start + i) % size;
|
||||||
|
ExtendedAttribute ea = screen.getCell(addr);
|
||||||
|
switch (planeType) {
|
||||||
|
case PLANE_TEXT:
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
destBuffer[i] = ' ';
|
||||||
|
} else if (ea.ucs4 != 0) {
|
||||||
|
destBuffer[i] = (char) ea.ucs4;
|
||||||
|
} else if (ea.ec != 0) {
|
||||||
|
destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||||
|
} else {
|
||||||
|
destBuffer[i] = ' ';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PLANE_COLOR:
|
||||||
|
destBuffer[i] = (char) (ea.fg & 0xFF);
|
||||||
|
break;
|
||||||
|
case PLANE_HILITE:
|
||||||
|
destBuffer[i] = (char) (ea.gr & 0xFF);
|
||||||
|
break;
|
||||||
|
case PLANE_EXTENDED:
|
||||||
|
destBuffer[i] = (char) (ea.cs & 0xFF);
|
||||||
|
break;
|
||||||
|
case PLANE_FIELD:
|
||||||
|
destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
destBuffer[i] = ' ';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copyLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string of characters from the presentation space starting at address pos.
|
||||||
|
*/
|
||||||
|
public synchronized String getString(int pos, int length) {
|
||||||
|
if (length <= 0) return "";
|
||||||
|
char[] buf = new char[length];
|
||||||
|
getPlane(PLANE_TEXT, buf, pos, length);
|
||||||
|
return new String(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string of characters from the presentation space starting at row/column.
|
||||||
|
*/
|
||||||
|
public String getString(int row, int col, int length) {
|
||||||
|
int pos = screen.rowColToAddress(row, col);
|
||||||
|
return getString(pos, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert text directly into unprotected fields in the presentation space starting at pos.
|
||||||
|
*/
|
||||||
|
public synchronized void setText(String text, int pos) {
|
||||||
|
if (text == null || text.isEmpty() || screen == null) return;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
|
||||||
|
pos = ((pos % size) + size) % size;
|
||||||
|
setCursorPos(pos);
|
||||||
|
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
inputProcessor.typeCharacter(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert text starting at row and column.
|
||||||
|
*/
|
||||||
|
public void setText(String text, int row, int col) {
|
||||||
|
int pos = screen.rowColToAddress(row, col);
|
||||||
|
setText(text, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for a string in the presentation space.
|
||||||
|
* Returns 1-based or 0-based position, or -1 if not found.
|
||||||
|
*/
|
||||||
|
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
|
||||||
|
if (target == null || target.isEmpty() || screen == null) return -1;
|
||||||
|
int rows = screen.getRows();
|
||||||
|
int cols = screen.getCols();
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return -1;
|
||||||
|
|
||||||
|
int startPos = (startRow * cols + startCol) % size;
|
||||||
|
char[] fullScreen = new char[size];
|
||||||
|
getPlane(PLANE_TEXT, fullScreen, 0, size);
|
||||||
|
String screenText = new String(fullScreen);
|
||||||
|
|
||||||
|
if (ignoreCase) {
|
||||||
|
screenText = screenText.toLowerCase();
|
||||||
|
target = target.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetLen = target.length();
|
||||||
|
if (dir == SEARCH_FORWARD) {
|
||||||
|
// Forward search with wrapping
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
int pos = (startPos + i) % size;
|
||||||
|
if (matchesAt(screenText, target, pos, size)) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Backward search with wrapping
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
int pos = (startPos - i + size) % size;
|
||||||
|
if (matchesAt(screenText, target, pos, size)) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesAt(String screenText, String target, int pos, int size) {
|
||||||
|
int len = target.length();
|
||||||
|
for (int j = 0; j < len; j++) {
|
||||||
|
int charPos = (pos + j) % size;
|
||||||
|
if (screenText.charAt(charPos) != target.charAt(j)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paste text with line wrapping across unprotected fields.
|
||||||
|
*/
|
||||||
|
public synchronized int pasteLineWrap(String text, int startPos, int endCol, boolean wordWrap) {
|
||||||
|
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||||
|
int cols = screen.getCols();
|
||||||
|
int rows = screen.getRows();
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return 0;
|
||||||
|
|
||||||
|
setCursorPos(startPos);
|
||||||
|
int charsPasted = 0;
|
||||||
|
String[] lines = text.split("\r?\n");
|
||||||
|
|
||||||
|
for (int l = 0; l < lines.length; l++) {
|
||||||
|
String line = lines[l];
|
||||||
|
for (int i = 0; i < line.length(); i++) {
|
||||||
|
int curPos = screen.getCursorAddress();
|
||||||
|
int curCol = curPos % cols;
|
||||||
|
if (endCol > 0 && curCol >= endCol) {
|
||||||
|
// Advance to next row
|
||||||
|
int nextRow = (curPos / cols + 1) % rows;
|
||||||
|
setCursorPos(nextRow * cols);
|
||||||
|
}
|
||||||
|
inputProcessor.typeCharacter(line.charAt(i));
|
||||||
|
charsPasted++;
|
||||||
|
}
|
||||||
|
if (l < lines.length - 1) {
|
||||||
|
// Newline key between lines
|
||||||
|
inputProcessor.newline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return charsPasted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send standard IBM ECL mnemonic keystrokes.
|
||||||
|
*/
|
||||||
|
public void sendKeys(String keys) {
|
||||||
|
if (inputProcessor != null) {
|
||||||
|
inputProcessor.sendKeys(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -863,6 +863,204 @@ public class InputProcessor {
|
|||||||
return fieldLen;
|
return fieldLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and execute IBM ECL standard mnemonic keystrokes (e.g. "USER[tab]PASS[enter]").
|
||||||
|
*/
|
||||||
|
public void sendKeys(String keys) {
|
||||||
|
if (keys == null || keys.isEmpty()) return;
|
||||||
|
|
||||||
|
int len = keys.length();
|
||||||
|
int i = 0;
|
||||||
|
while (i < len) {
|
||||||
|
char c = keys.charAt(i);
|
||||||
|
if (c == '[') {
|
||||||
|
int end = keys.indexOf(']', i);
|
||||||
|
if (end > i) {
|
||||||
|
String token = keys.substring(i + 1, end).trim().toLowerCase();
|
||||||
|
executeMnemonicToken(token);
|
||||||
|
i = end + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typeCharacter(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeMnemonicToken(String token) {
|
||||||
|
switch (token) {
|
||||||
|
case "enter":
|
||||||
|
case "return":
|
||||||
|
sendAid(AID_ENTER);
|
||||||
|
break;
|
||||||
|
case "clear":
|
||||||
|
sendAid(AID_CLEAR);
|
||||||
|
break;
|
||||||
|
case "tab":
|
||||||
|
tab();
|
||||||
|
break;
|
||||||
|
case "backtab":
|
||||||
|
case "btab":
|
||||||
|
backTab();
|
||||||
|
break;
|
||||||
|
case "newline":
|
||||||
|
case "nl":
|
||||||
|
newline();
|
||||||
|
break;
|
||||||
|
case "home":
|
||||||
|
cursorHome();
|
||||||
|
break;
|
||||||
|
case "up":
|
||||||
|
case "curup":
|
||||||
|
cursorUp();
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
case "curdown":
|
||||||
|
cursorDown();
|
||||||
|
break;
|
||||||
|
case "left":
|
||||||
|
case "curleft":
|
||||||
|
cursorLeft();
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
case "curright":
|
||||||
|
cursorRight();
|
||||||
|
break;
|
||||||
|
case "eraseeof":
|
||||||
|
case "erase_eof":
|
||||||
|
eraseEof();
|
||||||
|
break;
|
||||||
|
case "eraseinp":
|
||||||
|
case "erase_input":
|
||||||
|
eraseInput();
|
||||||
|
break;
|
||||||
|
case "dup":
|
||||||
|
dup();
|
||||||
|
break;
|
||||||
|
case "fm":
|
||||||
|
case "fieldmark":
|
||||||
|
fieldMark();
|
||||||
|
break;
|
||||||
|
case "attn":
|
||||||
|
attn();
|
||||||
|
break;
|
||||||
|
case "sysreq":
|
||||||
|
sysReq();
|
||||||
|
break;
|
||||||
|
case "reset":
|
||||||
|
reset();
|
||||||
|
break;
|
||||||
|
case "insert":
|
||||||
|
setInsertMode(!isInsertMode());
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
deleteChar();
|
||||||
|
break;
|
||||||
|
case "backspace":
|
||||||
|
case "bs":
|
||||||
|
backspace();
|
||||||
|
break;
|
||||||
|
case "wordtab":
|
||||||
|
processWordTab(true);
|
||||||
|
break;
|
||||||
|
case "wordbacktab":
|
||||||
|
processWordTab(false);
|
||||||
|
break;
|
||||||
|
case "deleteword":
|
||||||
|
processDeleteWord();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (token.startsWith("pf")) {
|
||||||
|
try {
|
||||||
|
int pfNum = Integer.parseInt(token.substring(2));
|
||||||
|
if (pfNum >= 1 && pfNum <= 24) {
|
||||||
|
sendAid(AID_PF1 + (pfNum - 1));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
} else if (token.startsWith("pa")) {
|
||||||
|
try {
|
||||||
|
int paNum = Integer.parseInt(token.substring(2));
|
||||||
|
if (paNum >= 1 && paNum <= 3) {
|
||||||
|
sendAid(AID_PA1 + (paNum - 1));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jump cursor to next or previous word boundary.
|
||||||
|
*/
|
||||||
|
public void processWordTab(boolean forward) {
|
||||||
|
if (keyboardLocked || screen == null) return;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
|
||||||
|
int cur = screen.getCursorAddress();
|
||||||
|
if (forward) {
|
||||||
|
// Find next whitespace then next non-whitespace
|
||||||
|
int pos = screen.incrementAddress(cur);
|
||||||
|
int count = 0;
|
||||||
|
while (count < size && !isWhitespaceOrNull(pos)) {
|
||||||
|
pos = screen.incrementAddress(pos);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
while (count < size && isWhitespaceOrNull(pos)) {
|
||||||
|
pos = screen.incrementAddress(pos);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
} else {
|
||||||
|
// Find previous non-whitespace after whitespace
|
||||||
|
int pos = screen.decrementAddress(cur);
|
||||||
|
int count = 0;
|
||||||
|
while (count < size && isWhitespaceOrNull(pos)) {
|
||||||
|
pos = screen.decrementAddress(pos);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
while (count < size && !isWhitespaceOrNull(screen.decrementAddress(pos))) {
|
||||||
|
pos = screen.decrementAddress(pos);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
}
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the word starting at the cursor position.
|
||||||
|
*/
|
||||||
|
public void processDeleteWord() {
|
||||||
|
if (keyboardLocked || screen == null) return;
|
||||||
|
int cur = screen.getCursorAddress();
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
byte fa = screen.getFieldAttributeAt(cur);
|
||||||
|
if (faIsProtected(fa & 0xFF)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
while (count < size && !isWhitespaceOrNull(screen.getCursorAddress())) {
|
||||||
|
deleteChar();
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
// Also delete trailing spaces
|
||||||
|
while (count < size && isWhitespaceOrNull(screen.getCursorAddress()) && screen.getCell(screen.getCursorAddress()).ec != 0) {
|
||||||
|
deleteChar();
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWhitespaceOrNull(int pos) {
|
||||||
|
if (screen == null) return true;
|
||||||
|
ExtendedAttribute ea = screen.getCell(pos);
|
||||||
|
if (ea.isFieldAttribute()) return true;
|
||||||
|
int ec = ea.ec & 0xFF;
|
||||||
|
return ec == 0 || ec == 0x40; // Null or EBCDIC Space
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a structured field response directly.
|
* Send a structured field response directly.
|
||||||
* Used by DFT mode file transfer.
|
* Used by DFT mode file transfer.
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
public class ECLFieldListTest {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private InputProcessor input;
|
||||||
|
private ECLPS ps;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||||
|
input = new InputProcessor(screen, translator, null);
|
||||||
|
ps = new ECLPS(screen, input, translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFieldListDetection() {
|
||||||
|
// Create 2 fields: pos 0 (unprotected, 9 chars), pos 10 (protected, 19 chars), pos 30 (high intensity)
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
|
||||||
|
|
||||||
|
ECLFieldList list = ps.getFieldList();
|
||||||
|
assertEquals(3, list.getFieldCount());
|
||||||
|
|
||||||
|
ECLField f1 = list.getFirstField();
|
||||||
|
assertNotNull(f1);
|
||||||
|
assertEquals(0, f1.getStart());
|
||||||
|
assertEquals(1, f1.getDataStart());
|
||||||
|
assertEquals(9, f1.getEnd());
|
||||||
|
assertEquals(9, f1.getLength());
|
||||||
|
assertFalse(f1.isProtected());
|
||||||
|
|
||||||
|
ECLField f2 = list.getNextField(f1);
|
||||||
|
assertNotNull(f2);
|
||||||
|
assertEquals(10, f2.getStart());
|
||||||
|
assertEquals(11, f2.getDataStart());
|
||||||
|
assertEquals(29, f2.getEnd());
|
||||||
|
assertEquals(19, f2.getLength());
|
||||||
|
assertTrue(f2.isProtected());
|
||||||
|
|
||||||
|
ECLField f3 = list.getNextField(f2);
|
||||||
|
assertNotNull(f3);
|
||||||
|
assertEquals(30, f3.getStart());
|
||||||
|
assertTrue(f3.isHighIntensity());
|
||||||
|
assertTrue(f3.isPenSelectable());
|
||||||
|
|
||||||
|
assertNull(list.getNextField(f3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindFieldByPositionAndRowCol() {
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
|
||||||
|
ECLFieldList list = ps.getFieldList();
|
||||||
|
ECLField found1 = list.findField(40);
|
||||||
|
assertNotNull(found1);
|
||||||
|
assertEquals(0, found1.getStart());
|
||||||
|
|
||||||
|
ECLField found2 = list.findField(1, 10); // Row 1, Col 10 -> addr 90
|
||||||
|
assertNotNull(found2);
|
||||||
|
assertEquals(80, found2.getStart());
|
||||||
|
assertTrue(found2.isProtected());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLightpenSelectionOnField() {
|
||||||
|
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
|
||||||
|
screen.setCell(1, translator.unicodeToEbcdic('?'));
|
||||||
|
screen.setCell(2, translator.unicodeToEbcdic('O'));
|
||||||
|
screen.setCell(3, translator.unicodeToEbcdic('K'));
|
||||||
|
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
screen.translateToUnicode();
|
||||||
|
|
||||||
|
ECLField field = ps.getFieldList().getFirstField();
|
||||||
|
assertEquals('?', field.getSelectorPenType());
|
||||||
|
|
||||||
|
field.selectField();
|
||||||
|
assertEquals('>', field.getSelectorPenType());
|
||||||
|
|
||||||
|
field.deSelectField();
|
||||||
|
assertEquals('?', field.getSelectorPenType());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
|
||||||
|
|
||||||
|
public class ECLOIATest {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private InputProcessor input;
|
||||||
|
private ECLOIA oia;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||||
|
input = new InputProcessor(screen, translator, null);
|
||||||
|
oia = new ECLOIA(screen, input, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInhibitStateTransitions() {
|
||||||
|
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||||
|
assertTrue(oia.waitForInput(100));
|
||||||
|
|
||||||
|
input.setKeyboardLocked(true);
|
||||||
|
assertEquals(INHIBIT_SYSTEM_LOCK, oia.getInputInhibited());
|
||||||
|
assertFalse(oia.waitForInput(50));
|
||||||
|
|
||||||
|
input.setKeyboardLocked(false);
|
||||||
|
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||||
|
assertTrue(oia.waitForInput(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOIAEventListener() {
|
||||||
|
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||||
|
oia.registerOIAEvent(changedOia -> triggered.set(true));
|
||||||
|
|
||||||
|
input.setKeyboardLocked(true);
|
||||||
|
assertTrue(triggered.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInsertModeState() {
|
||||||
|
assertFalse(oia.isInsertMode());
|
||||||
|
input.setInsertMode(true);
|
||||||
|
assertTrue(oia.isInsertMode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
public class ECLPSTest implements ECLConstants {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private InputProcessor input;
|
||||||
|
private ECLPS ps;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||||
|
input = new InputProcessor(screen, translator, null);
|
||||||
|
ps = new ECLPS(screen, input, translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGeometry() {
|
||||||
|
assertEquals(24, ps.getRows());
|
||||||
|
assertEquals(80, ps.getCols());
|
||||||
|
assertEquals(24 * 80, ps.getSize());
|
||||||
|
assertEquals(0, ps.getCursorPos());
|
||||||
|
|
||||||
|
ps.setCursorPos(10, 20);
|
||||||
|
assertEquals(10 * 80 + 20, ps.getCursorPos());
|
||||||
|
assertEquals(10, ps.getCursorRow());
|
||||||
|
assertEquals(20, ps.getCursorCol());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlaneTextExtractionAndSearch() {
|
||||||
|
// Set some text in buffer
|
||||||
|
screen.setCell(0, 0xC8); // 'H'
|
||||||
|
screen.setCell(1, 0xC5); // 'E'
|
||||||
|
screen.setCell(2, 0xD3); // 'L'
|
||||||
|
screen.setCell(3, 0xD3); // 'L'
|
||||||
|
screen.setCell(4, 0xD6); // 'O'
|
||||||
|
screen.translateToUnicode();
|
||||||
|
|
||||||
|
assertEquals("HELLO", ps.getString(0, 5));
|
||||||
|
|
||||||
|
// Search string forward
|
||||||
|
int pos = ps.searchString("ELL", 0, 0, SEARCH_FORWARD, false);
|
||||||
|
assertEquals(1, pos);
|
||||||
|
|
||||||
|
// Search string case-insensitive
|
||||||
|
int posLower = ps.searchString("hello", 0, 0, SEARCH_FORWARD, true);
|
||||||
|
assertEquals(0, posLower);
|
||||||
|
|
||||||
|
// Search not found
|
||||||
|
int notFound = ps.searchString("WORLD", 0, 0, SEARCH_FORWARD, false);
|
||||||
|
assertEquals(-1, notFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiPlaneReading() {
|
||||||
|
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
screen.setCell(1, 0xC1); // 'A'
|
||||||
|
screen.getCell(1).fg = COLOR_RED;
|
||||||
|
screen.getCell(1).gr = (byte) HILITE_BLINK;
|
||||||
|
|
||||||
|
char[] textPlane = new char[2];
|
||||||
|
char[] colorPlane = new char[2];
|
||||||
|
char[] hilitePlane = new char[2];
|
||||||
|
char[] fieldPlane = new char[2];
|
||||||
|
|
||||||
|
ps.getPlane(PLANE_TEXT, textPlane, 0, 2);
|
||||||
|
ps.getPlane(PLANE_COLOR, colorPlane, 0, 2);
|
||||||
|
ps.getPlane(PLANE_HILITE, hilitePlane, 0, 2);
|
||||||
|
ps.getPlane(PLANE_FIELD, fieldPlane, 0, 2);
|
||||||
|
|
||||||
|
assertEquals(' ', textPlane[0]); // FA is space in text plane
|
||||||
|
assertEquals('A', textPlane[1]);
|
||||||
|
|
||||||
|
assertEquals(0, colorPlane[0]);
|
||||||
|
assertEquals(COLOR_RED, colorPlane[1]);
|
||||||
|
|
||||||
|
assertEquals(0, hilitePlane[0]);
|
||||||
|
assertEquals((char) (HILITE_BLINK & 0xFF), hilitePlane[1]);
|
||||||
|
|
||||||
|
assertTrue((fieldPlane[0] & 0xFF) != 0);
|
||||||
|
assertEquals(0, fieldPlane[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSendKeysMnemonics() {
|
||||||
|
// Create an unprotected field at pos 0
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
ps.sendKeys("IBM[tab]");
|
||||||
|
assertEquals('I', screen.getCell(1).ucs4);
|
||||||
|
assertEquals('B', screen.getCell(2).ucs4);
|
||||||
|
assertEquals('M', screen.getCell(3).ucs4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasteLineWrap() {
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
int count = ps.pasteLineWrap("ABC\nDEF", 1, 80, false);
|
||||||
|
assertEquals(6, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user