Phase 6
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s

This commit is contained in:
2026-08-28 13:57:09 -04:00
parent 40ebd40fe2
commit 3a576b79b4
35 changed files with 5539 additions and 275 deletions
@@ -27,6 +27,7 @@ public class ConnectionConfig {
private int dynamicCols = 80;
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
private String codePage = "037";
private String associatedPrinterLu = null;
public ConnectionConfig() {}
@@ -214,4 +215,20 @@ public class ConnectionConfig {
}
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
}
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
pcfg.setUseTls(useTls);
pcfg.setTlsVerifyCert(tlsVerifyCert);
pcfg.setCertificateVerifier(certificateVerifier);
pcfg.setSslProtocol(sslProtocol);
pcfg.setConnectTimeoutMs(connectTimeoutMs);
pcfg.setPrinterLuName(associatedPrinterLu);
pcfg.setAssociatedDisplayLuName(luName);
pcfg.setCodePage(codePage);
return pcfg;
}
}
@@ -158,4 +158,43 @@ public class EbcdicTranslator {
public static List<CodePage> getAllCodePages() {
return CodePageRegistry.getAvailableCodePages();
}
/**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
*/
public char getAplGraphic(int ec) {
switch (ec & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return ebcdicToUnicode(ec & 0xFF);
}
}
}
@@ -87,6 +87,16 @@ public class DataStreamProcessor {
this.ftDft = ftDft;
}
private haus.nightmare.lib3270j.printer.PrintSCS3270 embeddedScsProcessor;
public void setEmbeddedScsProcessor(haus.nightmare.lib3270j.printer.PrintSCS3270 scsProcessor) {
this.embeddedScsProcessor = scsProcessor;
}
public haus.nightmare.lib3270j.printer.PrintSCS3270 getEmbeddedScsProcessor() {
return embeddedScsProcessor;
}
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
this.inputProcessor = inputProcessor;
}
@@ -1168,6 +1178,9 @@ public class DataStreamProcessor {
public void processSCSData(byte[] data, int offset, int length) {
log.info("Received embedded SCS printer data (" + length + " bytes)");
if (embeddedScsProcessor != null && data != null && length > 0) {
embeddedScsProcessor.processHostData(data, offset, length);
}
}
public void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
@@ -478,6 +478,10 @@ public class QueryReplyBuilder {
};
}
public byte[] buildAuxDevice() {
return buildAuxDev();
}
private byte[] buildAuxDev() {
return new byte[]{
0x00, 0x09, 0x00, 0x07,
@@ -0,0 +1,273 @@
package haus.nightmare.lib3270j.printer;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Print Output Destination & Spool Interface (PD3270).
* Implements 1:1 functional compatibility with IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PD3270.
*
* Supports printing to:
* 1. In-memory buffer / capture (for programmatic access or UI display)
* 2. Local disk file (with append/overwrite modes)
* 3. External system print command / pipe process (e.g. lpr, lp)
*/
public class PD3270 {
private static final Logger log = Logger.getLogger(PD3270.class.getName());
private final PrinterConfig config;
private String destination;
private boolean open = false;
private OutputStream targetOutputStream;
private Writer targetWriter;
private Process pipeProcess;
private final ByteArrayOutputStream memoryStream = new ByteArrayOutputStream();
private long byteCount = 0;
private int pageCount = 0;
private Charset outputCharset = StandardCharsets.UTF_8;
public PD3270() {
this(new PrinterConfig());
}
public PD3270(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.destination = this.config.getDestinationTarget();
}
/**
* Open print destination matching IBM HoD openPrinter specification.
* @param destination Target path, command string, or null/empty for in-memory capture.
* @return true if opened successfully, false on error.
*/
public synchronized boolean openPrinter(String destination) {
if (open) {
closePrinter();
}
this.destination = destination != null ? destination : (config != null ? config.getDestinationTarget() : null);
this.byteCount = 0;
this.pageCount = 0;
this.memoryStream.reset();
PrinterConfig.DestinationType destType = config != null ? config.getDestinationType() : PrinterConfig.DestinationType.MEMORY;
try {
if (this.destination != null && !this.destination.trim().isEmpty()) {
String trimmedDest = this.destination.trim();
if (destType == PrinterConfig.DestinationType.COMMAND || trimmedDest.startsWith("|")) {
String cmd = trimmedDest.startsWith("|") ? trimmedDest.substring(1).trim() : trimmedDest;
log.info("Opening printer pipe to process: " + cmd);
pipeProcess = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
targetOutputStream = new BufferedOutputStream(pipeProcess.getOutputStream());
targetWriter = new OutputStreamWriter(targetOutputStream, outputCharset);
} else if (destType == PrinterConfig.DestinationType.FILE || destType != PrinterConfig.DestinationType.MEMORY) {
log.info("Opening printer file: " + trimmedDest);
File file = new File(trimmedDest);
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
targetOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
targetWriter = new OutputStreamWriter(targetOutputStream, outputCharset);
}
}
open = true;
log.fine("PD3270 printer opened: destination=" + this.destination);
return true;
} catch (Exception e) {
log.log(Level.SEVERE, "Failed to open printer destination: " + destination, e);
abortPrinter();
return false;
}
}
/**
* Write raw bytes directly to print output destination.
*/
public synchronized void writePrintBytes(byte[] data, int offset, int length) {
if (!open) {
openPrinter(destination);
}
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
return;
}
try {
memoryStream.write(data, offset, length);
byteCount += length;
if (targetOutputStream != null) {
targetOutputStream.write(data, offset, length);
}
} catch (IOException e) {
log.log(Level.WARNING, "Error writing bytes to printer destination", e);
}
}
/**
* Write a single character to the active print destination.
*/
public synchronized void writePrintChar(char c) {
if (!open) {
openPrinter(destination);
}
try {
byte[] b = String.valueOf(c).getBytes(outputCharset);
memoryStream.write(b);
byteCount += b.length;
if (targetWriter != null) {
targetWriter.write(c);
} else if (targetOutputStream != null) {
targetOutputStream.write(b);
}
} catch (IOException e) {
log.log(Level.WARNING, "Error writing character to printer", e);
}
}
/**
* Write a string to the active print destination.
*/
public synchronized void writePrintString(String s) {
if (s == null || s.isEmpty()) return;
if (!open) {
openPrinter(destination);
}
try {
byte[] b = s.getBytes(outputCharset);
memoryStream.write(b);
byteCount += b.length;
if (targetWriter != null) {
targetWriter.write(s);
} else if (targetOutputStream != null) {
targetOutputStream.write(b);
}
} catch (IOException e) {
log.log(Level.WARNING, "Error writing string to printer", e);
}
}
/**
* Write a line with system line-separator.
*/
public synchronized void writePrintLine(String line) {
writePrintString((line != null ? line : "") + "\n");
}
/**
* Advance / Form Feed to next page.
*/
public synchronized void formFeed() {
pageCount++;
writePrintChar('\f');
flush();
}
/**
* Flush all buffered print data to target stream.
*/
public synchronized void flush() {
try {
if (targetWriter != null) {
targetWriter.flush();
}
if (targetOutputStream != null) {
targetOutputStream.flush();
}
} catch (IOException e) {
log.log(Level.FINE, "Exception flushing print stream", e);
}
}
/**
* Close the printer destination cleanly.
*/
public synchronized void closePrinter() {
if (!open) return;
try {
flush();
if (targetWriter != null) {
targetWriter.close();
targetWriter = null;
}
if (targetOutputStream != null) {
targetOutputStream.close();
targetOutputStream = null;
}
if (pipeProcess != null) {
pipeProcess.getOutputStream().close();
try {
pipeProcess.waitFor();
} catch (InterruptedException ignored) {}
pipeProcess = null;
}
} catch (IOException e) {
log.log(Level.FINE, "Exception closing printer", e);
} finally {
open = false;
}
}
/**
* Abort the printer and clean up resources immediately.
*/
public synchronized void abortPrinter() {
try {
if (targetWriter != null) {
try { targetWriter.close(); } catch (Exception ignored) {}
targetWriter = null;
}
if (targetOutputStream != null) {
try { targetOutputStream.close(); } catch (Exception ignored) {}
targetOutputStream = null;
}
if (pipeProcess != null) {
pipeProcess.destroy();
pipeProcess = null;
}
} finally {
open = false;
}
}
// ========== Status & Accessors ==========
public boolean isOpen() { return open; }
public String getDestination() { return destination; }
public long getByteCount() { return byteCount; }
public int getPageCount() { return pageCount; }
public synchronized String getCapturedText() {
return new String(memoryStream.toByteArray(), outputCharset);
}
public synchronized byte[] getCapturedBytes() {
return memoryStream.toByteArray();
}
public synchronized void resetCapture() {
memoryStream.reset();
byteCount = 0;
pageCount = 0;
}
public Charset getOutputCharset() { return outputCharset; }
public void setOutputCharset(Charset charset) {
if (charset != null) {
this.outputCharset = charset;
}
}
}
@@ -0,0 +1,393 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* LU Type 3 3270 Printer Data Stream Processor (PrintPS3270 / DS3270P).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintPS3270 / DS3270P.
*
* Simulates a 3270 printer buffer, processes Write Control Characters (WCC),
* interprets 3270 buffer orders (SBA, SF, SFE, SA, RA, EUA, GE, NL, EM, FF, CR),
* and renders formatted lines to PD3270.
*/
public class PrintPS3270 {
private static final Logger log = Logger.getLogger(PrintPS3270.class.getName());
private final PrinterConfig config;
private final PD3270 pd;
private final EbcdicTranslator translator;
private int rows = 24;
private int cols = 80;
private int bufferSize = 24 * 80;
private int bufferAddress = 0;
// Buffer planes
private byte[] textPlane;
private byte[] attrPlane;
private byte[] colorPlane;
private byte[] hilitePlane;
private int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL;
public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
int mpp = this.config.getMpp();
this.cols = (mpp > 0) ? mpp : 80;
this.rows = (this.config.getMpl() > 0) ? this.config.getMpl() : 24;
this.bufferSize = this.rows * this.cols;
initBuffer();
}
private void initBuffer() {
this.textPlane = new byte[bufferSize];
this.attrPlane = new byte[bufferSize];
this.colorPlane = new byte[bufferSize];
this.hilitePlane = new byte[bufferSize];
erasePrintBuffer();
}
/**
* Erase all contents of the printer buffer to nulls (0x00).
*/
public synchronized void erasePrintBuffer() {
Arrays.fill(textPlane, (byte) 0x00);
Arrays.fill(attrPlane, (byte) 0x00);
Arrays.fill(colorPlane, (byte) 0x00);
Arrays.fill(hilitePlane, (byte) 0x00);
this.bufferAddress = 0;
}
/**
* Main entry point for processing LU Type 3 3270 Printer Data Stream records.
*/
public synchronized void process3270PrintDS(byte[] data, int offset, int length) {
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
return;
}
int cmd = data[offset] & 0xFF;
switch (cmd) {
case DS3270Constants.CMD_WRITE:
case DS3270Constants.SNA_CMD_W:
processWrite(data, offset, length, false);
break;
case DS3270Constants.CMD_ERASE_WRITE:
case DS3270Constants.SNA_CMD_EW:
processEraseWrite(data, offset, length);
break;
case DS3270Constants.CMD_ERASE_WRITE_ALT:
case DS3270Constants.SNA_CMD_EWA:
processEraseWriteAlternate(data, offset, length);
break;
case DS3270Constants.CMD_ERASE_ALL_UNPROTECTED:
case DS3270Constants.SNA_CMD_EAU:
erasePrintBuffer();
break;
default:
// If command byte is unrecognized, treat entire stream as write without erase
processWrite(data, offset - 1, length + 1, false);
break;
}
}
public synchronized void processWrite(byte[] data, int offset, int length, boolean eraseFirst) {
if (eraseFirst) {
erasePrintBuffer();
}
int idx = offset + 1; // Skip command byte
int end = offset + length;
if (idx >= end) return;
// WCC (Write Control Character)
int wcc = data[idx++] & 0xFF;
boolean startPrint = isStartPrint(wcc);
this.currentPrintFormat = getPrintFormat(wcc);
byte currentColor = 0;
byte currentHilite = 0;
while (idx < end) {
int b = data[idx++] & 0xFF;
switch (b) {
case PrinterConstants.ORDER_SBA:
if (idx + 1 < end) {
int b1 = data[idx++] & 0xFF;
int b2 = data[idx++] & 0xFF;
bufferAddress = decodeBufferAddress(b1, b2) % bufferSize;
}
break;
case PrinterConstants.ORDER_SF:
if (idx < end) {
byte fa = data[idx++];
setCellFA(bufferAddress, fa);
bufferAddress = (bufferAddress + 1) % bufferSize;
}
break;
case PrinterConstants.ORDER_SFE:
if (idx < end) {
int pairCount = data[idx++] & 0xFF;
byte fa = 0;
for (int p = 0; p < pairCount && idx + 1 < end; p++) {
int type = data[idx++] & 0xFF;
int val = data[idx++] & 0xFF;
if (type == 0x00 || type == 0xC0) {
fa = (byte) val;
} else if (type == PrinterConstants.SA_COLOR) {
currentColor = (byte) val;
} else if (type == PrinterConstants.SA_HILITE) {
currentHilite = (byte) val;
}
}
setCellFA(bufferAddress, fa);
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
break;
case PrinterConstants.ORDER_SA:
if (idx + 1 < end) {
int type = data[idx++] & 0xFF;
int val = data[idx++] & 0xFF;
if (type == PrinterConstants.SA_COLOR) {
currentColor = (byte) val;
} else if (type == PrinterConstants.SA_HILITE) {
currentHilite = (byte) val;
} else if (type == PrinterConstants.SA_RESET) {
currentColor = 0;
currentHilite = 0;
}
}
break;
case PrinterConstants.ORDER_RA:
if (idx + 2 < end) {
int b1 = data[idx++] & 0xFF;
int b2 = data[idx++] & 0xFF;
int stopAddr = decodeBufferAddress(b1, b2) % bufferSize;
byte fillChar = data[idx++];
if (stopAddr == bufferAddress) {
Arrays.fill(textPlane, fillChar);
} else {
while (bufferAddress != stopAddr) {
textPlane[bufferAddress] = fillChar;
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
}
}
break;
case PrinterConstants.ORDER_EUA:
if (idx + 1 < end) {
int b1 = data[idx++] & 0xFF;
int b2 = data[idx++] & 0xFF;
int stopAddr = decodeBufferAddress(b1, b2) % bufferSize;
while (bufferAddress != stopAddr) {
textPlane[bufferAddress] = 0x00;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
}
break;
case PrinterConstants.ORDER_IC:
case PrinterConstants.ORDER_PT:
// Position cursor / tab
break;
case PrinterConstants.ORDER_GE:
if (idx < end) {
byte geByte = data[idx++];
textPlane[bufferAddress] = geByte;
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
}
break;
case PrinterConstants.ORDER_NL:
// Advance bufferAddress to start of next line
int curRow = bufferAddress / cols;
bufferAddress = ((curRow + 1) * cols) % bufferSize;
break;
case PrinterConstants.ORDER_EM:
// End of message: trigger print buffer flush
flushPrintBuffer();
break;
case PrinterConstants.ORDER_FF:
pd.formFeed();
break;
case PrinterConstants.ORDER_CR:
bufferAddress = (bufferAddress / cols) * cols;
break;
default:
// Standard printable EBCDIC character
textPlane[bufferAddress] = (byte) b;
colorPlane[bufferAddress] = currentColor;
hilitePlane[bufferAddress] = currentHilite;
bufferAddress = (bufferAddress + 1) % bufferSize;
break;
}
}
if (startPrint) {
flushPrintBuffer();
}
}
public synchronized void processEraseWrite(byte[] data, int offset, int length) {
processWrite(data, offset, length, true);
}
public synchronized void processEraseWriteAlternate(byte[] data, int offset, int length) {
processWrite(data, offset, length, true);
}
private void setCellFA(int addr, byte fa) {
attrPlane[addr] = fa;
textPlane[addr] = 0x00; // Field attributes display as blanks/nulls
}
private int decodeBufferAddress(int b1, int b2) {
// Fast 3270 12-bit / 14-bit address calculation
if ((b1 & 0xC0) == 0) {
return ((b1 & 0x3F) << 8) | b2;
}
return ((b1 & 0x3F) << 6) | (b2 & 0x3F);
}
/**
* Format and print a specific row from the presentation buffer.
*/
public synchronized void printLine(int row, int length) {
if (row < 0 || row >= rows) return;
int lineLen = Math.min(length > 0 ? length : cols, cols);
int startAddr = row * cols;
char[] chars = new char[lineLen];
for (int c = 0; c < lineLen; c++) {
int ebc = textPlane[startAddr + c] & 0xFF;
if (ebc == 0x00) {
chars[c] = ' ';
} else {
chars[c] = translator.ebcdicToUnicode(ebc);
}
}
// Trim trailing blanks
int end = lineLen;
while (end > 0 && chars[end - 1] == ' ') {
end--;
}
if (end > 0) {
pd.writePrintString(new String(chars, 0, end));
}
pd.writePrintString("\n");
}
/**
* Print direct raw EBCDIC text through translator.
*/
public synchronized void printText(byte[] text, int len) {
if (text == null || len <= 0) return;
int actualLen = Math.min(len, text.length);
char[] chars = new char[actualLen];
for (int i = 0; i < actualLen; i++) {
int ebc = text[i] & 0xFF;
chars[i] = (ebc == 0x00) ? ' ' : translator.ebcdicToUnicode(ebc);
}
pd.writePrintString(new String(chars));
}
/**
* Render the entire 3270 printer buffer according to current formatting parameters.
*/
public synchronized void formatBufferToPrint(int lineLength) {
int effLineLen = lineLength > 0 ? lineLength : cols;
for (int r = 0; r < rows; r++) {
printLine(r, effLineLen);
}
}
/**
* Flush current printer buffer to PD3270 destination.
*/
public synchronized void flushPrintBuffer() {
int lineLen = cols;
switch (currentPrintFormat) {
case PrinterConstants.PRINT_FMT_40_COL: lineLen = 40; break;
case PrinterConstants.PRINT_FMT_64_COL: lineLen = 64; break;
case PrinterConstants.PRINT_FMT_80_COL: lineLen = 80; break;
default: lineLen = cols; break;
}
formatBufferToPrint(lineLen);
pd.flush();
}
// ========== Attribute & WCC Calculation Helpers ==========
public boolean isStartPrint(int wcc) {
return (wcc & PrinterConstants.WCC_START_PRINT_BIT) != 0;
}
public int getPrintFormat(int wcc) {
return wcc & PrinterConstants.WCC_PRINT_FORMAT_MASK;
}
public int calculateColor(int colorAttr) {
// Map IBM 3270 color attribute code (0xF1=Blue, 0xF2=Red, 0xF3=Pink, 0xF4=Green, 0xF5=Turquoise, 0xF6=Yellow, 0xF7=White)
switch (colorAttr) {
case 0xF1: return 1; // Blue
case 0xF2: return 2; // Red
case 0xF3: return 3; // Pink
case 0xF4: return 4; // Green
case 0xF5: return 5; // Turquoise
case 0xF6: return 6; // Yellow
case 0xF7: return 7; // Neutral White
default: return 0; // Default Neutral
}
}
public int calculateHighlight(int hiliteAttr) {
switch (hiliteAttr) {
case PrinterConstants.SEAC_BLINK: return 1;
case PrinterConstants.SEAC_REVERSE: return 2;
case PrinterConstants.SEAC_UNDERLINE: return 4;
default: return 0;
}
}
public int calculateCharset(int csAttr) {
return csAttr & 0xFF;
}
// ========== Accessors ==========
public int getRows() { return rows; }
public int getCols() { return cols; }
public int getBufferSize() { return bufferSize; }
public int getBufferAddress() { return bufferAddress; }
public PD3270 getPD() { return pd; }
public EbcdicTranslator getTranslator() { return translator; }
}
@@ -0,0 +1,586 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* SCS (SNA Character String) Interpreter for IBM LU Type 1 Printer Sessions.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintSCS3270.
*/
public class PrintSCS3270 {
private static final Logger log = Logger.getLogger(PrintSCS3270.class.getName());
private final PrinterConfig config;
private final PD3270 pd;
private final EbcdicTranslator translator;
// Formatting Parameters
private int mpp = PrinterConstants.DEFAULT_MPP; // Maximum Presentation Position (Line Length)
private int mpl = PrinterConstants.DEFAULT_MPL; // Maximum Page Length
private int leftMargin = 1;
private int rightMargin = PrinterConstants.DEFAULT_MPP;
private int topMargin = 1;
private int bottomMargin = PrinterConstants.DEFAULT_MPL;
private int cpi = PrinterConstants.DEFAULT_CPI;
private int lpi = PrinterConstants.DEFAULT_LPI;
// Tab Stops (1-based arrays)
private int[] horizontalTabs = new int[0];
private int[] verticalTabs = new int[0];
// Current State
private int currentRow = 1;
private int currentCol = 1;
private boolean doubleWidth = false;
private int activeColor = 0;
private int activeHighlight = PrinterConstants.SEAC_DEFAULT;
private int textOrientation = 0;
private boolean presentationEnabled = true;
// Line buffering for print composition
private char[] lineBuffer;
private boolean lineModified = false;
public PrintSCS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
resetSCSFormatDefaults();
}
/**
* Reset all SCS formatting parameters and presentation state to initial defaults.
*/
public synchronized void resetSCSFormatDefaults() {
this.mpp = config.getMpp();
this.mpl = config.getMpl();
this.leftMargin = config.getLeftMargin();
this.rightMargin = config.getRightMargin();
this.topMargin = config.getTopMargin();
this.bottomMargin = config.getBottomMargin();
this.cpi = config.getCpi();
this.lpi = config.getLpi();
this.horizontalTabs = new int[0];
this.verticalTabs = new int[0];
this.currentRow = this.topMargin;
this.currentCol = this.leftMargin;
this.doubleWidth = false;
this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
this.textOrientation = 0;
this.presentationEnabled = true;
this.lineBuffer = new char[Math.max(256, mpp + 1)];
Arrays.fill(lineBuffer, ' ');
this.lineModified = false;
}
/**
* Process host data stream containing SCS orders and characters.
* @param data Byte array from host.
* @param offset Starting offset.
* @param length Number of bytes to process.
*/
public synchronized void processHostData(byte[] data, int offset, int length) {
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
return;
}
int idx = offset;
int end = offset + length;
while (idx < end) {
int b = data[idx] & 0xFF;
// Check 2-byte SCS prefix (0x2B)
if (b == PrinterConstants.SCS_PREFIX_2B) {
if (idx + 1 >= end) {
idx++;
break;
}
int subOrder = data[idx + 1] & 0xFF;
int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0;
int orderTotalLen = 2 + (paramLen > 0 ? (paramLen + 1) : 1); // 0x2B + SubOrder + paramLen + payload
// In standard SCS, paramLen byte specifies length of following parameters
// or paramLen is total length including length byte.
int bytesAvailable = end - idx;
int sliceLen = Math.min(orderTotalLen, bytesAvailable);
switch (subOrder) {
case PrinterConstants.SCS_SHF:
processSetHorizontalFormat(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SVF:
processSetVerticalFormat(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SLD:
processSetLineDensity(data, idx, sliceLen);
break;
case PrinterConstants.SCS_STO:
processSetTextOrientation(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SEAC:
processSetEnhancedAttribute(data, idx, sliceLen);
break;
case PrinterConstants.SCS_PPA:
processPresentationPositionAdvancing(data, idx, sliceLen);
break;
case PrinterConstants.SCS_PPV:
processPresentationPositionVertical(data, idx, sliceLen);
break;
default:
log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder));
break;
}
idx += sliceLen;
continue;
}
// Check SA (Set Attribute 0x28)
if (b == PrinterConstants.SCS_SA) {
if (idx + 2 < end) {
processSetAttribute(data, idx, 3);
idx += 3;
} else {
idx = end;
}
continue;
}
// Check TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRS) {
if (idx + 1 < end) {
int trsLen = data[idx + 1] & 0xFF;
int actualTrs = Math.min(trsLen, end - (idx + 2));
processTransparentStream(data, idx + 2, actualTrs);
idx += 2 + actualTrs;
} else {
idx = end;
}
continue;
}
// Check Single-Byte SCS Controls
switch (b) {
case PrinterConstants.SCS_NUL:
// Null - ignored
idx++;
break;
case PrinterConstants.SCS_CR:
carriageReturn();
idx++;
break;
case PrinterConstants.SCS_LF:
lineFeed();
idx++;
break;
case PrinterConstants.SCS_NL:
case PrinterConstants.SCS_RNLS:
newLine();
idx++;
break;
case PrinterConstants.SCS_FF:
formFeed();
idx++;
break;
case PrinterConstants.SCS_BS:
case PrinterConstants.SCS_NBS:
backspace();
idx++;
break;
case PrinterConstants.SCS_HT:
processHorizontalTab();
idx++;
break;
case PrinterConstants.SCS_VT:
processVerticalTab();
idx++;
break;
case PrinterConstants.SCS_ENP:
presentationEnabled = true;
idx++;
break;
case PrinterConstants.SCS_INP:
presentationEnabled = false;
idx++;
break;
case PrinterConstants.SCS_BEL:
// Sound alarm
idx++;
break;
case PrinterConstants.SCS_GE:
if (idx + 1 < end) {
int geChar = data[idx + 1] & 0xFF;
processGraphicEscapeChar(geChar);
idx += 2;
} else {
idx++;
}
break;
case PrinterConstants.SCS_SP:
case PrinterConstants.SCS_RSP:
printCharacter(' ');
idx++;
break;
default:
// Standard printable character
if (presentationEnabled) {
char ch = translator.ebcdicToUnicode(b);
printCharacter(ch);
}
idx++;
break;
}
}
}
private void printCharacter(char c) {
if (currentCol > rightMargin || currentCol > mpp) {
advanceToNextLine();
}
ensureLineBufferSize(currentCol + (doubleWidth ? 2 : 1));
lineBuffer[currentCol - 1] = c;
lineModified = true;
currentCol += (doubleWidth ? 2 : 1);
}
private void processGraphicEscapeChar(int ec) {
char unicode = translator.getAplGraphic(ec);
if (unicode == 0 || unicode == ' ') {
unicode = translator.ebcdicToUnicode(ec);
}
printCharacter(unicode);
}
private void ensureLineBufferSize(int requiredSize) {
if (requiredSize > lineBuffer.length) {
int newSize = Math.max(requiredSize + 32, lineBuffer.length * 2);
char[] newBuf = new char[newSize];
Arrays.fill(newBuf, ' ');
System.arraycopy(lineBuffer, 0, newBuf, 0, lineBuffer.length);
lineBuffer = newBuf;
}
}
/**
* Flush the current line buffer to PD3270.
*/
public synchronized void flushLineBuffer() {
if (lineModified) {
// Trim trailing spaces
int len = lineBuffer.length;
while (len > 0 && lineBuffer[len - 1] == ' ') {
len--;
}
if (len > 0) {
pd.writePrintString(new String(lineBuffer, 0, len));
}
}
pd.writePrintString("\n");
Arrays.fill(lineBuffer, ' ');
lineModified = false;
}
// ========== SCS Order Implementations ==========
public synchronized void carriageReturn() {
currentCol = leftMargin;
}
public synchronized void lineFeed() {
flushLineBuffer();
currentRow++;
if (currentRow > bottomMargin || currentRow > mpl) {
advanceToNextPage();
}
}
public synchronized void newLine() {
carriageReturn();
lineFeed();
}
public synchronized void formFeed() {
flushLineBuffer();
pd.formFeed();
currentRow = topMargin;
currentCol = leftMargin;
}
public synchronized void backspace() {
if (currentCol > leftMargin) {
currentCol -= (doubleWidth ? 2 : 1);
if (currentCol < leftMargin) currentCol = leftMargin;
}
}
public synchronized void advanceToNextLine() {
newLine();
}
public synchronized void advanceToNextPage() {
formFeed();
}
// ========== Tab Stops and Calculations ==========
public synchronized int calculateHorizontalTab(int currentPos) {
if (horizontalTabs != null && horizontalTabs.length > 0) {
for (int tab : horizontalTabs) {
if (tab > currentPos) {
return tab;
}
}
}
// Default tab stop: advance to next multiple of 8 + 1
int nextTab = ((currentPos / 8) + 1) * 8 + 1;
return Math.min(nextTab, rightMargin);
}
public synchronized int calculateVerticalTab(int currentLine) {
if (verticalTabs != null && verticalTabs.length > 0) {
for (int tab : verticalTabs) {
if (tab > currentLine) {
return tab;
}
}
}
return -1; // No more vertical tabs on this page
}
public synchronized void processHorizontalTab() {
int nextTab = calculateHorizontalTab(currentCol);
if (nextTab <= rightMargin) {
currentCol = nextTab;
} else {
advanceToNextLine();
}
}
public synchronized void processVerticalTab() {
int nextTab = calculateVerticalTab(currentRow);
if (nextTab > 0 && nextTab <= bottomMargin) {
while (currentRow < nextTab) {
lineFeed();
}
} else {
advanceToNextPage();
}
}
// ========== Multi-Byte Order Parsers ==========
public synchronized void setHorizontalFormat(int lineLength, int[] tabs) {
this.mpp = lineLength > 0 ? lineLength : PrinterConstants.DEFAULT_MPP;
if (this.rightMargin > this.mpp) {
this.rightMargin = this.mpp;
}
if (tabs != null) {
this.horizontalTabs = Arrays.copyOf(tabs, tabs.length);
Arrays.sort(this.horizontalTabs);
}
}
public synchronized void setVerticalFormat(int pageLength, int[] tabs) {
this.mpl = pageLength > 0 ? pageLength : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) {
this.bottomMargin = this.mpl;
}
if (tabs != null) {
this.verticalTabs = Arrays.copyOf(tabs, tabs.length);
Arrays.sort(this.verticalTabs);
}
}
public synchronized void setHorizontalMargins(int leftMargin, int rightMargin) {
this.leftMargin = Math.max(1, leftMargin);
this.rightMargin = Math.min(this.mpp, Math.max(this.leftMargin, rightMargin));
if (currentCol < this.leftMargin) currentCol = this.leftMargin;
}
public synchronized void setVerticalMargins(int topMargin, int bottomMargin) {
this.topMargin = Math.max(1, topMargin);
this.bottomMargin = Math.min(this.mpl, Math.max(this.topMargin, bottomMargin));
if (currentRow < this.topMargin) currentRow = this.topMargin;
}
public synchronized void setPrintDensity(int cpi, int lpi) {
if (cpi > 0) this.cpi = cpi;
if (lpi > 0) this.lpi = lpi;
}
public synchronized void setEnhancedHighlight(int highlightType) {
this.activeHighlight = highlightType;
}
public synchronized void startDoubleWidthCharacters() {
this.doubleWidth = true;
}
public synchronized void endDoubleWidthCharacters() {
this.doubleWidth = false;
}
public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) {
if (len < 4) return;
int mppVal = data[offset + 3] & 0xFF;
if (mppVal > 0) {
this.mpp = mppVal;
this.rightMargin = Math.min(this.rightMargin, this.mpp);
}
if (len >= 6) {
int lm = data[offset + 4] & 0xFF;
int rm = data[offset + 5] & 0xFF;
if (lm > 0) this.leftMargin = lm;
if (rm > 0 && rm >= lm) this.rightMargin = rm;
}
if (len > 6) {
int tabCount = len - 6;
int[] tabs = new int[tabCount];
for (int i = 0; i < tabCount; i++) {
tabs[i] = data[offset + 6 + i] & 0xFF;
}
Arrays.sort(tabs);
this.horizontalTabs = tabs;
}
}
public synchronized void processSetVerticalFormat(byte[] data, int offset, int len) {
if (len < 4) return;
int mplVal = data[offset + 3] & 0xFF;
if (mplVal > 0) {
this.mpl = mplVal;
this.bottomMargin = Math.min(this.bottomMargin, this.mpl);
}
if (len >= 6) {
int tm = data[offset + 4] & 0xFF;
int bm = data[offset + 5] & 0xFF;
if (tm > 0) this.topMargin = tm;
if (bm > 0 && bm >= tm) this.bottomMargin = bm;
}
if (len > 6) {
int tabCount = len - 6;
int[] tabs = new int[tabCount];
for (int i = 0; i < tabCount; i++) {
tabs[i] = data[offset + 6 + i] & 0xFF;
}
Arrays.sort(tabs);
this.verticalTabs = tabs;
}
}
public synchronized void processSetLineDensity(byte[] data, int offset, int len) {
if (len < 4) return;
int points = data[offset + 3] & 0xFF;
if (points > 0) {
// Line density in points / inch (72 points = 1 inch)
// 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI
this.lpi = Math.max(1, 72 / points);
}
}
public synchronized void processSetTextOrientation(byte[] data, int offset, int len) {
if (len >= 4) {
this.textOrientation = data[offset + 3] & 0xFF;
}
}
public synchronized void processSetEnhancedAttribute(byte[] data, int offset, int len) {
if (len >= 5) {
int attrVal = data[offset + 4] & 0xFF;
setEnhancedHighlight(attrVal);
}
}
public synchronized void processPresentationPositionAdvancing(byte[] data, int offset, int len) {
if (len < 5) return;
int subfn = data[offset + 3] & 0xFF;
int val = data[offset + 4] & 0xFF;
if (subfn == PrinterConstants.POS_ABSOLUTE) {
currentCol = Math.min(mpp, Math.max(leftMargin, val));
} else if (subfn == PrinterConstants.POS_RELATIVE) {
currentCol = Math.min(mpp, currentCol + val);
}
}
public synchronized void processPresentationPositionVertical(byte[] data, int offset, int len) {
if (len < 5) return;
int subfn = data[offset + 3] & 0xFF;
int val = data[offset + 4] & 0xFF;
if (subfn == PrinterConstants.POS_ABSOLUTE) {
while (currentRow < val && currentRow < bottomMargin) {
lineFeed();
}
} else if (subfn == PrinterConstants.POS_RELATIVE) {
for (int i = 0; i < val && currentRow < bottomMargin; i++) {
lineFeed();
}
}
}
public synchronized void processSetAttribute(byte[] data, int offset, int len) {
if (len < 3) return;
int attrType = data[offset + 1] & 0xFF;
int attrVal = data[offset + 2] & 0xFF;
if (attrType == PrinterConstants.SA_COLOR) {
this.activeColor = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE) {
this.activeHighlight = attrVal;
} else if (attrType == PrinterConstants.SA_RESET) {
this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
this.doubleWidth = false;
}
}
public synchronized void processTransparentStream(byte[] data, int offset, int len) {
if (len > 0) {
pd.writePrintBytes(data, offset, len);
}
}
// ========== Accessors ==========
public int getLineLength() { return mpp; }
public void setLineLength(int len) { this.mpp = len; }
public int getPageLength() { return mpl; }
public void setPageLength(int len) { this.mpl = len; }
public int getLeftMargin() { return leftMargin; }
public int getRightMargin() { return rightMargin; }
public int getTopMargin() { return topMargin; }
public int getBottomMargin() { return bottomMargin; }
public int getCurrentRow() { return currentRow; }
public int getCurrentColumn() { return currentCol; }
public int getLinesPerInch() { return lpi; }
public int getCharsPerInch() { return cpi; }
public boolean isDoubleWidth() { return doubleWidth; }
public void setDoubleWidth(boolean dw) { this.doubleWidth = dw; }
public int getActiveColor() { return activeColor; }
public void setActiveColor(int color) { this.activeColor = color; }
public int getActiveHighlight() { return activeHighlight; }
public void setActiveHighlight(int hilite) { this.activeHighlight = hilite; }
public int getTextOrientation() { return textOrientation; }
public PD3270 getPD() { return pd; }
public EbcdicTranslator getTranslator() { return translator; }
}
@@ -0,0 +1,53 @@
package haus.nightmare.lib3270j.printer;
import java.util.EventObject;
/**
* Event object representing state and progress changes in a 3270/3287 printer session.
*/
public class PrintSessionEvent extends EventObject {
public static final int EVENT_JOB_STARTED = 1;
public static final int EVENT_JOB_DATA = 2;
public static final int EVENT_PAGE_COMPLETE = 3;
public static final int EVENT_JOB_COMPLETE = 4;
public static final int EVENT_STATUS_CHANGED = 5;
public static final int EVENT_ERROR = 6;
private final int eventType;
private final String jobName;
private final int pageNumber;
private final long byteCount;
private final int totalPages;
private final int statusCode;
private final String message;
private final byte[] data;
public PrintSessionEvent(Object source, int eventType, String jobName, int pageNumber,
long byteCount, int totalPages, int statusCode, String message, byte[] data) {
super(source);
this.eventType = eventType;
this.jobName = jobName;
this.pageNumber = pageNumber;
this.byteCount = byteCount;
this.totalPages = totalPages;
this.statusCode = statusCode;
this.message = message;
this.data = data;
}
public int getEventType() { return eventType; }
public String getJobName() { return jobName; }
public int getPageNumber() { return pageNumber; }
public long getByteCount() { return byteCount; }
public int getTotalPages() { return totalPages; }
public int getStatusCode() { return statusCode; }
public String getMessage() { return message; }
public byte[] getData() { return data; }
@Override
public String toString() {
return "PrintSessionEvent[type=" + eventType + ", page=" + pageNumber + ", bytes=" + byteCount +
", status=" + statusCode + ", msg=" + message + "]";
}
}
@@ -0,0 +1,27 @@
package haus.nightmare.lib3270j.printer;
import java.util.EventListener;
/**
* Listener interface for 3270/3287 printer session events.
*/
public interface PrintSessionListener extends EventListener {
/** Fired when a new print job begins. */
void onPrintJobStarted(PrintSessionEvent event);
/** Fired when raw or formatted print data is processed. */
void onPrintJobData(PrintSessionEvent event);
/** Fired when a page is completed (Form Feed). */
void onPrintJobPageComplete(PrintSessionEvent event);
/** Fired when the print job is completed (End Of Job). */
void onPrintJobComplete(PrintSessionEvent event);
/** Fired when the printer session status changes. */
void onPrinterStatusChanged(PrintSessionEvent event);
/** Fired when a printer error occurs. */
void onPrinterError(PrintSessionEvent event);
}
@@ -0,0 +1,162 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.tls.TlsCertificateVerifier;
/**
* Configuration parameters for an IBM 3287 / 3286 printer session.
*/
public class PrinterConfig {
public enum DestinationType {
MEMORY,
FILE,
COMMAND,
LISTENER
}
private String host;
private int port = 23;
private boolean useTls = false;
private boolean tlsVerifyCert = true;
private TlsCertificateVerifier certificateVerifier = null;
private String sslProtocol = "TLS";
private int connectTimeoutMs = 15000;
private int soTimeoutMs = 0;
private boolean tcpNoDelay = true;
private boolean soKeepAlive = true;
// LU and Association
private String printerLuName = null;
private String associatedDisplayLuName = null;
private String deviceType = PrinterConstants.DEV_IBM_3287_1;
private String codePage = "037";
// Geometry and formatting
private int mpp = PrinterConstants.DEFAULT_MPP;
private int mpl = PrinterConstants.DEFAULT_MPL;
private int leftMargin = 1;
private int rightMargin = PrinterConstants.DEFAULT_MPP;
private int topMargin = 1;
private int bottomMargin = PrinterConstants.DEFAULT_MPL;
private int cpi = PrinterConstants.DEFAULT_CPI;
private int lpi = PrinterConstants.DEFAULT_LPI;
// Spool destination
private DestinationType destinationType = DestinationType.MEMORY;
private String destinationTarget = null;
private boolean formFeedAtEoj = true;
private boolean autoFlushOnEoj = true;
private boolean autoReconnect = false;
public PrinterConfig() {}
public PrinterConfig(String host, int port) {
this.host = host;
this.port = port;
}
public PrinterConfig(String host, int port, String printerLuName) {
this.host = host;
this.port = port;
this.printerLuName = printerLuName;
}
public PrinterConfig(String host, int port, String printerLuName, boolean useTls) {
this.host = host;
this.port = port;
this.printerLuName = printerLuName;
this.useTls = useTls;
}
// Getters and Setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
public TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
public void setCertificateVerifier(TlsCertificateVerifier certificateVerifier) { this.certificateVerifier = certificateVerifier; }
public String getSslProtocol() { return sslProtocol; }
public void setSslProtocol(String sslProtocol) { this.sslProtocol = sslProtocol; }
public int getConnectTimeoutMs() { return connectTimeoutMs; }
public void setConnectTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; }
public int getSoTimeoutMs() { return soTimeoutMs; }
public void setSoTimeoutMs(int soTimeoutMs) { this.soTimeoutMs = soTimeoutMs; }
public boolean isTcpNoDelay() { return tcpNoDelay; }
public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; }
public boolean isSoKeepAlive() { return soKeepAlive; }
public void setSoKeepAlive(boolean soKeepAlive) { this.soKeepAlive = soKeepAlive; }
public String getPrinterLuName() { return printerLuName; }
public void setPrinterLuName(String printerLuName) { this.printerLuName = printerLuName; }
public String getAssociatedDisplayLuName() { return associatedDisplayLuName; }
public void setAssociatedDisplayLuName(String associatedDisplayLuName) { this.associatedDisplayLuName = associatedDisplayLuName; }
public String getDeviceType() { return deviceType; }
public void setDeviceType(String deviceType) {
this.deviceType = (deviceType != null && !deviceType.trim().isEmpty()) ? deviceType.trim() : PrinterConstants.DEV_IBM_3287_1;
}
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) {
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
}
public int getMpp() { return mpp; }
public void setMpp(int mpp) {
this.mpp = mpp > 0 ? mpp : PrinterConstants.DEFAULT_MPP;
if (this.rightMargin > this.mpp) this.rightMargin = this.mpp;
}
public int getMpl() { return mpl; }
public void setMpl(int mpl) {
this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl;
}
public int getLeftMargin() { return leftMargin; }
public void setLeftMargin(int leftMargin) { this.leftMargin = Math.max(1, leftMargin); }
public int getRightMargin() { return rightMargin; }
public void setRightMargin(int rightMargin) { this.rightMargin = Math.min(mpp, Math.max(leftMargin, rightMargin)); }
public int getTopMargin() { return topMargin; }
public void setTopMargin(int topMargin) { this.topMargin = Math.max(1, topMargin); }
public int getBottomMargin() { return bottomMargin; }
public void setBottomMargin(int bottomMargin) { this.bottomMargin = Math.min(mpl, Math.max(topMargin, bottomMargin)); }
public int getCpi() { return cpi; }
public void setCpi(int cpi) { this.cpi = cpi > 0 ? cpi : PrinterConstants.DEFAULT_CPI; }
public int getLpi() { return lpi; }
public void setLpi(int lpi) { this.lpi = lpi > 0 ? lpi : PrinterConstants.DEFAULT_LPI; }
public DestinationType getDestinationType() { return destinationType; }
public void setDestinationType(DestinationType destinationType) { this.destinationType = destinationType != null ? destinationType : DestinationType.MEMORY; }
public String getDestinationTarget() { return destinationTarget; }
public void setDestinationTarget(String destinationTarget) { this.destinationTarget = destinationTarget; }
public boolean isFormFeedAtEoj() { return formFeedAtEoj; }
public void setFormFeedAtEoj(boolean formFeedAtEoj) { this.formFeedAtEoj = formFeedAtEoj; }
public boolean isAutoFlushOnEoj() { return autoFlushOnEoj; }
public void setAutoFlushOnEoj(boolean autoFlushOnEoj) { this.autoFlushOnEoj = autoFlushOnEoj; }
public boolean isAutoReconnect() { return autoReconnect; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
}
@@ -0,0 +1,118 @@
package haus.nightmare.lib3270j.printer;
/**
* Constants for IBM 3287 / 3286 Printer Sessions and SCS (SNA Character String) Protocol.
* Conforms to IBM Host On-Demand v14 (com.ibm.eNetwork.ECL.tn3270p.*) and RFC 2355.
*/
public final class PrinterConstants {
private PrinterConstants() {}
// ========== LU BIND Types ==========
public static final short LU_TYPE_UNKNOWN = 0;
public static final short LU_TYPE_1_SCS = 1;
public static final short LU_TYPE_3_DS = 3;
// ========== Device Type Names ==========
public static final String DEV_IBM_3287_1 = "IBM-3287-1";
public static final String DEV_IBM_3287_2 = "IBM-3287-2";
public static final String DEV_IBM_3286_2 = "IBM-3286-2";
public static final String DEV_IBM_387_1 = "IBM-387-1";
public static final String DEV_IBM_387_2 = "IBM-387-2";
// ========== SCS Single-Byte Control Codes (EBCDIC) ==========
public static final int SCS_NUL = 0x00; // Null
public static final int SCS_HT = 0x05; // Horizontal Tab
public static final int SCS_RNLS = 0x06; // Required New Line
public static final int SCS_RCR = 0x07; // Required Carriage Return
public static final int SCS_GE = 0x08; // Graphic Escape
public static final int SCS_VT = 0x0B; // Vertical Tab
public static final int SCS_FF = 0x0C; // Form Feed
public static final int SCS_CR = 0x0D; // Carriage Return
public static final int SCS_ENP = 0x14; // Enable Presentation
public static final int SCS_NL = 0x15; // New Line
public static final int SCS_BS = 0x16; // Backspace
public static final int SCS_POC = 0x17; // Program Operator Communication
public static final int SCS_INP = 0x24; // Inhibit Presentation
public static final int SCS_LF = 0x25; // Line Feed
public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm
public static final int SCS_TRS = 0x35; // Transparent Stream (0x35 <len> <bytes>)
public static final int SCS_NBS = 0x36; // Numeric Backspace
public static final int SCS_SP = 0x40; // Space
public static final int SCS_RSP = 0x41; // Required Space
// ========== SCS Multi-Byte Order Prefixes ==========
public static final int SCS_PREFIX_2B = 0x2B; // 2-byte SCS prefix
public static final int SCS_SA = 0x28; // Set Attribute (0x28 <type> <val>)
// 0x2B Sub-orders
public static final int SCS_SHF = 0xD1; // Set Horizontal Format
public static final int SCS_SVF = 0xD2; // Set Vertical Format
public static final int SCS_STO = 0xD3; // Set Text Orientation
public static final int SCS_SCS = 0xD4; // Select Character Set
public static final int SCS_SEAC = 0xD5; // Set Enhanced Attribute / Highlight
public static final int SCS_SLD = 0xD6; // Set Line Density
public static final int SCS_PPV = 0xC4; // Presentation Position Vertical
public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal)
// SA Attribute Types
public static final int SA_RESET = 0x00;
public static final int SA_HILITE = 0x41;
public static final int SA_COLOR = 0x42;
public static final int SA_CHARSET = 0x43;
// SEAC Highlight Values
public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_BLINK = 0xF1;
public static final int SEAC_REVERSE = 0xF2;
public static final int SEAC_UNDERLINE = 0xF4;
// PPA/PPV Positioning Types
public static final int POS_ABSOLUTE = 0x01; // Absolute position (1-based)
public static final int POS_RELATIVE = 0x02; // Relative offset
// ========== 3270 Print Data Stream Constants (LU3) ==========
public static final int WCC_START_PRINT_BIT = 0x08; // Start Print in 3270 WCC
public static final int WCC_PRINT_FORMAT_MASK = 0x03; // Formatting: 00=unformatted, 01=40 col, 10=64 col, 11=80 col
public static final int PRINT_FMT_UNFORMATTED = 0;
public static final int PRINT_FMT_40_COL = 1;
public static final int PRINT_FMT_64_COL = 2;
public static final int PRINT_FMT_80_COL = 3;
// 3270 Orders in Print Stream
public static final int ORDER_SBA = 0x11;
public static final int ORDER_SF = 0x1D;
public static final int ORDER_SFE = 0x29;
public static final int ORDER_SA = 0x28;
public static final int ORDER_MF = 0x2C;
public static final int ORDER_IC = 0x13;
public static final int ORDER_PT = 0x05;
public static final int ORDER_RA = 0x3C;
public static final int ORDER_EUA = 0x12;
public static final int ORDER_GE = 0x08;
public static final int ORDER_NL = 0x15;
public static final int ORDER_EM = 0x19; // End of Message
public static final int ORDER_FF = 0x0C;
public static final int ORDER_CR = 0x0D;
// ========== Printer Session Status Codes ==========
public static final int STATUS_CONNECTING = 650;
public static final int STATUS_NEGOTIATING = 651;
public static final int STATUS_CONNECTED = 652;
public static final int STATUS_SECURITY = 654;
public static final int STATUS_BIND_ERROR = 655;
public static final int STATUS_DISCONNECTED = 656;
public static final int STATUS_PRINTER_READY = 700;
public static final int STATUS_PRINTING = 701;
public static final int STATUS_PAGE_COMPLETE = 702;
public static final int STATUS_JOB_COMPLETE = 703;
public static final int STATUS_PRINTER_BUSY = 704;
public static final int STATUS_PRINTER_ERROR = 705;
public static final int STATUS_PRINTER_CLOSED = 706;
// Default Geometry
public static final int DEFAULT_MPP = 80; // Default line length
public static final int DEFAULT_MPL = 66; // Default page length (11 inches @ 6 LPI)
public static final int DEFAULT_CPI = 10; // 10 chars per inch
public static final int DEFAULT_LPI = 6; // 6 lines per inch
}
@@ -0,0 +1,765 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import haus.nightmare.lib3270j.tls.TlsTrustManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* TN3270E Printer Connection & Protocol Engine (Telnet3270EP).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.Telnet3270EP.
*/
public class Telnet3270EP implements Runnable {
private static final Logger log = Logger.getLogger(Telnet3270EP.class.getName());
private final PrinterConfig config;
private final PD3270 pd;
private final EbcdicTranslator translator;
private final PrintSCS3270 scs;
private final PrintPS3270 printPs;
private final List<PrintSessionListener> listeners = new CopyOnWriteArrayList<>();
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
private Thread readerThread;
private volatile boolean running = false;
private volatile boolean connected = false;
// Protocol State
private int statusCode = PrinterConstants.STATUS_DISCONNECTED;
private short activeLuType = PrinterConstants.LU_TYPE_UNKNOWN;
private String assignedLuName = null;
private String negotiatedDeviceType = null;
private boolean tn3270eMode = false;
private final boolean[] negotiatedFunctions = new boolean[10];
// Sequence tracking
private int sendSeqNumber = 0;
private int lastRecvSeqNumber = 0;
private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE;
// Buffer for Telnet streaming
private final ByteArrayOutputStream recordBuffer = new ByteArrayOutputStream();
private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream();
private boolean inSubnegotiation = false;
public Telnet3270EP(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.pd = new PD3270(this.config);
this.translator = new EbcdicTranslator(this.config.getCodePage());
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
this.printPs = new PrintPS3270(this.config, this.pd, this.translator);
}
public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
this.printPs = new PrintPS3270(this.config, this.pd, this.translator);
}
// ========== Connection Lifecycle (Fn #1) ==========
/**
* Open connection to TN3270E printer host matching open() specification.
*/
public synchronized boolean open() {
if (connected || running) {
close();
}
updateStatus(PrinterConstants.STATUS_CONNECTING, "Connecting to host " + config.getHost() + ":" + config.getPort());
try {
if (config.isUseTls()) {
SSLContext sslContext = SSLContext.getInstance(config.getSslProtocol() != null ? config.getSslProtocol() : "TLS");
TrustManager[] tm = new TrustManager[]{new TlsTrustManager(config.isTlsVerifyCert(), config.getCertificateVerifier())};
sslContext.init(null, tm, new SecureRandom());
SSLSocketFactory factory = sslContext.getSocketFactory();
SSLSocket sslSocket = (SSLSocket) factory.createSocket();
if (config.getConnectTimeoutMs() > 0) {
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()), config.getConnectTimeoutMs());
} else {
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()));
}
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
sslSocket.setKeepAlive(config.isSoKeepAlive());
if (config.getSoTimeoutMs() > 0) {
sslSocket.setSoTimeout(config.getSoTimeoutMs());
}
sslSocket.startHandshake();
this.socket = sslSocket;
} else {
Socket sock = new Socket();
if (config.getConnectTimeoutMs() > 0) {
sock.connect(new InetSocketAddress(config.getHost(), config.getPort()), config.getConnectTimeoutMs());
} else {
sock.connect(new InetSocketAddress(config.getHost(), config.getPort()));
}
sock.setTcpNoDelay(config.isTcpNoDelay());
sock.setKeepAlive(config.isSoKeepAlive());
if (config.getSoTimeoutMs() > 0) {
sock.setSoTimeout(config.getSoTimeoutMs());
}
this.socket = sock;
}
this.inputStream = new BufferedInputStream(socket.getInputStream());
this.outputStream = new BufferedOutputStream(socket.getOutputStream());
this.connected = true;
this.running = true;
reset();
updateStatus(PrinterConstants.STATUS_NEGOTIATING, "Connected, negotiating TN3270E");
readerThread = new Thread(this, "Telnet3270EP-Reader-" + config.getHost());
readerThread.setDaemon(true);
readerThread.start();
// Initiate Telnet negotiation: DO TN3270E
sendTelnetCommand(TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
return true;
} catch (Exception e) {
log.log(Level.SEVERE, "Failed to connect to printer host", e);
updateStatus(PrinterConstants.STATUS_PRINTER_ERROR, "Connection failed: " + e.getMessage());
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, e.getMessage());
close();
return false;
}
}
/**
* Close printer connection cleanly.
*/
public synchronized void close() {
running = false;
connected = false;
if (socket != null) {
try {
socket.close();
} catch (IOException ignored) {}
socket = null;
}
if (readerThread != null) {
readerThread.interrupt();
readerThread = null;
}
pd.closePrinter();
updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected");
}
/**
* Reset printer session state.
*/
public synchronized void reset() {
this.activeLuType = PrinterConstants.LU_TYPE_UNKNOWN;
this.assignedLuName = null;
this.negotiatedDeviceType = null;
this.tn3270eMode = false;
Arrays.fill(negotiatedFunctions, false);
this.sendSeqNumber = 0;
this.lastRecvSeqNumber = 0;
this.recordBuffer.reset();
this.subnegBuffer.reset();
this.inSubnegotiation = false;
scs.resetSCSFormatDefaults();
printPs.erasePrintBuffer();
}
/**
* Send raw data stream to host with IAC (0xFF) escaping.
*/
public synchronized void send(byte[] data, int length) {
if (!connected || outputStream == null || data == null || length <= 0) {
return;
}
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int i = 0; i < length && i < data.length; i++) {
int b = data[i] & 0xFF;
out.write(b);
if (b == TelnetConstants.IAC) {
out.write(TelnetConstants.IAC); // 0xFF escaping
}
}
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.log(Level.WARNING, "Error sending data to printer host", e);
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, "Send error: " + e.getMessage());
}
}
// ========== BIND, Ready & EOJ Protocol Handlers (Fn #2, #3, #4) ==========
/**
* Process SNA BIND image to determine printer LU type (LU1 SCS vs LU3 3270 DS).
* @param bindType Inferred or explicit bind type (LU_TYPE_1_SCS or LU_TYPE_3_DS).
*/
public synchronized void process_bind(short bindType) {
this.activeLuType = bindType;
log.info("Printer session BIND accepted, LU type = " +
(activeLuType == PrinterConstants.LU_TYPE_1_SCS ? "LU-1 (SCS)" : "LU-3 (3270 DS)"));
updateStatus(PrinterConstants.STATUS_PRINTER_READY, "Printer session bound as " +
(activeLuType == PrinterConstants.LU_TYPE_1_SCS ? "LU-1" : "LU-3"));
sendPrinterReady();
}
/**
* Parse SNA BIND image payload and extract LU type and parameters.
*/
public synchronized void processBindImage(byte[] bindData, int offset, int length) {
if (bindData == null || length < 14) {
process_bind(PrinterConstants.LU_TYPE_1_SCS);
return;
}
// In SNA BIND request:
// Byte 14 (profile byte / secondary LU type):
// 0x01 = LU Type 1 (SCS Printer)
// 0x03 = LU Type 3 (3270 Printer Data Stream)
int profile = bindData[offset + 14] & 0xFF;
short inferredLuType = (profile == 0x03) ? PrinterConstants.LU_TYPE_3_DS : PrinterConstants.LU_TYPE_1_SCS;
process_bind(inferredLuType);
}
/**
* Send TN3270E Positive Response indicating printer readiness (DEVICE-END).
*/
public synchronized void sendPrinterReady() {
sendTN3270EResponse(lastRecvSeqNumber, TN3270EConstants.RSF_POSITIVE_RESPONSE, TN3270EConstants.POS_DEVICE_END);
}
/**
* Handle End-Of-Job indicator from host.
* @param isComplete true if print job is complete.
*/
public synchronized void sendEOJ(boolean isComplete) {
log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete);
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
firePrintJobComplete(pd.getPageCount(), pd.getByteCount());
updateStatus(PrinterConstants.STATUS_JOB_COMPLETE, "Print job completed (" + pd.getPageCount() + " pages)");
// Send EOJ acknowledgment to host if required
if (lastResponseRequired == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(lastRecvSeqNumber);
}
}
// ========== TN3270E Responses & Packet Construction ==========
public synchronized void sendTN3270EResponse(int seqNumber, int respType, int respCode) {
if (!tn3270eMode || outputStream == null) return;
byte[] resp = new byte[TN3270EConstants.EH_SIZE + 1];
resp[0] = (byte) TN3270EConstants.DT_RESPONSE;
resp[1] = 0; // Request flag
resp[2] = (byte) respType;
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
resp[4] = (byte) (seqNumber & 0xFF);
resp[5] = (byte) respCode;
sendTN3270ERecord(resp);
}
public synchronized void sendTN3270EPositiveResponse(int seqNumber) {
sendTN3270EResponse(seqNumber, TN3270EConstants.RSF_POSITIVE_RESPONSE, TN3270EConstants.POS_DEVICE_END);
}
public synchronized void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
sendTN3270EResponse(seqNumber, TN3270EConstants.RSF_NEGATIVE_RESPONSE, negCode);
}
private synchronized void sendTN3270ERecord(byte[] record) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (byte b : record) {
int ub = b & 0xFF;
out.write(ub);
if (ub == TelnetConstants.IAC) {
out.write(TelnetConstants.IAC);
}
}
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.EOR);
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.log(Level.WARNING, "Error sending TN3270E record", e);
}
}
private synchronized void sendTelnetCommand(int command, int option) throws IOException {
outputStream.write(new byte[]{(byte) TelnetConstants.IAC, (byte) command, (byte) option});
outputStream.flush();
}
// ========== Telnet & TN3270E Protocol I/O Loop ==========
@Override
public void run() {
byte[] buf = new byte[8192];
try {
while (running && connected) {
int read = inputStream.read(buf);
if (read < 0) {
log.info("Host closed socket connection");
break;
}
processIncomingBytes(buf, 0, read);
}
} catch (IOException e) {
if (running) {
log.log(Level.WARNING, "Connection exception in printer reader thread", e);
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, e.getMessage());
}
} finally {
close();
}
}
private void processIncomingBytes(byte[] data, int offset, int length) {
int idx = offset;
int end = offset + length;
while (idx < end) {
int b = data[idx++] & 0xFF;
if (inSubnegotiation) {
if (b == TelnetConstants.IAC) {
if (idx < end && (data[idx] & 0xFF) == TelnetConstants.SE) {
idx++;
inSubnegotiation = false;
processSubnegotiation(subnegBuffer.toByteArray());
subnegBuffer.reset();
} else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) {
subnegBuffer.write(TelnetConstants.IAC);
idx++;
}
} else {
subnegBuffer.write(b);
}
continue;
}
if (b == TelnetConstants.IAC) {
if (idx >= end) break;
int cmd = data[idx++] & 0xFF;
switch (cmd) {
case TelnetConstants.IAC:
recordBuffer.write(TelnetConstants.IAC);
break;
case TelnetConstants.SB:
inSubnegotiation = true;
subnegBuffer.reset();
break;
case TelnetConstants.EOR:
processCompleteRecord(recordBuffer.toByteArray());
recordBuffer.reset();
break;
case TelnetConstants.DO:
if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
break;
case TelnetConstants.DONT:
if (idx < end) handleTelnetDont(data[idx++] & 0xFF);
break;
case TelnetConstants.WILL:
if (idx < end) handleTelnetWill(data[idx++] & 0xFF);
break;
case TelnetConstants.WONT:
if (idx < end) handleTelnetWont(data[idx++] & 0xFF);
break;
default:
break;
}
} else {
recordBuffer.write(b);
}
}
}
private void handleTelnetDo(int option) {
try {
if (option == TelnetConstants.TELOPT_TN3270E) {
sendTelnetCommand(TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
tn3270eMode = true;
} else if (option == TelnetConstants.TELOPT_BINARY || option == TelnetConstants.TELOPT_EOR) {
sendTelnetCommand(TelnetConstants.WILL, option);
} else {
sendTelnetCommand(TelnetConstants.WONT, option);
}
} catch (IOException e) {
log.warning("Error responding to Telnet DO: " + e.getMessage());
}
}
private void handleTelnetDont(int option) {
try {
sendTelnetCommand(TelnetConstants.WONT, option);
} catch (IOException ignored) {}
}
private void handleTelnetWill(int option) {
try {
if (option == TelnetConstants.TELOPT_TN3270E) {
sendTelnetCommand(TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
tn3270eMode = true;
} else if (option == TelnetConstants.TELOPT_BINARY || option == TelnetConstants.TELOPT_EOR) {
sendTelnetCommand(TelnetConstants.DO, option);
} else {
sendTelnetCommand(TelnetConstants.DONT, option);
}
} catch (IOException e) {
log.warning("Error responding to Telnet WILL: " + e.getMessage());
}
}
private void handleTelnetWont(int option) {
try {
sendTelnetCommand(TelnetConstants.DONT, option);
} catch (IOException ignored) {}
}
private void processSubnegotiation(byte[] sb) {
if (sb.length == 0) return;
int opt = sb[0] & 0xFF;
if (opt == TelnetConstants.TELOPT_TN3270E && sb.length > 1) {
int op = sb[1] & 0xFF;
switch (op) {
case TN3270EConstants.OP_SEND:
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_DEVICE_TYPE) {
sendDeviceTypeRequest();
}
break;
case TN3270EConstants.OP_DEVICE_TYPE:
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_IS) {
parseDeviceTypeIs(sb);
sendFunctionsRequest();
}
break;
case TN3270EConstants.OP_FUNCTIONS:
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_IS) {
parseFunctionsIs(sb);
} else if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_REQUEST) {
parseFunctionsRequest(sb);
}
break;
default:
break;
}
}
}
private void sendDeviceTypeRequest() {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SB);
out.write(TelnetConstants.TELOPT_TN3270E);
out.write(TN3270EConstants.OP_DEVICE_TYPE);
out.write(TN3270EConstants.OP_REQUEST);
String devType = config.getDeviceType() != null ? config.getDeviceType() : PrinterConstants.DEV_IBM_3287_1;
out.write(devType.getBytes(StandardCharsets.US_ASCII));
if (config.getAssociatedDisplayLuName() != null && !config.getAssociatedDisplayLuName().trim().isEmpty()) {
out.write(TN3270EConstants.OP_ASSOCIATE);
out.write(config.getAssociatedDisplayLuName().trim().getBytes(StandardCharsets.US_ASCII));
} else if (config.getPrinterLuName() != null && !config.getPrinterLuName().trim().isEmpty()) {
out.write(TN3270EConstants.OP_CONNECT);
out.write(config.getPrinterLuName().trim().getBytes(StandardCharsets.US_ASCII));
}
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SE);
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.warning("Error sending TN3270E device type request: " + e.getMessage());
}
}
private void parseDeviceTypeIs(byte[] sb) {
// SB TN3270E DEVICE-TYPE IS <devtype> [CONNECT|ASSOCIATE <luname>]
int idx = 3;
ByteArrayOutputStream devOut = new ByteArrayOutputStream();
while (idx < sb.length && (sb[idx] & 0xFF) != TN3270EConstants.OP_CONNECT && (sb[idx] & 0xFF) != TN3270EConstants.OP_ASSOCIATE) {
devOut.write(sb[idx++]);
}
negotiatedDeviceType = new String(devOut.toByteArray(), StandardCharsets.US_ASCII).trim();
if (idx < sb.length) {
idx++; // skip CONNECT / ASSOCIATE
ByteArrayOutputStream luOut = new ByteArrayOutputStream();
while (idx < sb.length) {
luOut.write(sb[idx++]);
}
assignedLuName = new String(luOut.toByteArray(), StandardCharsets.US_ASCII).trim();
}
log.info("TN3270E device negotiated: " + negotiatedDeviceType + ", LU=" + assignedLuName);
}
private void sendFunctionsRequest() {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SB);
out.write(TelnetConstants.TELOPT_TN3270E);
out.write(TN3270EConstants.OP_FUNCTIONS);
out.write(TN3270EConstants.OP_REQUEST);
// Request BIND-IMAGE, RESPONSES, SCS-CTL-CODES
out.write(TN3270EConstants.FUNC_BIND_IMAGE);
out.write(TN3270EConstants.FUNC_RESPONSES);
out.write(TN3270EConstants.FUNC_SCS_CTL_CODES);
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SE);
outputStream.write(out.toByteArray());
outputStream.flush();
} catch (IOException e) {
log.warning("Error sending TN3270E functions request: " + e.getMessage());
}
}
private void parseFunctionsIs(byte[] sb) {
Arrays.fill(negotiatedFunctions, false);
for (int i = 3; i < sb.length; i++) {
int fn = sb[i] & 0xFF;
if (fn < negotiatedFunctions.length) {
negotiatedFunctions[fn] = true;
}
}
updateStatus(PrinterConstants.STATUS_CONNECTED, "Connected and negotiated with host");
}
private void parseFunctionsRequest(byte[] sb) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SB);
out.write(TelnetConstants.TELOPT_TN3270E);
out.write(TN3270EConstants.OP_FUNCTIONS);
out.write(TN3270EConstants.OP_IS);
Arrays.fill(negotiatedFunctions, false);
for (int i = 3; i < sb.length; i++) {
int fn = sb[i] & 0xFF;
if (fn == TN3270EConstants.FUNC_BIND_IMAGE || fn == TN3270EConstants.FUNC_RESPONSES || fn == TN3270EConstants.FUNC_SCS_CTL_CODES) {
negotiatedFunctions[fn] = true;
out.write(fn);
}
}
out.write(TelnetConstants.IAC);
out.write(TelnetConstants.SE);
outputStream.write(out.toByteArray());
outputStream.flush();
updateStatus(PrinterConstants.STATUS_CONNECTED, "Connected and negotiated with host");
} catch (IOException e) {
log.warning("Error replying to TN3270E functions request: " + e.getMessage());
}
}
// ========== TN3270E Record Dispatching ==========
private void processCompleteRecord(byte[] record) {
if (record == null || record.length == 0) return;
if (tn3270eMode) {
if (record.length < TN3270EConstants.EH_SIZE) {
return;
}
int dataType = record[0] & 0xFF;
int reqFlag = record[1] & 0xFF;
int respFlag = record[2] & 0xFF;
int seqNum = ((record[3] & 0xFF) << 8) | (record[4] & 0xFF);
this.lastRecvSeqNumber = seqNum;
this.lastResponseRequired = respFlag;
firePrintJobData(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
switch (dataType) {
case TN3270EConstants.DT_SCS_DATA:
updateStatus(PrinterConstants.STATUS_PRINTING, "Processing SCS print stream");
scs.processHostData(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNum);
}
break;
case TN3270EConstants.DT_3270_DATA:
updateStatus(PrinterConstants.STATUS_PRINTING, "Processing 3270 printer data stream");
printPs.process3270PrintDS(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNum);
}
break;
case TN3270EConstants.DT_BIND_IMAGE:
processBindImage(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
break;
case TN3270EConstants.DT_UNBIND:
log.info("Received TN3270E UNBIND");
reset();
updateStatus(PrinterConstants.STATUS_CONNECTED, "Session unbound");
break;
case TN3270EConstants.DT_PRINT_EOJ:
sendEOJ(true);
break;
case TN3270EConstants.DT_REQUEST:
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNum);
}
break;
case TN3270EConstants.DT_RESPONSE:
log.fine("Received response, seq=" + seqNum);
break;
default:
// Fallback to active LU type
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
printPs.process3270PrintDS(record, 0, record.length);
} else {
scs.processHostData(record, 0, record.length);
}
break;
}
} else {
// Non-TN3270E mode
firePrintJobData(record, 0, record.length);
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
printPs.process3270PrintDS(record, 0, record.length);
} else {
scs.processHostData(record, 0, record.length);
}
}
}
// ========== Event Dispatching & Listeners (Fn #51 - #58) ==========
public void addPrintListener(PrintSessionListener listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public void removePrintListener(PrintSessionListener listener) {
listeners.remove(listener);
}
public void firePrintJobStarted(String jobName) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_STARTED, jobName,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), statusCode, "Print job started", null);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobStarted(event); } catch (Exception ignored) {}
}
}
public void firePrintJobData(byte[] data, int offset, int length) {
byte[] copy = null;
if (data != null && length > 0) {
copy = new byte[length];
System.arraycopy(data, offset, copy, 0, length);
}
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_DATA, null,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), statusCode, "Print data", copy);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobData(event); } catch (Exception ignored) {}
}
}
public void firePrintJobPageComplete(int pageNumber) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_PAGE_COMPLETE, null,
pageNumber, pd.getByteCount(), pageNumber, statusCode, "Page complete", null);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobPageComplete(event); } catch (Exception ignored) {}
}
}
public void firePrintJobComplete(int totalPages, long totalBytes) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_COMPLETE, null,
totalPages, totalBytes, totalPages, statusCode, "Print job complete", null);
for (PrintSessionListener l : listeners) {
try { l.onPrintJobComplete(event); } catch (Exception ignored) {}
}
}
public void firePrinterStatusChanged(int code, String message) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_STATUS_CHANGED, null,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), code, message, null);
for (PrintSessionListener l : listeners) {
try { l.onPrinterStatusChanged(event); } catch (Exception ignored) {}
}
}
public void firePrinterError(int errorCode, String errorMessage) {
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_ERROR, null,
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), errorCode, errorMessage, null);
for (PrintSessionListener l : listeners) {
try { l.onPrinterError(event); } catch (Exception ignored) {}
}
}
private void updateStatus(int newCode, String msg) {
this.statusCode = newCode;
firePrinterStatusChanged(newCode, msg);
}
// ========== Accessors ==========
public boolean isConnected() { return connected; }
public int getStatusCode() { return statusCode; }
public short getActiveLuType() { return activeLuType; }
public String getAssignedLuName() { return assignedLuName; }
public String getNegotiatedDeviceType() { return negotiatedDeviceType; }
public PrinterConfig getConfig() { return config; }
public PD3270 getPD() { return pd; }
public EbcdicTranslator getTranslator() { return translator; }
public PrintSCS3270 getSCS() { return scs; }
public PrintPS3270 getPrintPS() { return printPs; }
}
@@ -0,0 +1,101 @@
package haus.nightmare.lib3270j.printer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* High-level API Client for managing 3270/3287 Printer Sessions.
* Coordinates Telnet3270EP protocol engine, SCS/LU3 interpreters, and PD3270 print spoolers.
*/
public class Telnet3270EPClient {
private final PrinterConfig config;
private final Telnet3270EP protocolEngine;
public Telnet3270EPClient(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.protocolEngine = new Telnet3270EP(this.config);
}
public Telnet3270EPClient(String host, int port, String printerLuName) {
this(new PrinterConfig(host, port, printerLuName));
}
/**
* Connect to printer host asynchronously.
*/
public boolean connect() {
return protocolEngine.open();
}
/**
* Disconnect from printer host.
*/
public void disconnect() {
protocolEngine.close();
}
public boolean isConnected() {
return protocolEngine.isConnected();
}
public int getStatusCode() {
return protocolEngine.getStatusCode();
}
public void addPrintListener(PrintSessionListener listener) {
protocolEngine.addPrintListener(listener);
}
public void removePrintListener(PrintSessionListener listener) {
protocolEngine.removePrintListener(listener);
}
public PrinterConfig getConfig() {
return config;
}
public Telnet3270EP getProtocolEngine() {
return protocolEngine;
}
public PD3270 getPD() {
return protocolEngine.getPD();
}
public PrintSCS3270 getSCS() {
return protocolEngine.getSCS();
}
public PrintPS3270 getPrintPS() {
return protocolEngine.getPrintPS();
}
/**
* Print raw data stream directly through active printer interpreter.
*/
public void printDirectBytes(byte[] data, int offset, int length) {
if (protocolEngine.getActiveLuType() == PrinterConstants.LU_TYPE_3_DS) {
protocolEngine.getPrintPS().process3270PrintDS(data, offset, length);
} else {
protocolEngine.getSCS().processHostData(data, offset, length);
}
}
/**
* Print local file through active printer destination.
*/
public void printFile(File file) throws IOException {
if (file == null || !file.exists()) {
throw new IllegalArgumentException("File not found: " + file);
}
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buf = new byte[8192];
int r;
while ((r = fis.read(buf)) > 0) {
protocolEngine.getPD().writePrintBytes(buf, 0, r);
}
}
}
}
@@ -19,6 +19,12 @@ public final class DS3270Constants {
public static final int CMD_EAU = 0x0f; // Erase All Unprotected
public static final int CMD_WSF = 0x11; // Write Structured Field
// Command aliases
public static final int CMD_WRITE = CMD_W;
public static final int CMD_ERASE_WRITE = CMD_EW;
public static final int CMD_ERASE_WRITE_ALT = CMD_EWA;
public static final int CMD_ERASE_ALL_UNPROTECTED = CMD_EAU;
// SNA 3270 Commands
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
@@ -25,6 +25,18 @@ public class TlsTrustManager implements X509TrustManager {
public TlsTrustManager(ConnectionConfig config) {
this.config = config;
initDefaultTrustManager();
}
public TlsTrustManager(boolean tlsVerifyCert, TlsCertificateVerifier verifier) {
ConnectionConfig cfg = new ConnectionConfig();
cfg.setTlsVerifyCert(tlsVerifyCert);
cfg.setCertificateVerifier(verifier);
this.config = cfg;
initDefaultTrustManager();
}
private void initDefaultTrustManager() {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
@@ -0,0 +1,60 @@
package haus.nightmare.lib3270j.printer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.*;
public class PD3270Test {
private PD3270 pd;
@BeforeEach
public void setUp() {
pd = new PD3270();
}
@Test
public void testMemoryCaptureAndPageCount() {
assertTrue(pd.openPrinter(null));
assertTrue(pd.isOpen());
pd.writePrintLine("Line 1");
pd.writePrintLine("Line 2");
pd.formFeed();
assertEquals(1, pd.getPageCount());
assertTrue(pd.getByteCount() > 0);
String text = pd.getCapturedText();
assertTrue(text.contains("Line 1"));
assertTrue(text.contains("Line 2"));
assertTrue(text.contains("\f"));
pd.closePrinter();
assertFalse(pd.isOpen());
}
@Test
public void testFileOutput() throws IOException {
File tempFile = File.createTempFile("printer_test_", ".txt");
tempFile.deleteOnExit();
PrinterConfig config = new PrinterConfig();
config.setDestinationType(PrinterConfig.DestinationType.FILE);
config.setDestinationTarget(tempFile.getAbsolutePath());
PD3270 filePd = new PD3270(config);
assertTrue(filePd.openPrinter(tempFile.getAbsolutePath()));
filePd.writePrintLine("Test Output Line");
filePd.closePrinter();
String fileContent = new String(Files.readAllBytes(tempFile.toPath()));
assertTrue(fileContent.contains("Test Output Line"));
}
}
@@ -0,0 +1,82 @@
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.*;
public class PrintPS3270Test {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private PrintPS3270 printPs;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
printPs = new PrintPS3270(config, pd, translator);
pd.openPrinter(null);
}
@Test
public void testEraseWriteAndStartPrint() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(DS3270Constants.CMD_ERASE_WRITE);
out.write(PrinterConstants.WCC_START_PRINT_BIT); // WCC with Start Print bit
// SBA 0, 0
out.write(PrinterConstants.ORDER_SBA);
out.write(0x40); out.write(0x40); // 0, 0 address
// Write EBCDIC "REPORT"
byte[] rep = translator.stringToEbcdic("REPORT");
out.write(rep, 0, rep.length);
byte[] payload = out.toByteArray();
printPs.process3270PrintDS(payload, 0, payload.length);
String captured = pd.getCapturedText();
assertTrue(captured.contains("REPORT"));
}
@Test
public void testRepeatToAddressOrder() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(DS3270Constants.CMD_WRITE);
out.write(0x00); // WCC
out.write(PrinterConstants.ORDER_SBA);
out.write(0x40); out.write(0x40); // 0
// Repeat '*' to address 10
out.write(PrinterConstants.ORDER_RA);
out.write(0x40); out.write(0x4A); // Address 10
out.write(translator.unicodeToEbcdic('*'));
byte[] payload = out.toByteArray();
printPs.process3270PrintDS(payload, 0, payload.length);
printPs.flushPrintBuffer();
String captured = pd.getCapturedText();
assertTrue(captured.contains("**********"));
}
@Test
public void testFieldAttributesAndColorCalculations() {
assertEquals(1, printPs.calculateColor(0xF1)); // Blue
assertEquals(2, printPs.calculateColor(0xF2)); // Red
assertEquals(4, printPs.calculateColor(0xF4)); // Green
assertEquals(0, printPs.calculateColor(0x00)); // Neutral
assertEquals(1, printPs.calculateHighlight(PrinterConstants.SEAC_BLINK));
assertEquals(2, printPs.calculateHighlight(PrinterConstants.SEAC_REVERSE));
assertEquals(4, printPs.calculateHighlight(PrinterConstants.SEAC_UNDERLINE));
}
}
@@ -0,0 +1,131 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
public class PrintSCS3270Test {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private PrintSCS3270 scs;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
scs = new PrintSCS3270(config, pd, translator);
pd.openPrinter(null); // In-memory capture
}
@Test
public void testBasicPrintAndNewLines() {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// EBCDIC "HELLO" + NL + "WORLD" + FF
byte[] helloEbc = translator.stringToEbcdic("HELLO");
byte[] worldEbc = translator.stringToEbcdic("WORLD");
stream.write(helloEbc, 0, helloEbc.length);
stream.write(PrinterConstants.SCS_NL);
stream.write(worldEbc, 0, worldEbc.length);
stream.write(PrinterConstants.SCS_FF);
byte[] payload = stream.toByteArray();
scs.processHostData(payload, 0, payload.length);
String text = pd.getCapturedText();
assertTrue(text.contains("HELLO"));
assertTrue(text.contains("WORLD"));
assertEquals(1, pd.getPageCount());
}
@Test
public void testSetHorizontalFormatAndTabs() {
// SHF order: 0x2B 0xD1 <len> <mpp> <lm> <rm> <tab1> <tab2>
byte[] shfOrder = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SHF,
0x08, // Length
(byte) 132, // MPP = 132
0x05, // Left margin = 5
(byte) 120, // Right margin = 120
0x10, // Tab 1 = 16
0x20 // Tab 2 = 32
};
scs.processHostData(shfOrder, 0, shfOrder.length);
assertEquals(132, scs.getLineLength());
assertEquals(5, scs.getLeftMargin());
assertEquals(120, scs.getRightMargin());
int nextTab = scs.calculateHorizontalTab(10);
assertEquals(16, nextTab);
}
@Test
public void testSetVerticalFormatAndTabs() {
// SVF order: 0x2B 0xD2 <len> <mpl> <tm> <bm> <tab1> <tab2>
byte[] svfOrder = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_SVF,
0x08, // Length
(byte) 88, // MPL = 88
0x03, // Top margin = 3
(byte) 80, // Bottom margin = 80
0x0A, // Tab 1 = 10
0x14 // Tab 2 = 20
};
scs.processHostData(svfOrder, 0, svfOrder.length);
assertEquals(88, scs.getPageLength());
assertEquals(3, scs.getTopMargin());
assertEquals(80, scs.getBottomMargin());
int nextVTab = scs.calculateVerticalTab(5);
assertEquals(10, nextVTab);
}
@Test
public void testDoubleWidthAndHighlighting() {
scs.startDoubleWidthCharacters();
assertTrue(scs.isDoubleWidth());
scs.setEnhancedHighlight(PrinterConstants.SEAC_UNDERLINE);
assertEquals(PrinterConstants.SEAC_UNDERLINE, scs.getActiveHighlight());
scs.endDoubleWidthCharacters();
assertFalse(scs.isDoubleWidth());
}
@Test
public void testPPAandPPVPositioning() {
// PPA absolute to column 25: 0x2B 0xC6 0x02 0x01 0x19
byte[] ppa = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_PPA,
0x02,
PrinterConstants.POS_ABSOLUTE,
0x19 // col 25
};
scs.processHostData(ppa, 0, ppa.length);
assertEquals(25, scs.getCurrentColumn());
}
@Test
public void testTransparentStream() {
// TRS: 0x35 <len> <raw data>
byte[] trs = new byte[]{
PrinterConstants.SCS_TRS,
0x04,
'T', 'E', 'S', 'T'
};
scs.processHostData(trs, 0, trs.length);
assertTrue(pd.getByteCount() >= 4);
}
}
@@ -0,0 +1,94 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
public class PrinterPhase6Test {
private PrinterConfig printerConfig;
private Telnet3270EP printer;
private PD3270 pd;
private EbcdicTranslator translator;
@BeforeEach
public void setUp() {
printerConfig = new PrinterConfig("localhost", 23, "PRT_LU1");
printerConfig.setAssociatedDisplayLuName("DSP_LU1");
pd = new PD3270(printerConfig);
translator = new EbcdicTranslator("037");
printer = new Telnet3270EP(printerConfig, pd, translator);
pd.openPrinter(null);
}
@Test
public void testConnectionConfigToPrinterConfigMapping() {
ConnectionConfig ccfg = new ConnectionConfig("10.0.0.1", 992, true);
ccfg.setLuName("DSP01");
ccfg.setAssociatedPrinterLu("PRT01");
ccfg.setCodePage("1047");
PrinterConfig pcfg = ccfg.toPrinterConfig();
assertEquals("10.0.0.1", pcfg.getHost());
assertEquals(992, pcfg.getPort());
assertTrue(pcfg.isUseTls());
assertEquals("PRT01", pcfg.getPrinterLuName());
assertEquals("DSP01", pcfg.getAssociatedDisplayLuName());
assertEquals("1047", pcfg.getCodePage());
}
@Test
public void testDataStreamProcessorEmbeddedScsRouting() {
ScreenBuffer screen = new ScreenBuffer(haus.nightmare.lib3270j.TerminalModel.IBM_3279_4, translator);
DataStreamProcessor dsp = new DataStreamProcessor(screen, translator);
PrintSCS3270 scs = new PrintSCS3270(printerConfig, pd, translator);
dsp.setEmbeddedScsProcessor(scs);
assertSame(scs, dsp.getEmbeddedScsProcessor());
byte[] helloEbc = translator.stringToEbcdic("EMBEDDED SCS");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(helloEbc, 0, helloEbc.length);
stream.write(PrinterConstants.SCS_NL);
byte[] payload = stream.toByteArray();
dsp.processSCSData(payload, 0, payload.length);
String text = pd.getCapturedText();
assertTrue(text.contains("EMBEDDED SCS"));
}
@Test
public void testQueryReplyBuilderAuxDevice() {
ScreenBuffer screen = new ScreenBuffer(haus.nightmare.lib3270j.TerminalModel.IBM_3279_4, translator);
haus.nightmare.lib3270j.datastream.QueryReplyBuilder qrb = new haus.nightmare.lib3270j.datastream.QueryReplyBuilder(screen);
byte[] auxDev = qrb.buildAuxDevice();
assertNotNull(auxDev);
assertTrue(auxDev.length >= 10);
}
@Test
public void testTelnet3270EPClientFacade() {
Telnet3270EPClient client = new Telnet3270EPClient(printerConfig);
assertNotNull(client.getPD());
assertNotNull(client.getSCS());
assertNotNull(client.getPrintPS());
assertNotNull(client.getConfig());
// Test direct print through client
byte[] testText = translator.stringToEbcdic("CLIENT PRINT");
client.printDirectBytes(testText, 0, testText.length);
client.getSCS().flushLineBuffer();
String captured = client.getPD().getCapturedText();
assertTrue(captured.contains("CLIENT PRINT"));
}
}
@@ -0,0 +1,80 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class Telnet3270EPTest {
private PrinterConfig config;
private PD3270 pd;
private Telnet3270EP printer;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23, "PRT01");
config.setAssociatedDisplayLuName("DSP01");
pd = new PD3270(config);
printer = new Telnet3270EP(config, pd, null);
}
@Test
public void testPrinterConfigAndDefaults() {
assertNotNull(config);
assertEquals("PRT01", config.getPrinterLuName());
assertEquals("DSP01", config.getAssociatedDisplayLuName());
assertEquals(PrinterConstants.DEV_IBM_3287_1, config.getDeviceType());
assertEquals(80, config.getMpp());
assertEquals(66, config.getMpl());
assertTrue(config.isFormFeedAtEoj());
assertTrue(config.isAutoFlushOnEoj());
}
@Test
public void testBindProcessingLU1andLU3() {
// Test LU-1 SCS bind
printer.process_bind(PrinterConstants.LU_TYPE_1_SCS);
assertEquals(PrinterConstants.LU_TYPE_1_SCS, printer.getActiveLuType());
assertEquals(PrinterConstants.STATUS_PRINTER_READY, printer.getStatusCode());
// Test LU-3 3270 DS bind
printer.process_bind(PrinterConstants.LU_TYPE_3_DS);
assertEquals(PrinterConstants.LU_TYPE_3_DS, printer.getActiveLuType());
}
@Test
public void testBindImagePayloadParsing() {
byte[] bindImage = new byte[30];
bindImage[14] = 0x01; // LU1 profile
printer.processBindImage(bindImage, 0, bindImage.length);
assertEquals(PrinterConstants.LU_TYPE_1_SCS, printer.getActiveLuType());
bindImage[14] = 0x03; // LU3 profile
printer.processBindImage(bindImage, 0, bindImage.length);
assertEquals(PrinterConstants.LU_TYPE_3_DS, printer.getActiveLuType());
}
@Test
public void testEOJAndListeners() {
List<PrintSessionEvent> events = new ArrayList<>();
printer.addPrintListener(new PrintSessionListener() {
@Override public void onPrintJobStarted(PrintSessionEvent event) { events.add(event); }
@Override public void onPrintJobData(PrintSessionEvent event) { events.add(event); }
@Override public void onPrintJobPageComplete(PrintSessionEvent event) { events.add(event); }
@Override public void onPrintJobComplete(PrintSessionEvent event) { events.add(event); }
@Override public void onPrinterStatusChanged(PrintSessionEvent event) { events.add(event); }
@Override public void onPrinterError(PrintSessionEvent event) { events.add(event); }
});
printer.firePrintJobStarted("JOB001");
printer.sendEOJ(true);
assertEquals(PrinterConstants.STATUS_JOB_COMPLETE, printer.getStatusCode());
assertTrue(events.size() >= 2);
}
}