Add facade classes for 3rd party programs
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m18s

This commit is contained in:
2026-09-18 16:48:49 +00:00
parent badd026055
commit 5049d8df74
70 changed files with 6687 additions and 354 deletions
@@ -56,4 +56,51 @@ public class ECLOIATest {
input.setInsertMode(true);
assertTrue(oia.isInsertMode());
}
@Test
public void testXSystemAndXProtTransitionsAfterReset() {
input.setOIA(oia);
// 1. Initial ready state
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertFalse(oia.isXSystem());
assertFalse(oia.isXProt());
// 2. Clear / reset called explicitly (as on connect or packet receipt)
oia.setInputInhibited(INHIBIT_NOT_INHIBITED);
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertFalse(oia.isXSystem());
// 3. User sends AID / locks keyboard -> MUST trigger X SYSTEM
input.setKeyboardLocked(true);
assertTrue(oia.isXSystem(), "X SYSTEM must be triggered when keyboard is locked");
assertEquals(INHIBIT_SYSTEM_LOCK, oia.getInputInhibited());
assertEquals("X-SYSTEM", oia.getStatusString());
// 4. Host unlocks keyboard
input.setKeyboardLocked(false);
assertFalse(oia.isXSystem());
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertEquals("READY", oia.getStatusString());
// 5. User types in protected field -> triggers X PROT
oia.setInputInhibited(INHIBIT_PROTECTED_FIELD);
assertTrue(oia.isXProt(), "X PROT must be triggered");
assertFalse(oia.isXSystem());
assertEquals(INHIBIT_PROTECTED_FIELD, oia.getInputInhibited());
assertEquals("X-PROT", oia.getStatusString());
// 6. User presses Reset
input.reset();
assertFalse(input.isKeyboardLocked());
assertFalse(oia.isXProt());
assertFalse(oia.isXSystem());
assertEquals(INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
// 7. Subsequent AID send -> MUST trigger X SYSTEM again
input.setKeyboardLocked(true);
assertTrue(oia.isXSystem(), "X SYSTEM must trigger on subsequent lock after reset");
assertEquals(INHIBIT_SYSTEM_LOCK, oia.getInputInhibited());
assertEquals("X-SYSTEM", oia.getStatusString());
}
}
@@ -0,0 +1,291 @@
package haus.nightmare.lib3270j.ecl;
import com.ibm.eNetwork.ECL.ECLConstants;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLHostPrintSession;
import com.ibm.eNetwork.ECL.ECLSession;
import com.ibm.eNetwork.ECL.ECLPS;
import com.ibm.eNetwork.ECL.ECLOIA;
import com.ibm.eNetwork.ECL.ECLConnection;
import com.ibm.eNetwork.ECL.ECLField;
import com.ibm.eNetwork.ECL.ECLFieldList;
import com.ibm.eNetwork.ECL.ECLScreenDesc;
import com.ibm.eNetwork.ECL.ECLScreenReco;
import com.ibm.eNetwork.ECL.ECLXfer;
import com.ibm.eNetwork.ECL.event.ECLPSEvent;
import com.ibm.eNetwork.ECL.event.ECLPSListener;
import com.ibm.eNetwork.ECL.ECLOIANotify;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Targeted Unit Tests for Phase 22: HACL com.ibm.eNetwork.ECL.* Drop-In API Facades.
*/
public class Phase22HaclFacadesTest {
private ECLSession session;
private ECLPS ps;
private ECLOIA oia;
private ECLConnection conn;
@BeforeEach
public void setUp() throws ECLErr {
Properties props = new Properties();
props.setProperty(ECLSession.SESSION_HOST, "mainframe.test.local");
props.setProperty(ECLSession.SESSION_PORT, "23");
props.setProperty(ECLSession.SESSION_CODE_PAGE, "037");
session = new ECLSession(props);
ps = session.GetPS();
oia = session.GetOIA();
conn = session.GetConnection();
}
@Test
public void testFacadeInstantiationAndNamespaceIntegrity() {
assertNotNull(session, "ECLSession should be created");
assertNotNull(ps, "ECLPS facade should be non-null");
assertNotNull(oia, "ECLOIA facade should be non-null");
assertNotNull(conn, "ECLConnection facade should be non-null");
assertNotNull(session.GetXfer(), "ECLXfer facade should be non-null");
assertNotNull(session.GetScreenReco(), "ECLScreenReco facade should be non-null");
// Constants checking
assertEquals(1, ECLConstants.PLANE_TEXT);
assertEquals(2, ECLConstants.PLANE_COLOR);
assertEquals(4, ECLConstants.PLANE_EXTENDED);
assertEquals(1, ECLConstants.SEARCH_FORWARD);
assertEquals(2, ECLConstants.SEARCH_BACKWARD);
// Connection metadata
assertEquals("mainframe.test.local", conn.GetHost());
assertEquals(23, conn.GetPort());
}
@Test
public void test1BasedCoordinateConversion() throws ECLErr {
assertEquals(24, ps.GetRows());
assertEquals(80, ps.GetCols());
assertEquals(1920, ps.GetSize());
// 1-based pos to row/col
// pos 1 -> row 1, col 1
assertEquals(1, ps.ConvertPosToRow(1));
assertEquals(1, ps.ConvertPosToCol(1));
assertEquals(1, ps.ConvertRowColToPos(1, 1));
// pos 80 -> row 1, col 80
assertEquals(1, ps.ConvertPosToRow(80));
assertEquals(80, ps.ConvertPosToCol(80));
assertEquals(80, ps.ConvertRowColToPos(1, 80));
// pos 81 -> row 2, col 1
assertEquals(2, ps.ConvertPosToRow(81));
assertEquals(1, ps.ConvertPosToCol(81));
assertEquals(81, ps.ConvertRowColToPos(2, 1));
// pos 1920 -> row 24, col 80
assertEquals(24, ps.ConvertPosToRow(1920));
assertEquals(80, ps.ConvertPosToCol(1920));
assertEquals(1920, ps.ConvertRowColToPos(24, 80));
// ConvertPosToRowCol int[] array
int[] rowCol = new int[2];
ps.ConvertPosToRowCol(81, rowCol);
assertEquals(2, rowCol[0]);
assertEquals(1, rowCol[1]);
// Out of bounds pos checks
assertThrows(ECLErr.class, () -> ps.ConvertPosToRow(0));
assertThrows(ECLErr.class, () -> ps.ConvertPosToRow(1921));
assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(0, 1));
assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(25, 1));
assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(1, 0));
assertThrows(ECLErr.class, () -> ps.ConvertRowColToPos(1, 81));
}
@Test
public void testCursorPositioning1Based() throws ECLErr {
// Initial cursor at pos 1 (row 1, col 1)
ps.SetCursorPos(1);
assertEquals(1, ps.GetCursorPos());
assertEquals(1, ps.GetCursorRow());
assertEquals(1, ps.GetCursorCol());
// Set by pos
ps.SetCursorPos(85);
assertEquals(85, ps.GetCursorPos());
assertEquals(2, ps.GetCursorRow());
assertEquals(5, ps.GetCursorCol());
// Set by row and col
ps.SetCursorPos(5, 10);
assertEquals(330, ps.GetCursorPos()); // (5-1)*80 + 10 = 330
assertEquals(5, ps.GetCursorRow());
assertEquals(10, ps.GetCursorCol());
// Invalid cursor pos throws ECLErr
assertThrows(ECLErr.class, () -> ps.SetCursorPos(0));
assertThrows(ECLErr.class, () -> ps.SetCursorPos(1921));
assertThrows(ECLErr.class, () -> ps.SetCursorPos(0, 10));
assertThrows(ECLErr.class, () -> ps.SetCursorPos(25, 10));
}
@Test
public void testGetAndSetString1Based() throws ECLErr {
// Write text at 1-based pos 1
ps.SetString("LOGON APPLID(CICS01)", 1);
assertEquals("LOGON APPLID(CICS01)", ps.GetString(1, 20));
// GetString with char buffer
char[] buf = new char[5];
int read = ps.GetString(buf, 5, 1);
assertEquals(5, read);
assertEquals("LOGON", new String(buf));
// Write text by (row, col)
ps.SetString("PASSWORD", 2, 1);
assertEquals("PASSWORD", ps.GetString(81, 8));
assertEquals("PASSWORD", ps.GetString(2, 1, 8));
// Bounds checks
assertThrows(ECLErr.class, () -> ps.GetString(0, 5));
assertThrows(ECLErr.class, () -> ps.GetString(1921, 5));
assertThrows(ECLErr.class, () -> ps.SetString("TEST", 0));
assertThrows(ECLErr.class, () -> ps.SetString("TEST", 1921));
}
@Test
public void testSearchText1Based() throws ECLErr {
ps.SetString("ENTER USERID:", 1, 1);
ps.SetString("ENTER PASSWORD:", 2, 1);
// Forward search from pos 1
int found1 = ps.SearchText("USERID", ECLConstants.SEARCH_FORWARD, 1);
assertEquals(7, found1); // "ENTER " is 6 chars, USERID starts at 7
// Forward search for PASSWORD
int found2 = ps.SearchText("PASSWORD", ECLConstants.SEARCH_FORWARD, 1);
assertEquals(87, found2); // 80 + 7 = 87
// Backward search
int foundBack = ps.SearchText("USERID", ECLConstants.SEARCH_BACKWARD, 100);
assertEquals(7, foundBack);
// Not found returns 0
assertEquals(0, ps.SearchText("NOTPRESENT", ECLConstants.SEARCH_FORWARD, 1));
}
@Test
public void testSendKeysAndMnemonics() throws ECLErr {
// Verify send keys mnemonics array
String[] mnemonics = ps.GetSendKeyMnemonics();
assertNotNull(mnemonics);
assertTrue(mnemonics.length > 20);
boolean hasEnter = false;
boolean hasClear = false;
boolean hasTab = false;
for (String m : mnemonics) {
if ("[enter]".equals(m)) hasEnter = true;
if ("[clear]".equals(m)) hasClear = true;
if ("[tab]".equals(m)) hasTab = true;
}
assertTrue(hasEnter, "Must have [enter] mnemonic");
assertTrue(hasClear, "Must have [clear] mnemonic");
assertTrue(hasTab, "Must have [tab] mnemonic");
// SendKeys to presentation space
ps.SendKeys("ABC[enter]");
// Verify buffer got ABC
assertEquals("ABC", ps.GetString(1, 3));
}
@Test
public void testPSEventListenerDispatching() throws Exception {
AtomicBoolean updatedCalled = new AtomicBoolean(false);
AtomicInteger eventTypeSeen = new AtomicInteger(-1);
ECLPSListener listener = new ECLPSListener() {
@Override
public void PSNotifyEvent(ECLPSEvent event) {
updatedCalled.set(true);
eventTypeSeen.set(event.GetType());
}
};
ps.RegisterPSEvent(listener);
// Update presentation space
ps.SetString("TESTING NOTIFY", 1);
// Dispatch notifications through delegate
ps.getDelegate().notifyPSUpdate(1, 1, 1, 14, false);
assertTrue(updatedCalled.get(), "PSNotifyEvent should be invoked");
// Unregister
ps.UnregisterPSEvent(listener);
updatedCalled.set(false);
ps.getDelegate().notifyPSUpdate(1, 1, 1, 14, false);
assertFalse(updatedCalled.get(), "PSNotifyEvent should not be called after Unregister");
}
@Test
public void testOIANotificationsAndStatus() throws Exception {
assertNotNull(oia);
assertTrue(oia.IsCommError(), "IsCommError should be true when not connected");
assertFalse(oia.IsInsertMode());
assertTrue(oia.IsKeyLocked(), "IsKeyLocked should be true when disconnected");
// Clear inhibit override
oia.getDelegate().setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
assertFalse(oia.IsKeyLocked(), "IsKeyLocked should be false when not inhibited");
AtomicBoolean oiaNotified = new AtomicBoolean(false);
ECLOIANotify notify = (o, reason, state, text) -> {
oiaNotified.set(true);
};
oia.RegisterOIAEvent(notify);
oia.getDelegate().notifyOIAChanged();
assertTrue(oiaNotified.get(), "OIA notification callback should be invoked");
oia.UnregisterOIAEvent(notify);
}
@Test
public void testECLFieldAndFieldList1Based() throws ECLErr {
// Unformatted screen field list
ECLFieldList fl = ps.GetFieldList();
assertNotNull(fl);
// Screen Desc & Screen Reco
ECLScreenDesc desc = new ECLScreenDesc();
desc.AddString("ENTER USERID:", 1, 1);
ps.SetString("ENTER USERID:", 1, 1);
assertTrue(desc.Matches(ps.getDelegate(), oia.getDelegate()), "Matches ScreenDesc");
ECLScreenReco reco = session.GetScreenReco();
assertNotNull(reco);
assertTrue(reco.IsMatch(desc), "ScreenReco matches descriptor");
}
@Test
public void testHostPrintSessionInstantiation() throws ECLErr {
Properties printProps = new Properties();
printProps.setProperty(ECLSession.SESSION_HOST, "printhost.test.local");
printProps.setProperty(ECLSession.SESSION_PORT, "515");
ECLHostPrintSession printSession = new ECLHostPrintSession(printProps);
assertNotNull(printSession);
assertEquals("printhost.test.local", printSession.GetHost());
assertEquals(515, printSession.GetPort());
}
}
@@ -0,0 +1,324 @@
package haus.nightmare.lib3270j.ecl;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.ecl.screenreco.*;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* Phase 23 test suite: Advanced Screen Recognition (ECLScreenDesc) Composite Engine & XML Schema.
*/
public class Phase23ScreenRecoCompositeTest {
private ScreenBuffer screen;
private InputProcessor inputProcessor;
private EbcdicTranslator translator;
private ECLPS ps;
private ECLOIA oia;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator("037");
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
inputProcessor = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, inputProcessor, translator);
oia = new ECLOIA(screen, inputProcessor, null);
}
@Test
@DisplayName("23.1: ECLSDString absolute, rect, case-sense, wrap, and invert matching")
public void testECLSDStringMatching() {
ps.SetText("TSO/E LOGON", 1, 35);
ps.SetText("ENTER USERID", 5, 10);
// 1. Absolute position matching at row 1, col 35
ECLSDString sd1 = new ECLSDString("TSO/E LOGON", 1, 35, true);
assertTrue(sd1.matches(ps, oia));
// Case-insensitive match
ECLSDString sdCaseInsensitive = new ECLSDString("tso/e logon", 1, 35, false);
assertTrue(sdCaseInsensitive.matches(ps, oia));
// Case-sensitive mismatch
ECLSDString sdCaseSensitiveMismatch = new ECLSDString("tso/e logon", 1, 35, true);
assertFalse(sdCaseSensitiveMismatch.matches(ps, oia));
// Invert match on mismatch -> true
sdCaseSensitiveMismatch.setInvertMatch(true);
assertTrue(sdCaseSensitiveMismatch.matches(ps, oia));
// 2. Rectangular region matching
ECLSDString sdRect = new ECLSDString("USERID", 4, 1, 6, 80, false, false);
assertTrue(sdRect.matches(ps, oia));
ECLSDString sdRectMiss = new ECLSDString("UNKNOWN", 4, 1, 6, 80, false, false);
assertFalse(sdRectMiss.matches(ps, oia));
// 3. Search whole screen (row=0, col=0)
ECLSDString sdWholeScreen = new ECLSDString("LOGON");
assertTrue(sdWholeScreen.matches(ps, oia));
// 4. Linear buffer pos
int pos = screen.rowColToAddress(0, 34) + 1; // 1-based pos
ECLSDString sdPos = new ECLSDString("TSO/E", pos, true);
assertTrue(sdPos.matches(ps, oia));
}
@Test
@DisplayName("23.1: ECLSDCursor location match (row, col) and pos")
public void testECLSDCursorMatching() {
ps.setCursorPos(7, 18); // 0-based row 7, col 18 = 1-based row 8, col 19
ECLSDCursor curMatch = new ECLSDCursor(8, 19);
assertTrue(curMatch.matches(ps, oia));
ECLSDCursor curMismatch = new ECLSDCursor(1, 1);
assertFalse(curMismatch.matches(ps, oia));
curMismatch.setInvertMatch(true);
assertTrue(curMismatch.matches(ps, oia));
// By linear position
int pos = screen.rowColToAddress(7, 18) + 1;
ECLSDCursor curPos = new ECLSDCursor(pos);
assertTrue(curPos.matches(ps, oia));
}
@Test
@DisplayName("23.1: ECLSDFields and ECLSDInputFields field count matching")
public void testECLSDFieldsAndInputFields() {
// Create formatted fields on screen
screen.setFieldAttribute(0, (byte) 0x20); // protected
screen.setFieldAttribute(80, (byte) 0x00); // unprotected input
screen.setFieldAttribute(160, (byte) 0x00); // unprotected input
ps.getFieldList().refresh();
assertEquals(3, ps.getFieldList().getFieldCount());
// Total field count
ECLSDFields fields3 = new ECLSDFields(3);
assertTrue(fields3.matches(ps, oia));
ECLSDFields fieldsWrong = new ECLSDFields(5);
assertFalse(fieldsWrong.matches(ps, oia));
fieldsWrong.setInvertMatch(true);
assertTrue(fieldsWrong.matches(ps, oia));
// Unprotected input fields count = 2
ECLSDInputFields inFields2 = new ECLSDInputFields(2);
assertTrue(inFields2.matches(ps, oia));
ECLSDInputFields inFieldsWrong = new ECLSDInputFields(3);
assertFalse(inFieldsWrong.matches(ps, oia));
inFieldsWrong.setInvertMatch(true);
assertTrue(inFieldsWrong.matches(ps, oia));
}
@Test
@DisplayName("23.1: ECLSDOIA OIA status matching (NOTINHIBITED, DONTCARE, input=true/false)")
public void testECLSDOIAMatching() {
oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED);
ECLSDOIA oiaNot = new ECLSDOIA(ECLSDOIA.OIA_NOTINHIBITED);
assertTrue(oiaNot.matches(ps, oia));
ECLSDOIA oiaDontCare = new ECLSDOIA(ECLSDOIA.OIA_DONTCARE);
assertTrue(oiaDontCare.matches(ps, oia));
ECLSDOIA oiaInputTrue = new ECLSDOIA("true", false);
assertTrue(oiaInputTrue.matches(ps, oia));
// Lock keyboard (system lock)
oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEM_LOCK);
assertFalse(oiaNot.matches(ps, oia));
assertFalse(oiaInputTrue.matches(ps, oia));
// Invert match should now match because input is inhibited
oiaNot.setInvertMatch(true);
assertTrue(oiaNot.matches(ps, oia));
// Explicit inhibit code check
ECLSDOIA oiaSysLock = new ECLSDOIA();
oiaSysLock.setExplicitInhibitStatus(ECLOIA.INHIBIT_SYSTEM_LOCK);
assertTrue(oiaSysLock.matches(ps, oia));
}
@Test
@DisplayName("23.1: ECLSDChecksum screen buffer hash matching")
public void testECLSDChecksumMatching() {
screen.setFieldAttribute(0, (byte) 0x20);
screen.setFieldAttribute(100, (byte) 0x00);
screen.setFieldAttribute(300, (byte) 0x20);
ps.getFieldList().refresh();
int checksum = ECLSDChecksum.CalculateChecksum(ps.getFieldList(), ps.getSize());
assertTrue(checksum != 0);
ECLSDChecksum sdChecksum = new ECLSDChecksum(checksum);
assertTrue(sdChecksum.matches(ps, oia));
ECLSDChecksum sdWrong = new ECLSDChecksum(checksum + 1);
assertFalse(sdWrong.matches(ps, oia));
sdWrong.setInvertMatch(true);
assertTrue(sdWrong.matches(ps, oia));
}
@Test
@DisplayName("23.1: ECLSDBlock multi-line block text matching")
public void testECLSDBlockMatching() {
ps.SetText("IBM MAINFRAME", 10, 5);
ps.SetText("SYSTEM/390", 11, 5);
String[] blockLines = new String[] { "IBM MAINFRAME", "SYSTEM/390" };
// Absolute block match
ECLSDBlock blkAbs = new ECLSDBlock(blockLines, 10, 5, true);
assertTrue(blkAbs.matches(ps, oia));
// Absolute block mismatch
ECLSDBlock blkAbsMiss = new ECLSDBlock(blockLines, 10, 6, true);
assertFalse(blkAbsMiss.matches(ps, oia));
// In-rect block match
ECLSDBlock blkRect = new ECLSDBlock(1, 1, 24, 80, true, false);
blkRect.setStrings(blockLines);
assertTrue(blkRect.matches(ps, oia));
blkRect.setInvertMatch(true);
assertFalse(blkRect.matches(ps, oia));
}
@Test
@DisplayName("23.1: ECLSDAttrib plane and attribute matching")
public void testECLSDAttribMatching() {
screen.setFieldAttribute(0, (byte) 0x20); // row 1, col 1 = protected (0x20)
ps.getFieldList().refresh();
ECLSDAttrib sdAttrib = new ECLSDAttrib((char) 0x20, 1, 1, ECLSDAttrib.FIELD_PLANE);
assertTrue(sdAttrib.matches(ps, oia));
ECLSDAttrib sdAttribWrong = new ECLSDAttrib((char) 0x00, 1, 1, ECLSDAttrib.FIELD_PLANE);
assertFalse(sdAttribWrong.matches(ps, oia));
sdAttribWrong.setInvertMatch(true);
assertTrue(sdAttribWrong.matches(ps, oia));
}
@Test
@DisplayName("23.2: Composite ECLScreenDesc matching and XML round-trip (<description> schema)")
public void testCompositeAndXmlRoundtrip() {
// Set up host screen
ps.SetText("TSO/E LOGON", 1, 35);
ps.setCursorPos(7, 18); // row 8, col 19
oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED);
screen.setFieldAttribute(0, (byte) 0x20); // 1 field
ps.getFieldList().refresh();
// 1. Programmatic Composite ECLScreenDesc
ECLScreenDesc desc = new ECLScreenDesc();
desc.AddString("TSO/E LOGON", 1, 35, false);
desc.AddCursorPos(8, 19);
desc.AddOIAStatus(ECLOIA.INHIBIT_NOTINHIBITED);
desc.AddNumFields(1);
assertEquals(4, desc.Size());
assertTrue(desc.Matches(ps, oia));
// 2. Check XML serialization
String xml = desc.getXML();
assertNotNull(xml);
assertTrue(xml.contains("<description>"));
assertTrue(xml.contains("<string"));
assertTrue(xml.contains("value=\"TSO/E LOGON\""));
assertTrue(xml.contains("<cursor"));
assertTrue(xml.contains("<oia"));
assertTrue(xml.contains("<numfields"));
assertTrue(xml.contains("</description>"));
// 3. Test XML parsing matching exact HoD schema from Phase 23 prompt
String sampleXml =
"<description>\n" +
" <string value=\"TSO/E LOGON\" row=\"1\" col=\"35\" casesense=\"false\"/>\n" +
" <cursor row=\"8\" col=\"19\"/>\n" +
" <oia input=\"true\"/>\n" +
" <numfields value=\"1\"/>\n" +
"</description>";
ECLScreenDesc parsedDesc = new ECLScreenDesc(sampleXml);
assertEquals(4, parsedDesc.Size());
assertTrue(parsedDesc.Matches(ps, oia));
// If screen text changes, match fails
ps.SetText("DIFFERENT SCR", 1, 35);
assertFalse(parsedDesc.Matches(ps, oia));
}
@Test
@DisplayName("23.1 & 23.2: ECLScreenReco dynamic registration triggers on PS and OIA updates")
public void testScreenRecoDynamicCallbacks() {
ECLScreenDesc desc = new ECLScreenDesc();
desc.AddString("READY", 1, 1);
desc.AddOIAStatus(ECLOIA.INHIBIT_NOTINHIBITED);
ECLScreenReco reco = new ECLScreenReco(ps, oia);
AtomicBoolean matched = new AtomicBoolean(false);
reco.RegisterScreen(desc, event -> matched.set(true));
// Screen is not matching yet
assertFalse(matched.get());
// Screen text updated but OIA locked
ps.SetText("READY", 1, 1);
oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEM_LOCK);
ps.notifyKeyUnlocked();
assertFalse(matched.get());
// Unlock OIA -> triggers reco callback
oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED);
assertTrue(matched.get());
reco.dispose();
}
@Test
@DisplayName("23.1: Drop-in compatibility with com.ibm.eNetwork.ECL.screenreco facade classes")
public void testHaclFacadesCompatibility() {
com.ibm.eNetwork.ECL.screenreco.ECLSDString s = new com.ibm.eNetwork.ECL.screenreco.ECLSDString("HELLO", 1, 1);
com.ibm.eNetwork.ECL.screenreco.ECLSDCursor c = new com.ibm.eNetwork.ECL.screenreco.ECLSDCursor(1, 1);
com.ibm.eNetwork.ECL.screenreco.ECLSDFields f = new com.ibm.eNetwork.ECL.screenreco.ECLSDFields(5);
com.ibm.eNetwork.ECL.screenreco.ECLSDInputFields inf = new com.ibm.eNetwork.ECL.screenreco.ECLSDInputFields(2);
com.ibm.eNetwork.ECL.screenreco.ECLSDOIA o = new com.ibm.eNetwork.ECL.screenreco.ECLSDOIA("NOTINHIBITED", false);
com.ibm.eNetwork.ECL.screenreco.ECLSDChecksum cs = new com.ibm.eNetwork.ECL.screenreco.ECLSDChecksum(100);
com.ibm.eNetwork.ECL.screenreco.ECLSDBlock blk = new com.ibm.eNetwork.ECL.screenreco.ECLSDBlock(1, 1, false, false);
com.ibm.eNetwork.ECL.screenreco.ECLSDBlockString bs = new com.ibm.eNetwork.ECL.screenreco.ECLSDBlockString("LINE");
com.ibm.eNetwork.ECL.screenreco.ECLSDAttrib attr = new com.ibm.eNetwork.ECL.screenreco.ECLSDAttrib((char) 0x20, 1, 1, 16);
assertTrue(s instanceof ECLScreenDescriptor);
assertTrue(c instanceof ECLScreenDescriptor);
assertTrue(f instanceof ECLScreenDescriptor);
assertTrue(inf instanceof ECLScreenDescriptor);
assertTrue(o instanceof ECLScreenDescriptor);
assertTrue(cs instanceof ECLScreenDescriptor);
assertTrue(blk instanceof ECLScreenDescriptor);
assertTrue(attr instanceof ECLScreenDescriptor);
assertEquals("HELLO", s.GetString());
assertEquals(1, c.GetRow());
assertEquals(5, f.GetNum());
assertEquals(2, inf.GetNum());
assertEquals(ECLSDOIA.OIA_NOTINHIBITED, o.GetOIAType());
assertEquals(100, cs.GetValue());
assertEquals(1, blk.getSRow());
assertEquals("LINE", bs.GetString());
assertEquals(0x20, attr.getAttrib());
}
}
@@ -0,0 +1,271 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.Telnet3270Client;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class SendKeyTokenizerPhase21Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private ECLSession session;
private ECLPS ps;
@BeforeEach
public void setUp() {
ConnectionConfig config = new ConnectionConfig("mainframe.example.com", 23, TerminalModel.IBM_3279_4);
Telnet3270Client client = new Telnet3270Client(config);
session = new ECLSession(client);
ps = session.GetPS();
screen = client.getScreenBuffer();
translator = client.getTranslator();
inputProcessor = client.getInputProcessor();
}
@Test
public void testTokenizerBasicPlainAndBracketed() throws ECLErr {
SendKeyTokenizer tok = new SendKeyTokenizer("LOGON APPLID[enter]");
assertTrue(tok.hasMoreTokens());
assertEquals("LOGON APPLID", tok.nextToken());
assertTrue(tok.hasMoreTokens());
assertEquals("[enter]", tok.nextToken());
assertFalse(tok.hasMoreTokens());
assertNull(tok.nextToken());
assertEquals("", tok.remainder());
tok.setSendKeyString("USER[tab]PASS[enter]");
assertEquals("USER", tok.nextToken());
assertEquals("[tab]", tok.nextToken());
assertEquals("PASS", tok.nextToken());
assertEquals("[enter]", tok.nextToken());
assertFalse(tok.hasMoreTokens());
}
@Test
public void testTokenizerEscapedBrackets() throws ECLErr {
SendKeyTokenizer tok = new SendKeyTokenizer("[[");
assertEquals("[", tok.nextToken());
assertFalse(tok.hasMoreTokens());
tok.setSendKeyString("]]");
assertEquals("]", tok.nextToken());
assertFalse(tok.hasMoreTokens());
tok.setSendKeyString("A[[B]]C");
assertEquals("A", tok.nextToken());
assertEquals("[", tok.nextToken());
assertEquals("B", tok.nextToken());
assertEquals("]", tok.nextToken());
assertEquals("C", tok.nextToken());
assertFalse(tok.hasMoreTokens());
// Escaped [[enter]] should yield "[", "enter", "]" rather than the AID key [enter]
tok.setSendKeyString("[[enter]]");
assertEquals("[", tok.nextToken());
assertEquals("enter", tok.nextToken());
assertEquals("]", tok.nextToken());
assertFalse(tok.hasMoreTokens());
}
@Test
public void testTokenizerExceptions() {
// Unterminated bracket
SendKeyTokenizer tok1 = new SendKeyTokenizer("[enter");
ECLErr err1 = assertThrows(ECLErr.class, tok1::nextToken);
assertEquals("ECL0012", err1.getID());
assertEquals(1, err1.getReasonCode());
assertEquals(12, err1.getMsgNumber());
// Single opening bracket at EOF
SendKeyTokenizer tok2 = new SendKeyTokenizer("[");
ECLErr err2 = assertThrows(ECLErr.class, tok2::nextToken);
assertEquals("ECL0012", err2.getID());
assertEquals(1, err2.getReasonCode());
// Single closing bracket at EOF
SendKeyTokenizer tok3 = new SendKeyTokenizer("]");
ECLErr err3 = assertThrows(ECLErr.class, tok3::nextToken);
assertEquals("ECL0012", err3.getID());
assertEquals(1, err3.getReasonCode());
// Unescaped closing bracket followed by characters
SendKeyTokenizer tok4 = new SendKeyTokenizer("]invalid");
ECLErr err4 = assertThrows(ECLErr.class, tok4::nextToken);
assertEquals("ECL0012", err4.getID());
assertEquals(1, err4.getReasonCode());
}
@Test
public void testTokenizerNoAidAndCharMode() throws ECLErr {
SendKeyTokenizer tok = new SendKeyTokenizer("A[enter]B");
assertEquals("A", tok.nextTokenNoAid());
assertEquals("[", tok.nextTokenNoAid());
assertEquals("e", tok.nextTokenNoAid());
// Character mode (chunkPlainText = false) matching HoD 1:1 internal loop
tok.setSendKeyString("ABC[tab]");
tok.setChunkPlainText(false);
assertEquals("A", tok.nextToken());
assertEquals("B", tok.nextToken());
assertEquals("C", tok.nextToken());
assertEquals("[tab]", tok.nextToken());
assertFalse(tok.hasMoreTokens());
// Single digit keypad shortcut [0]..[9] -> "0".."9"
tok.setSendKeyString("[5]");
tok.setChunkPlainText(true);
assertEquals("5", tok.nextToken());
}
@Test
public void testECLErrConstructorsAndParity() {
ECLErr err = new ECLErr("SendKeyTokenizer:1", "ECL0012", 1);
assertEquals("SendKeyTokenizer:1", err.getLocation());
assertEquals("SendKeyTokenizer:1", err.GetLocation());
assertEquals("ECL0012", err.getID());
assertEquals("ECL0012", err.GetID());
assertEquals(1, err.getReasonCode());
assertEquals(1, err.GetReasonCode());
assertEquals(12, err.getMsgNumber());
assertEquals(12, err.GetMsgNumber());
assertNotNull(err.toString());
ECLErr err2 = new ECLErr("ECLPS", "ECL0010");
assertEquals("ECLPS", err2.getLocation());
assertEquals("ECL0010", err2.getID());
ECLErr err3 = new ECLErr("ECLPS", "ECL0010", "param", 2);
assertEquals(2, err3.getReasonCode());
}
@Test
public void testSendKeysEscapingInScreen() {
// Setup unprotected field at 0
screen.setCellFA(0, (byte) 0x40);
screen.setCursorAddress(1);
// Send keys with escaped brackets
inputProcessor.sendKeys("ABC[[XYZ]]123");
assertEquals("ABC[XYZ]123", ps.getString(1, 11));
}
@Test
public void test120MnemonicTokenTableInInputProcessor() {
screen.clear();
// Setup two fields: field 1 at 0 (len 10), field 2 at 11 (len 10)
screen.setCellFA(0, (byte) 0x40);
screen.setCellFA(11, (byte) 0x40);
screen.setCursorAddress(1);
// Test typing and tab
inputProcessor.sendKeys("HELLO[tab]WORLD");
assertEquals("HELLO", ps.getString(1, 5));
assertEquals("WORLD", ps.getString(12, 5));
// Test backtab: first backtab moves to beginning of current field (12)
inputProcessor.sendKeys("[backtab]");
assertEquals(12, screen.getCursorAddress());
// Second backtab moves to beginning of previous field (1)
inputProcessor.sendKeys("[backtab]");
assertEquals(1, screen.getCursorAddress());
// Test bof and eof
inputProcessor.sendKeys("[eof]");
assertEquals(6, screen.getCursorAddress()); // end of "HELLO"
inputProcessor.sendKeys("[bof]");
assertEquals(1, screen.getCursorAddress()); // start of field
// Test erasefld
inputProcessor.sendKeys("[erasefld]");
assertEquals(1, screen.getCursorAddress());
assertEquals(" ", ps.getString(1, 5));
// Test dup and fieldmark (dup enters DUP at 1 and tabs to next field at 12)
inputProcessor.sendKeys("[dup][fieldmark]");
assertEquals(0x1C, screen.getCell(1).ec & 0xFF);
assertEquals(0x1E, screen.getCell(12).ec & 0xFF);
// Test numeric keypad tokens
screen.setCursorAddress(1);
inputProcessor.sendKeys("[keypad1][keypad2][keypad.][keypad-]");
assertEquals("12.-", ps.getString(1, 4));
// Test enterreset
inputProcessor.setKeyboardLocked(true);
inputProcessor.sendKeys("[enterreset]");
assertFalse(inputProcessor.isKeyboardLocked());
// Test docmode, wordwrap, apl
boolean docBefore = screen.isEntryAssistDOCmode();
inputProcessor.sendKeys("[docmode]");
assertEquals(!docBefore, screen.isEntryAssistDOCmode());
boolean wrapBefore = screen.isEntryAssistWordWrap();
inputProcessor.sendKeys("[wordwrap]");
assertEquals(!wrapBefore, screen.isEntryAssistWordWrap());
// Test dspsosi
inputProcessor.sendKeys("[dspsosi]");
// Test altview
inputProcessor.sendKeys("[altview]");
// Test cursor selection and light pen
screen.setCellFA(30, (byte) (0x40 | DS3270Constants.FA_INT_NORM_SEL));
screen.getCell(31).ec = (byte) 0x6F; // '?'
screen.getCell(31).ucs4 = '?';
screen.setCursorAddress(31);
inputProcessor.sendKeys("[selfld]");
assertEquals('>', (char) screen.getCell(31).ucs4);
assertTrue((screen.getCell(30).fa & DS3270Constants.FA_MODIFY) != 0);
inputProcessor.sendKeys("[deselfld]");
assertEquals('?', (char) screen.getCell(31).ucs4);
assertEquals(0, screen.getCell(30).fa & DS3270Constants.FA_MODIFY);
// Test safe handling of 30+ extended HoD tokens (bidi, print, gui, layers)
inputProcessor.sendKeys("[screenrev][latinlayer][bidilayer][push][endpush][autopush][fieldshape][prtestpage][copy][paste]");
}
@Test
public void testSendCharactersPacingAndPositioning() {
screen.clear();
screen.setCellFA(0, (byte) 0x40);
screen.setCursorAddress(1);
long start = System.currentTimeMillis();
// Paced typing with 10ms delay over 4 tokens
ps.sendCharacters("TEST", 10);
long elapsed = System.currentTimeMillis() - start;
assertEquals("TEST", ps.getString(1, 4));
assertTrue(elapsed >= 25, "Elapsed time should reflect paced typing delay: " + elapsed + "ms");
// Test SendKeys / SendCharacters with 1-based pos
screen.clear();
screen.setCellFA(9, (byte) 0x40); // FA at pos 10, data starts at pos 11
ps.SendKeys("HOD", 11);
assertEquals("HOD", ps.getString(10, 3));
ps.SendCharacters("123", 11, 5);
assertEquals("123", ps.getString(10, 3));
// Test SendKeysNoAid
ps.SendKeysNoAid("XYZ", false);
assertEquals("XYZ", ps.getString(13, 3));
}
}
@@ -0,0 +1,391 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Targeted unit tests for Phase 20: IBM 3287/3286 Host Printing Protocol (SCS LU-1 & DSC/DSE LU-3).
* Validates 1:1 parity with IBM Host On-Demand v14 com.ibm.eNetwork.ECL.tn3270p.*
*/
public class Phase20HostPrintingTest {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
pd.openPrinter(null); // Capture in memory
}
// ==========================================
// 20.1 SCS Control Orders Execution (LU-1)
// ==========================================
@Test
public void testSCSHorizontalFormatSHF() {
PrintSCS3270 scs = new PrintSCS3270(config, pd, translator);
// Standard SNA SHF (0x2B 0xC1): count=6, MPP=132, LM=10, RM=120, tabs at 20, 30
byte[] shf = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SHF,
0x06, // count
(byte) 132, // MPP
10, // Left Margin
120, // Right Margin
20, // Tab 1
30 // Tab 2
};
scs.processHostData(shf, 0, shf.length);
assertEquals(132, scs.getLineLength());
assertEquals(10, scs.getLeftMargin());
assertEquals(120, scs.getRightMargin());
assertEquals(10, scs.getCurrentColumn()); // Clamped to LM
// Test Horizontal Tab (HT 0x05)
byte[] ht = new byte[]{ PrinterConstants.SCS_HT };
scs.processHostData(ht, 0, ht.length);
assertEquals(20, scs.getCurrentColumn());
scs.processHostData(ht, 0, ht.length);
assertEquals(30, scs.getCurrentColumn());
}
@Test
public void testSCSVerticalFormatSVF() {
PrintSCS3270 scs = new PrintSCS3270(config, pd, translator);
// Standard SNA SVF (0x2B 0xC2): count=6, MPL=66, TM=3, BM=60, tabs at 15, 30
byte[] svf = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SVF,
0x06, // count
66, // MPL
3, // Top Margin
60, // Bottom Margin
15, // VTab 1
30 // VTab 2
};
scs.processHostData(svf, 0, svf.length);
assertEquals(66, scs.getPageLength());
assertEquals(3, scs.getTopMargin());
assertEquals(60, scs.getBottomMargin());
assertEquals(3, scs.getCurrentRow()); // Clamped to TM
// Test Vertical Tab (VT 0x0B)
byte[] vt = new byte[]{ PrinterConstants.SCS_VT };
scs.processHostData(vt, 0, vt.length);
assertEquals(15, scs.getCurrentRow());
scs.processHostData(vt, 0, vt.length);
assertEquals(30, scs.getCurrentRow());
}
@Test
public void testSCSLineDensitySLD() {
PrintSCS3270 scs = new PrintSCS3270(config, pd, translator);
// SLD (0x2B 0xC6): 12 points = 6 LPI
byte[] sld6 = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SLD,
0x02, // count
12 // points (12 = 6 LPI)
};
scs.processHostData(sld6, 0, sld6.length);
assertEquals(6, scs.getLinesPerInch());
assertEquals(6, pd.getLPI());
// SLD (0x2B 0xC6): 9 points = 8 LPI
byte[] sld8 = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SLD,
0x02, // count
9 // points (9 = 8 LPI)
};
scs.processHostData(sld8, 0, sld8.length);
assertEquals(8, scs.getLinesPerInch());
assertEquals(8, pd.getLPI());
// SLD with count=1 (defaults to 6 LPI)
byte[] sldDefault = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SLD,
0x01
};
scs.processHostData(sldDefault, 0, sldDefault.length);
assertEquals(6, scs.getLinesPerInch());
}
@Test
public void testSCSPrintDensitySPD() {
PrintSCS3270 scs = new PrintSCS3270(config, pd, translator);
// SPD (0x2B 0xD2): standard parameter format count=4, type=0x29, density=12 CPI
byte[] spd12 = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SPD,
0x04,
PrinterConstants.SPD_TYPE_ID, // 0x29
0x00,
12
};
scs.processHostData(spd12, 0, spd12.length);
assertEquals(12, scs.getCharsPerInch());
assertEquals(12, pd.getCPI());
assertFalse(scs.isDoubleWidth());
// SPD (0x2B 0xD2): density=15 CPI
byte[] spd15 = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SPD,
0x04,
PrinterConstants.SPD_TYPE_ID,
0x00,
15
};
scs.processHostData(spd15, 0, spd15.length);
assertEquals(15, scs.getCharsPerInch());
assertFalse(scs.isDoubleWidth());
// SPD density=5 CPI (triggers double-width 10 CPI)
byte[] spd5 = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SPD,
0x04,
PrinterConstants.SPD_TYPE_ID,
0x00,
5
};
scs.processHostData(spd5, 0, spd5.length);
assertEquals(10, scs.getCharsPerInch());
assertTrue(scs.isDoubleWidth());
}
@Test
public void testSCSTransparentDataTRN() {
PrintSCS3270 scs = new PrintSCS3270(config, pd, translator);
// TRN (0x35): bypass translation and emit raw binary escape bytes
byte[] rawPayload = new byte[]{ 0x1B, 0x5B, 0x32, 0x4A }; // ANSI Clear Screen ESC [ 2 J
byte[] trn = new byte[]{
PrinterConstants.SCS_TRN,
(byte) rawPayload.length,
rawPayload[0], rawPayload[1], rawPayload[2], rawPayload[3]
};
scs.processHostData(trn, 0, trn.length);
byte[] captured = pd.getCapturedBytes();
assertNotNull(captured);
assertTrue(captured.length >= 4);
assertEquals(0x1B, captured[0]);
assertEquals(0x5B, captured[1]);
assertEquals(0x32, captured[2]);
assertEquals(0x4A, captured[3]);
}
@Test
public void testSCSFormFeedAndPositions() {
PrintSCS3270 scs = new PrintSCS3270(config, pd, translator);
scs.setHorizontalMargins(5, 80);
scs.setVerticalMargins(2, 60);
// Print some characters
byte[] text = translator.stringToEbcdic("LINE 1");
scs.processHostData(text, 0, text.length);
// Form Feed
scs.processHostData(new byte[]{ PrinterConstants.SCS_FF }, 0, 1);
assertEquals(1, pd.getPageCount());
assertEquals(2, scs.getCurrentRow()); // Reset to TM
assertEquals(5, scs.getCurrentColumn()); // Reset to LM
}
@Test
public void testSCSPresentationPositionPP() {
PrintSCS3270 scs = new PrintSCS3270(config, pd, translator);
// PP (0x34): absolute horizontal column 35
byte[] ppHoriz = new byte[]{
PrinterConstants.SCS_PP,
(byte) PrinterConstants.PP_HORIZ_ABS,
35
};
scs.processHostData(ppHoriz, 0, ppHoriz.length);
assertEquals(35, scs.getCurrentColumn());
// PP (0x34): relative horizontal +10
byte[] ppHorizRel = new byte[]{
PrinterConstants.SCS_PP,
(byte) PrinterConstants.PP_HORIZ_REL,
10
};
scs.processHostData(ppHorizRel, 0, ppHorizRel.length);
assertEquals(45, scs.getCurrentColumn());
// PP (0x34): absolute vertical row 5
byte[] ppVert = new byte[]{
PrinterConstants.SCS_PP,
(byte) PrinterConstants.PP_VERT_ABS,
5
};
scs.processHostData(ppVert, 0, ppVert.length);
assertEquals(5, scs.getCurrentRow());
}
// ==========================================
// 20.2 DSC / DSE Buffer Processing (LU-3)
// ==========================================
@Test
public void testLU3WriteAndNonDisplayFieldMasking() throws Exception {
PrintPS3270 ps = new PrintPS3270(config, pd, translator);
ByteArrayOutputStream ds = new ByteArrayOutputStream();
ds.write(DS3270Constants.CMD_ERASE_WRITE);
ds.write(PrinterConstants.WCC_START_PRINT_BIT); // WCC: Start print
// SBA to (0, 0)
ds.write(PrinterConstants.ORDER_SBA);
ds.write(0x40);
ds.write(0x40);
// SF with visible attribute (0xC0: unprotected, display)
ds.write(PrinterConstants.ORDER_SF);
ds.write(0xC0);
ds.write(translator.stringToEbcdic("PUBLIC"));
// SF with non-display attribute (0x4C: protected, non-display)
ds.write(PrinterConstants.ORDER_SF);
ds.write(0x4C);
ds.write(translator.stringToEbcdic("SECRET"));
// SF with visible attribute again
ds.write(PrinterConstants.ORDER_SF);
ds.write(0xC0);
ds.write(translator.stringToEbcdic("VISIBLE"));
byte[] record = ds.toByteArray();
ps.process3270PrintDS(record, 0, record.length);
String captured = pd.getCapturedText();
assertNotNull(captured);
assertTrue(captured.contains("PUBLIC"));
assertFalse(captured.contains("SECRET")); // Masked as blanks!
assertTrue(captured.contains("VISIBLE"));
}
@Test
public void testLU3EraseWriteAndEORFlush() throws Exception {
PrintPS3270 ps = new PrintPS3270(config, pd, translator);
// Write first record without Start Print bit
ByteArrayOutputStream ds = new ByteArrayOutputStream();
ds.write(DS3270Constants.CMD_WRITE);
ds.write(0x00); // WCC without start print
ds.write(PrinterConstants.ORDER_SBA);
ds.write(0x40);
ds.write(0x40);
ds.write(translator.stringToEbcdic("RECORD 1 DATA"));
byte[] r1 = ds.toByteArray();
ps.process3270PrintDS(r1, 0, r1.length);
// EOR should flush formatted page
ps.endOfRecord();
String captured = pd.getCapturedText();
assertTrue(captured.contains("RECORD 1 DATA"));
}
// ==========================================
// 20.3 Double-Byte (DBCS) SCS & DSE Print Stream
// ==========================================
@Test
public void testDBCSSCSSpacingAndPitch() throws Exception {
config.setCodePage("930"); // Japanese DBCS
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
PrintSCS3270DB scsDb = new PrintSCS3270DB(config, pd, dbcsTranslator);
// Setting base CPI to 12 should update DBCS CPI to 6
scsDb.setPrintDensity(12, 6);
assertEquals(12, scsDb.getCharsPerInch());
assertEquals(6, scsDb.getDBCSCPI());
// Print 'X', SO, DBCS ideographic blank (0x4040), SI, 'Y'
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(translator.stringToEbcdic("X"));
stream.write(PrinterConstants.SCS_SO);
stream.write(0x40);
stream.write(0x40);
stream.write(PrinterConstants.SCS_SI);
stream.write(translator.stringToEbcdic("Y"));
scsDb.processHostData(stream.toByteArray(), 0, stream.size());
scsDb.flushLineBuffer();
String captured = pd.getCapturedText();
assertTrue(captured.contains("X\u3000 Y")); // X at col 1, DBCS 2 cols (col 2,3), Y at col 4
// EOR resets DBCS mode if active
scsDb.processSO();
assertTrue(scsDb.isDBCSMode());
scsDb.endOfRecord();
assertFalse(scsDb.isDBCSMode());
}
@Test
public void testDBCSLU3PrintStreamSOandSI() {
config.setCodePage("930");
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
PrintPS3270DB psDb = new PrintPS3270DB(config, pd, dbcsTranslator);
ByteArrayOutputStream ds = new ByteArrayOutputStream();
ds.write(DS3270Constants.CMD_ERASE_WRITE);
ds.write(PrinterConstants.WCC_START_PRINT_BIT);
ds.write(PrinterConstants.ORDER_SBA);
ds.write(0x40);
ds.write(0x40);
// SBCS 'A'
ds.write(0xC1);
// SO
ds.write(PrinterConstants.SCS_SO);
// DBCS Kanji '日' (in Cp930: 0x43 0x81)
ds.write(0x43);
ds.write(0x81);
// SI
ds.write(PrinterConstants.SCS_SI);
// SBCS 'Z'
ds.write(0xE9);
byte[] record = ds.toByteArray();
psDb.process3270PrintDS(record, 0, record.length);
// Check DBCS cells in buffer
assertTrue(psDb.isDBCSCell(1));
assertTrue(psDb.isDBCSLeft(1));
assertTrue(psDb.isDBCSCell(2));
assertTrue(psDb.isDBCSRight(2));
String captured = pd.getCapturedText();
assertNotNull(captured);
assertTrue(captured.contains("A"));
assertTrue(captured.contains("Z"));
}
}