This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user