Bugfixes from zOS testing
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 37s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m16s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m52s

This commit is contained in:
2026-09-17 23:09:43 +00:00
parent 30b1a77b61
commit ef70ea0f5a
25 changed files with 2686 additions and 335 deletions
@@ -456,11 +456,26 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.bytesTransferred = 0;
this.totalBytes = (config.isSend() && localFile.exists()) ? localFile.length() : 0;
// Auto-detect DBCS from translator codepage if not explicitly set
if (!config.isDbcs() && translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS()) {
config.setDbcs(true);
}
// Wait for keyboard unlock before typing command
waitForKeyboardUnlock(2000);
// If clear is requested, send Clear AID first to wipe intermediate screen status
if (config.isClear() && input != null && !input.isNvtMode()) {
input.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
waitForKeyboardUnlock(2000);
}
String command = config.buildCommand();
log.info("ECLXfer: initiating transfer: " + command);
int capacity = input.kybdPrime();
if (capacity < command.length()) {
int capacity = input != null ? input.kybdPrime() : 0;
if (input != null && capacity < command.length()) {
String err = "Input field capacity insufficient for command (" + capacity + " chars)";
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err);
return FTConstants.ECL_ERR_XFER_ABORT;
@@ -469,10 +484,25 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
setState(FTState.AWAIT_ACK);
fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated");
input.emulateInput(command + "\n");
if (input != null) {
input.emulateInput(command + "\n");
}
return 0;
}
private void waitForKeyboardUnlock(long timeoutMs) {
if (input == null) return;
long start = System.currentTimeMillis();
while (input.isKeyboardLocked() && (System.currentTimeMillis() - start) < timeoutMs) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private void parseOptionsIntoConfig(FTConfig config, String options) {
if (options == null || options.trim().isEmpty()) return;
config.parseOptions(options);
@@ -74,6 +74,11 @@ public class FTConfig {
private String codePage = null;
private boolean unicode = false;
private int unicodeType = 0; // 0 = UCS2, 1 = UTF8
private String programName = null;
private boolean dbcs = false;
private boolean optionNew = false;
private boolean clear = true;
private boolean optionNoso = false;
// ========== Derived convenience getters ==========
@@ -91,6 +96,37 @@ public class FTConfig {
}
public boolean isAppend() { return existAction == ExistAction.APPEND; }
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
public boolean isOptionNew() { return optionNew; }
public void setOptionNew(boolean optionNew) { this.optionNew = optionNew; }
public boolean isOptionNoso() { return optionNoso; }
public void setOptionNoso(boolean optionNoso) { this.optionNoso = optionNoso; }
public boolean isDbcs() { return dbcs; }
public void setDbcs(boolean dbcs) { this.dbcs = dbcs; }
public boolean isClear() { return clear; }
public void setClear(boolean clear) { this.clear = clear; }
public String getProgramName() {
if (programName != null && !programName.trim().isEmpty()) {
return programName.trim();
}
if (dbcs || isDbcsCodePage(codePage)) {
return "APVUFILE";
}
return "IND$FILE";
}
public void setProgramName(String programName) {
this.programName = programName;
}
private static boolean isDbcsCodePage(String cp) {
if (cp == null || cp.trim().isEmpty()) return false;
String clean = cp.trim().toUpperCase();
if (clean.startsWith("CP")) clean = clean.substring(2);
return clean.equals("930") || clean.equals("939") || clean.equals("935") ||
clean.equals("1388") || clean.equals("933") || clean.equals("937") ||
clean.equals("1371");
}
// ========== Standard getters and setters ==========
@@ -303,10 +339,22 @@ public class FTConfig {
if (upper.contains("APPEND")) {
this.existAction = ExistAction.APPEND;
} else if (upper.contains("NEW") && !upper.contains("NONEW")) {
this.optionNew = true;
} else if (upper.contains("REPLACE") || upper.contains("OVERWRITE")) {
this.existAction = ExistAction.REPLACE;
}
if (upper.contains("NOSO")) {
this.optionNoso = true;
}
if (upper.contains("APVUFILE")) {
this.programName = "APVUFILE";
} else if (upper.contains("IND$FILE")) {
this.programName = "IND$FILE";
}
if (upper.contains("CICS")) {
this.hostType = HostType.CICS;
} else if (upper.contains("CMS") || upper.contains("VM")) {
@@ -332,6 +380,10 @@ public class FTConfig {
String hostTrimmed = hostFilename.trim();
if (hostType == HostType.TSO && hostTrimmed.contains(" ") && validateCmsFilename(hostTrimmed) == null) {
hostType = HostType.CMS;
}
if (hostType == HostType.TSO) {
String tsoError = validateTsoDatasetName(hostTrimmed);
if (tsoError != null) return tsoError;
@@ -448,8 +500,8 @@ public class FTConfig {
public String buildCommand() {
StringBuilder cmd = new StringBuilder();
// IND$FILE GET/PUT hostfile
cmd.append("IND$FILE ");
// Host program selection: IND$FILE or APVUFILE
cmd.append(getProgramName()).append(" ");
cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename != null ? hostFilename.trim() : "");
@@ -477,16 +529,23 @@ public class FTConfig {
opts.append("NOCRLF");
}
// Append (send only)
if (isAppend() && isSend()) {
if (isOptionNoso()) {
if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
opts.append("NOSO");
}
// Overwrite / Replace (if send or explicitly requested)
if (isOverwrite() && isSend()) {
if (opts.length() > 0) opts.append(" ");
opts.append("REPLACE");
// Append / New / Replace (send only)
if (isSend()) {
if (isAppend()) {
if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
} else if (isOptionNew()) {
if (opts.length() > 0) opts.append(" ");
opts.append("NEW");
} else if (isOverwrite()) {
if (opts.length() > 0) opts.append(" ");
opts.append("REPLACE");
}
}
// Host-specific send options
@@ -123,15 +123,69 @@ public final class FTConstants {
// Error codes
public static final int TR_ERR_EOF = 0x2200;
public static final int TR_ERR_CMDFAIL = 0x0100;
public static final int TR_ERR_OPEN_FAILED = 0x0100;
public static final int TR_ERR_SEQ_NOT_ALLOWED = 0x0200;
public static final int TR_ERR_NOT_AUTHORIZED = 0x3E00;
public static final int TR_ERR_SYNTAX_ERROR = 0x6000;
public static final int TR_ERR_GET_PAST_EOF = 0x2200;
// DFT buffer size limits
// DFT buffer size limits (Default matching IBM HoD bmtuSize = 2500)
public static final int DFT_MIN_BUF = 256;
public static final int DFT_MAX_BUF = 32768;
public static final int DFT_BUF = 4096;
public static final int DFT_BUF = 2500;
// AID for structured field response
public static final int AID_SF = 0x88;
// DDM Stream Names (ASCII and EBCDIC)
public static final String STREAM_NAME_FT_DATA = "FT:DATA";
public static final String STREAM_NAME_FT_MSG = "FT:MSG";
public static final byte[] STREAM_NAME_FT_DATA_BYTES_ASCII = {
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // 'F','T',':','D','A','T','A'
};
public static final byte[] STREAM_NAME_FT_DATA_BYTES_EBCDIC = {
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xC4, (byte) 0xC1, (byte) 0xE3, (byte) 0xC1
};
public static final byte[] STREAM_NAME_FT_MSG_BYTES_ASCII = {
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // 'F','T',':','M','S','G',' '
};
public static final byte[] STREAM_NAME_FT_MSG_BYTES_EBCDIC = {
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xD4, (byte) 0xE2, (byte) 0xC7, 0x40
};
// HoD Binary Packet Templates (Xfer3270.java)
public static final byte[] RB_OPEN_ACK = {
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x00, 0x09, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_CLOSE_ACK = {
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x41, 0x09, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_OPEN_FAILED = {
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_GET_PAST_EOF = {
(byte) 0x88, 0x00, 0x09, (byte) 0xD0, 0x46, 0x08, 0x69, 0x04, 0x22, 0x00, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_UPLOAD_DATA_HEADER = {
0x11, 0x11, (byte) 0xD0, 0x46, 0x05, 0x63, 0x06, 0x00, 0x00, 0x00, 0x00,
(byte) 0xC0, (byte) 0x80, 0x61, 0x00, 0x00
};
/**
* Builds exact 14-byte Data Acknowledgement matching HoD Xfer3270:
* [0x88, 0x00, 0x0B, 0xD0, 0x47, 0x05, 0x63, 0x06, rec_b0, rec_b1, rec_b2, rec_b3, 0xFF, 0xEF]
*/
public static byte[] createDataAck(long recnum) {
return new byte[] {
(byte) 0x88, 0x00, 0x0B, (byte) 0xD0, 0x47, 0x05, 0x63, 0x06,
(byte) ((recnum >> 24) & 0xFF),
(byte) ((recnum >> 16) & 0xFF),
(byte) ((recnum >> 8) & 0xFF),
(byte) (recnum & 0xFF),
(byte) 0xFF, (byte) 0xEF
};
}
// ========== Transfer State ==========
public enum FTState {
@@ -215,11 +215,13 @@ public class FTCut {
// CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) {
// Not a CUT frame (likely local echo, menu return, or intermediate screen)
// Not a CUT frame (likely local echo, menu return, or host error screen)
if (xferInProgress) {
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
xferInProgress = false;
listener.onTransferAborted("Host aborted transfer (returned to menu)");
String hostErr = scanScreenForErrors();
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
listener.onTransferAborted(abortMsg);
}
return;
}
@@ -245,6 +247,38 @@ public class FTCut {
}
}
/**
* Scans rows 1-24 of the 3270 screen buffer for host error indicators.
* Checks for TSO (IKJ*), CMS (DMS*), CICS (DFH*), and generic transfer abort messages.
*/
public String scanScreenForErrors() {
if (screen == null) return null;
int rows = screen.getRows();
int cols = screen.getCols();
String[] errorPrefixes = {
"IKJ", "DMS", "DFH", "TRANS", "NOT FOUND", "ERROR", "REJECTED", "INVALID", "COMMAND NOT FOUND", "FILE NOT FOUND"
};
for (int r = 0; r < rows; r++) {
StringBuilder lineSb = new StringBuilder();
for (int c = 0; c < cols; c++) {
int pos = r * cols + c;
int ec = screen.getCellEC(pos);
if (ec != 0) {
lineSb.append(translator.ebcdicToUnicode(ec));
}
}
String line = lineSb.toString().trim();
if (line.isEmpty()) continue;
String lineUpper = line.toUpperCase();
for (String prefix : errorPrefixes) {
if (lineUpper.contains(prefix)) {
return line;
}
}
}
return null;
}
private void checkForHostPrompts() {
if (screen == null) return;
int size = screen.getRows() * screen.getCols();
@@ -299,6 +333,10 @@ public class FTCut {
cutAck();
String msg = extractHostMessage();
if ("Host cancelled transfer".equals(msg)) {
String hostErr = scanScreenForErrors();
if (hostErr != null) msg = hostErr;
}
listener.onTransferAborted(msg);
break;
@@ -388,7 +426,7 @@ public class FTCut {
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
expandedLength += count;
listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER);
paceAndSendAid(AID_ENTER);
}
// ========== Data (Download) ==========
@@ -467,11 +505,11 @@ public class FTCut {
screen.setCellFA(O_DR_SF, attr);
log.fine("CUT: Retransmitting last upload frame (len=" + lastUploadCount + ")");
input.sendAidForFT(AID_ENTER);
paceAndSendAid(AID_ENTER);
} else {
// In download mode, send ACK_RETRANSMIT (PF1)
log.fine("CUT: Requesting frame retransmit from host via PF1");
input.sendAidForFT(ACK_RETRANSMIT);
paceAndSendAid(ACK_RETRANSMIT);
}
}
@@ -479,7 +517,7 @@ public class FTCut {
private void cutAck() {
log.fine("CUT: > ACK (Enter)");
input.sendAidForFT(AID_ENTER);
paceAndSendAid(AID_ENTER);
}
// ========== Abort ==========
@@ -492,10 +530,24 @@ public class FTCut {
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
input.sendAidForFT(AID_PF2);
paceAndSendAid(AID_PF2);
listener.onTransferAborted(message);
}
private void paceAndSendAid(int aid) {
if (input == null) return;
long start = System.currentTimeMillis();
while (input.isKeyboardLocked() && System.currentTimeMillis() - start < 1000) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
input.sendAidForFT((byte) aid);
}
// ========== Character Translation ==========
/**
@@ -49,6 +49,14 @@ public class FTDft {
private int customMtuSize = 0;
private int pendingByte = -1;
// Record formatting buffers and SO/SI tracking
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
private boolean dbcsMode = false;
private boolean hasPendingByteInDBCS = false;
private byte pendingByteDBCS = 0;
private byte[] pendingUploadRecord = null;
private int pendingUploadRecordOffset = 0;
// Savebuf for Read Modified retransmit
private byte[] dftSaveBuf = null;
private int dftSaveBufLen = 0;
@@ -159,6 +167,12 @@ public class FTDft {
dftSaveBufLen = 0;
lastCr = false;
pendingByte = -1;
downloadRecordBuffer.reset();
dbcsMode = false;
hasPendingByteInDBCS = false;
pendingByteDBCS = 0;
pendingUploadRecord = null;
pendingUploadRecordOffset = 0;
}
/**
@@ -201,6 +215,12 @@ public class FTDft {
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
// Check if this is a host error reply (e.g. TR_ERROR_REPLY 0x08, or contains TR_ERROR_HDR 0x6904)
if ((requestCode & 0xFF) == TR_ERROR_REPLY || isErrorField(data, payloadStart, offset + length - payloadStart)) {
handleHostErrorReply(data, payloadStart, offset + length - payloadStart);
return;
}
switch (requestCode) {
case TR_OPEN_REQ:
dftOpenRequest(data, offset, length);
@@ -228,8 +248,74 @@ public class FTDft {
}
}
private boolean isErrorField(byte[] data, int start, int length) {
int end = Math.min(start + length, data.length);
for (int i = start; i + 3 < end; i++) {
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
if (hdr == TR_ERROR_HDR) return true;
}
return false;
}
private void handleHostErrorReply(byte[] data, int payloadStart, int length) {
int end = Math.min(payloadStart + length, data.length);
int errCode = 0;
for (int i = payloadStart; i + 3 < end; i++) {
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
if (hdr == TR_ERROR_HDR) {
errCode = ((data[i + 2] & 0xFF) << 8) | (data[i + 3] & 0xFF);
break;
}
}
String message;
switch (errCode) {
case TR_ERR_OPEN_FAILED:
message = "Host file open failed (file not found, cannot open, or dataset does not exist)";
break;
case TR_ERR_SEQ_NOT_ALLOWED:
message = "Host transfer sequence not allowed (state violation)";
break;
case TR_ERR_GET_PAST_EOF:
if (listener != null && listener.getCurrentState() == FTState.RUNNING) {
log.info("DFT: Host reported end of file (0x2200)");
listener.onTransferComplete(null);
return;
}
message = "Host reached end of file";
break;
case TR_ERR_NOT_AUTHORIZED:
message = "Host file access not authorized (security violation)";
break;
case TR_ERR_SYNTAX_ERROR:
message = "Host command syntax error or invalid transfer options";
break;
default:
message = "Host transfer error (code 0x" + Integer.toHexString(errCode) + ")";
break;
}
log.warning("DFT host error reply: " + message + " (errCode=0x" + Integer.toHexString(errCode) + ")");
dftAbort(message, TR_DATA_INSERT);
}
// ========== Open Request ==========
private static boolean containsBytes(byte[] data, int start, int end, byte[] pattern) {
if (data == null || pattern == null || pattern.length == 0) return false;
int max = Math.min(end, data.length) - pattern.length;
for (int i = Math.max(0, start); i <= max; i++) {
boolean match = true;
for (int j = 0; j < pattern.length; j++) {
if (data[i + j] != pattern[j]) {
match = false;
break;
}
}
if (match) return true;
}
return false;
}
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
log.fine("DFT: Open request");
@@ -247,7 +333,10 @@ public class FTDft {
// Check for host DDM file attributes embedded in open request
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
if (isMessageStream(nameBuf)) {
int sfEnd = Math.min(sfOffset + sfLength, data.length);
if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) ||
containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_EBCDIC) ||
isMessageStream(nameBuf)) {
messageFlag = true;
log.info("DFT: Open request for message stream (" + nameBuf + ")");
} else {
@@ -258,7 +347,7 @@ public class FTDft {
dftEof = false;
recnum = 1;
// Acknowledge Open matching x3270 (SF_TRANSFER_DATA + 0x0009)
// Acknowledge Open matching IBM Host On-Demand
dftOpenAck();
}
@@ -322,12 +411,7 @@ public class FTDft {
}
private void dftOpenAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
out.write(0); out.write(5); // SF length
out.write(SF_TRANSFER_DATA);
out.write(0); out.write(9); // OpenAck response code 0x0009
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(RB_OPEN_ACK.clone());
}
// ========== Insert Request (host sending data for download) ==========
@@ -441,31 +525,125 @@ public class FTDft {
}
/**
* Write download data to the local file, handling ASCII conversion.
* Matching x3270 upload_convert logic.
* Write download data to the local file, handling ASCII conversion, RECFM F/V formatting, and EOF stripping.
* Conforms to IBM Host On-Demand specification.
*/
private void writeDownloadData(byte[] data, int offset, int length,
FTConfig config) throws IOException {
if (outputStream == null) return;
if (outputStream == null || length <= 0) return;
if (config != null && config.isUnicode()) {
outputStream.write(data, offset, length);
return;
}
if (!config.isAscii()) {
if (config != null && !config.isAscii()) {
outputStream.write(data, offset, length);
return;
}
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
"V".equalsIgnoreCase(hostRecfm);
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
"F".equalsIgnoreCase(hostRecfm);
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
if (isRecfmV) {
downloadRecordBuffer.write(data, offset, length);
processVariableDownloadRecords(config);
} else if (isRecfmF && lrecl > 0) {
downloadRecordBuffer.write(data, offset, length);
processFixedDownloadRecords(lrecl, config);
} else {
writeConvertedChunk(data, offset, length, config);
}
}
private void processVariableDownloadRecords(FTConfig config) throws IOException {
while (downloadRecordBuffer.size() >= 4) {
byte[] buf = downloadRecordBuffer.toByteArray();
int rdwLen = ((buf[0] & 0xFF) << 8) | (buf[1] & 0xFF);
if (rdwLen < 4) {
downloadRecordBuffer.reset();
if (buf.length > 1) {
downloadRecordBuffer.write(buf, 1, buf.length - 1);
}
continue;
}
if (buf.length < rdwLen) {
break;
}
int recDataLen = rdwLen - 4;
writeConvertedChunk(buf, 4, recDataLen, config);
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
downloadRecordBuffer.reset();
if (buf.length > rdwLen) {
downloadRecordBuffer.write(buf, rdwLen, buf.length - rdwLen);
}
}
}
private void processFixedDownloadRecords(int lrecl, FTConfig config) throws IOException {
while (downloadRecordBuffer.size() >= lrecl) {
byte[] buf = downloadRecordBuffer.toByteArray();
int end = lrecl;
while (end > 0) {
int b = buf[end - 1] & 0xFF;
if (b == 0x20 || b == 0x40 || (config != null && config.isRemapFlag() && b == FT2ASC[0x40])) {
end--;
} else {
break;
}
}
writeConvertedChunk(buf, 0, end, config);
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
downloadRecordBuffer.reset();
if (buf.length > lrecl) {
downloadRecordBuffer.write(buf, lrecl, buf.length - lrecl);
}
}
}
private void flushRemainingDownloadRecords() throws IOException {
if (outputStream == null || downloadRecordBuffer.size() == 0) return;
FTConfig config = listener != null ? listener.getConfig() : null;
byte[] remaining = downloadRecordBuffer.toByteArray();
downloadRecordBuffer.reset();
if (config != null && config.isAscii()) {
int end = remaining.length;
while (end > 0) {
int b = remaining[end - 1] & 0xFF;
if (b == 0x20 || b == 0x40 || (config.isRemapFlag() && b == FT2ASC[0x40])) {
end--;
} else {
break;
}
}
if (end > 0) {
writeConvertedChunk(remaining, 0, end, config);
}
} else {
outputStream.write(remaining);
}
outputStream.flush();
}
private void writeConvertedChunk(byte[] data, int offset, int length, FTConfig config) throws IOException {
if (outputStream == null || length <= 0) return;
for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF;
if (config.isCrFlag() && (b == '\r' || b == EOF_CTRL_Z || b == EOF_CTRL_D)) {
continue; // Strip CR and EOF (^Z / ^D)
if (b == EOF_CTRL_Z || b == EOF_CTRL_D) {
continue; // Strip EOF (^Z / ^D)
}
if (!config.isRemapFlag()) {
if (config != null && config.isCrFlag() && b == '\r') {
continue; // Strip CR
}
if (config == null || !config.isRemapFlag()) {
outputStream.write(b);
continue;
}
@@ -506,30 +684,42 @@ public class FTDft {
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
"F".equalsIgnoreCase(hostRecfm);
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
"V".equalsIgnoreCase(hostRecfm);
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
try {
while (!dftEof && totalRead < numbytes) {
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
if (config != null && config.isAscii() && isRecfmF && lrecl > 0) {
totalRead = readFixedRecordUpload(readBuf, numbytes, lrecl, config);
} else if (config != null && config.isAscii() && isRecfmV) {
totalRead = readVariableRecordUpload(readBuf, numbytes, config);
} else {
while (!dftEof && totalRead < numbytes) {
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
} else if (config != null && config.isAscii()) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
break;
}
readBuf[totalRead++] = (byte) b;
} else {
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
}
totalRead += n;
} else if (config.isAscii()) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
break;
}
readBuf[totalRead++] = (byte) b;
} else {
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
}
}
} catch (IOException e) {
@@ -537,17 +727,17 @@ public class FTDft {
return;
}
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
out.write(AID_SF);
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA);
if (totalRead > 0) {
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
out.write(AID_SF);
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA);
out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF);
@@ -570,31 +760,167 @@ public class FTDft {
out.write(readBuf, 0, totalRead);
bytesTransferred += totalRead;
byte[] result = out.toByteArray();
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred);
} else {
log.fine("DFT: > GetReply EOF");
out.write((TR_GET_REQ >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_EOF >> 8) & 0xFF);
out.write(TR_ERR_EOF & 0xFF);
dftEof = true;
dftSaveBuf = RB_GET_PAST_EOF.clone();
dftSaveBufLen = RB_GET_PAST_EOF.length;
input.sendStructuredFieldData(RB_GET_PAST_EOF.clone());
listener.onBytesTransferred(bytesTransferred);
}
}
private byte[] readNextUploadLineConverted(FTConfig config) throws IOException {
if (inputStream == null || dftEof) return null;
ByteArrayOutputStream lineBytes = new ByteArrayOutputStream();
int c;
boolean hasData = false;
while ((c = inputStream.read()) != -1) {
hasData = true;
if (c == '\r') {
continue; // ignore CR
}
if (c == '\n') {
break;
}
if (c == EOF_CTRL_Z || c == EOF_CTRL_D) {
break;
}
lineBytes.write(c);
}
if (!hasData && c == -1) {
return null;
}
byte[] result = out.toByteArray();
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
byte[] raw = lineBytes.toByteArray();
return convertLineForUpload(raw, config);
}
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
private byte[] convertLineForUpload(byte[] raw, FTConfig config) {
String lineStr = new String(raw, StandardCharsets.UTF_8);
ByteArrayOutputStream converted = new ByteArrayOutputStream(raw.length + 8);
boolean isDbcs = (config != null && config.isDbcs()) || (translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS());
boolean noSo = config != null && config.isOptionNoso();
boolean inDbcsState = false;
input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred);
for (int i = 0; i < lineStr.length(); i++) {
char ch = lineStr.charAt(i);
int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1;
boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF);
if (charIsDbcs) {
if (!inDbcsState && !noSo) {
converted.write(0x0E); // Shift-Out
inDbcsState = true;
}
converted.write((ebc >> 8) & 0xFF);
converted.write(ebc & 0xFF);
} else {
if (inDbcsState && !noSo) {
converted.write(0x0F); // Shift-In
inDbcsState = false;
}
converted.write(convertAsciiByte(ch, config));
}
}
if (inDbcsState && !noSo) {
converted.write(0x0F); // Shift-In at end of record
}
return converted.toByteArray();
}
private int readFixedRecordUpload(byte[] outBuf, int maxBytes, int lrecl, FTConfig config) throws IOException {
int totalRead = 0;
int padByte = (config != null && config.isRemapFlag()) ? FT2ASC[0x40] : 0x20;
while (totalRead + lrecl <= maxBytes && !dftEof) {
byte[] rec;
if (pendingUploadRecord != null) {
rec = pendingUploadRecord;
pendingUploadRecord = null;
} else {
rec = readNextUploadLineConverted(config);
if (rec == null) {
dftEof = true;
break;
}
}
if (rec.length <= lrecl) {
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
totalRead += rec.length;
int padCount = lrecl - rec.length;
for (int p = 0; p < padCount; p++) {
outBuf[totalRead++] = (byte) padByte;
}
} else {
// Longer than lrecl: split
System.arraycopy(rec, 0, outBuf, totalRead, lrecl);
totalRead += lrecl;
int rem = rec.length - lrecl;
pendingUploadRecord = new byte[rem];
System.arraycopy(rec, lrecl, pendingUploadRecord, 0, rem);
}
}
return totalRead;
}
private int readVariableRecordUpload(byte[] outBuf, int maxBytes, FTConfig config) throws IOException {
int totalRead = 0;
while (totalRead + 4 < maxBytes && !dftEof) {
byte[] rec;
if (pendingUploadRecord != null) {
rec = pendingUploadRecord;
pendingUploadRecord = null;
} else {
rec = readNextUploadLineConverted(config);
if (rec == null) {
dftEof = true;
break;
}
}
int needed = rec.length + 4;
if (totalRead + needed <= maxBytes) {
outBuf[totalRead++] = (byte) ((needed >> 8) & 0xFF);
outBuf[totalRead++] = (byte) (needed & 0xFF);
outBuf[totalRead++] = 0;
outBuf[totalRead++] = 0;
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
totalRead += rec.length;
} else {
if (totalRead > 0) {
// Save for next MTU buffer
pendingUploadRecord = rec;
break;
} else {
// Single record exceeds entire MTU: split
int chunk = maxBytes - 4;
int rdwLen = chunk + 4;
outBuf[totalRead++] = (byte) ((rdwLen >> 8) & 0xFF);
outBuf[totalRead++] = (byte) (rdwLen & 0xFF);
outBuf[totalRead++] = 0;
outBuf[totalRead++] = 0;
System.arraycopy(rec, 0, outBuf, totalRead, chunk);
totalRead += chunk;
int rem = rec.length - chunk;
pendingUploadRecord = new byte[rem];
System.arraycopy(rec, chunk, pendingUploadRecord, 0, rem);
break;
}
}
}
return totalRead;
}
/**
@@ -717,14 +1043,13 @@ public class FTDft {
private void dftCloseRequest() {
log.fine("DFT: Close");
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
out.write(TR_CLOSE_REPLY & 0xFF);
try {
flushRemainingDownloadRecords();
} catch (IOException e) {
log.warning("DFT: Error flushing download buffer on close: " + e.getMessage());
}
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(RB_CLOSE_ACK.clone());
if (listener.getCurrentState() != FTState.NONE) {
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
@@ -735,21 +1060,7 @@ public class FTDft {
// ========== Data Acknowledgement ==========
private void dftDataAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(12);
out.write(AID_SF);
out.write(0); out.write(11); // SF length = 11 (0x000B)
out.write(SF_TRANSFER_DATA);
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
out.write(TR_NORMAL_REPLY & 0xFF);
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF));
out.write((int) ((recnum >> 16) & 0xFF));
out.write((int) ((recnum >> 8) & 0xFF));
out.write((int) (recnum & 0xFF));
recnum++;
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(createDataAck(recnum++));
}
// ========== Abort ==========
@@ -757,18 +1068,22 @@ public class FTDft {
private void dftAbort(String message, int code) {
log.warning("DFT: ABORT — " + message);
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA);
out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF);
if (code == TR_OPEN_REQ) {
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
} else {
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA);
out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(out.toByteArray());
}
listener.onTransferAborted(message);
}
@@ -10,10 +10,14 @@ import java.util.List;
* <p>A GOCA fillet curve passes from the initial point P_0 to the terminal point P_{N-1},
* bending tangentially toward each intermediate control point P_i.
*/
public class FilletPts {
public class FilletPts extends HODEllipse {
public static final int DEFAULT_STEPS_PER_SEGMENT = 24;
public FilletPts() {
super(0.0, 0.0, 0.0, 0.0);
}
/**
* Calculates interpolated spline vertices across control points using double precision.
*
@@ -116,17 +120,115 @@ public class FilletPts {
}
/**
* Interpolates fillet points into provided output buffers.
* Interpolates fillet points into provided output buffers using HoD's exact rational quadratic
* polynomial spline algorithm.
* Matches IBM Host On-Demand FilletPts.getFilletPoints.
*/
public int getFilletPoints(int[] xIn, int[] yIn, int n, int[] xOut, int[] yOut) {
if (xIn == null || yIn == null || n <= 0 || xOut == null || yOut == null) {
public int getFilletPoints(int[] nArray, int[] nArray2, int n, int[] nArray3, int[] nArray4) {
if (nArray == null || nArray2 == null || n <= 0 || nArray3 == null || nArray4 == null) {
return 0;
}
int[][] pts = calculate(xIn, yIn, n, 16);
int count = Math.min(pts[0].length, Math.min(xOut.length, yOut.length));
System.arraycopy(pts[0], 0, xOut, 0, count);
System.arraycopy(pts[1], 0, yOut, 0, count);
return count;
int n2;
int n3 = 0;
int n4 = 0;
boolean bl = false;
int n5 = 0;
for (n2 = 1; n2 < n; ++n2) {
if (nArray[n2] != nArray[n5] || nArray2[n2] != nArray2[n5]) continue;
bl = true;
}
if (bl) {
int[] nArray5 = new int[n];
int[] nArray6 = new int[n];
nArray5[0] = nArray[0];
nArray6[0] = nArray2[0];
n5 = 0;
for (n2 = 1; n2 < n; ++n2) {
if (nArray[n2] == nArray[n5] && nArray2[n2] == nArray2[n5]) continue;
nArray5[++n5] = nArray[n2];
nArray6[n5] = nArray2[n2];
}
nArray = nArray5;
nArray2 = nArray6;
n = n5 + 1;
}
nArray3[n3] = nArray[0];
nArray4[n3] = nArray2[0];
int n6 = nArray[0];
int n9 = nArray2[0];
n3++;
if (n == 1) {
nArray3[n3] = nArray3[0];
nArray4[n3++] = nArray4[0];
return n3;
}
if (n == 2) {
nArray3[n3] = nArray[1];
nArray4[n3++] = nArray2[1];
return n3;
}
int[] nArray7 = new int[3];
int[] nArray8 = new int[3];
nArray7[0] = nArray[n4];
nArray8[0] = nArray2[n4++];
--n;
nArray7[1] = nArray[n4];
nArray8[1] = nArray2[n4++];
--n;
while (true) {
if (n == 0) {
nArray3[n3] = nArray7[1];
nArray4[n3++] = nArray8[1];
return n3;
}
nArray7[2] = nArray[n4];
nArray8[2] = nArray2[n4];
if (n > 1) {
nArray7[2] = (nArray7[2] + nArray7[1]) / 2;
nArray8[2] = (nArray8[2] + nArray8[1]) / 2;
}
--n;
this.dCenterX = nArray7[0] + nArray7[2] - nArray7[1];
this.dCenterY = nArray8[0] + nArray8[2] - nArray8[1];
this.dP = (double) nArray7[0] - this.dCenterX;
this.dQ = (double) nArray8[2] - this.dCenterY;
this.dR = (double) nArray7[2] - this.dCenterX;
this.dS = (double) nArray8[0] - this.dCenterY;
int n10 = (int) this.dP;
int n11 = (int) this.dQ;
int n12 = (int) this.dR;
int n13 = (int) this.dS;
int n14 = Math.max(Math.abs(n10) + Math.abs(n13), Math.abs(n12) + Math.abs(n11));
this.dRad = n14;
if (n14 == 0) {
return n3;
}
int n15 = this.bres(this.dRad);
for (n2 = 0; n2 <= n15; ++n2) {
this.filletHODSegment2(nArray3, nArray4, n3, this.dXq[n2], this.dYq[n2]);
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
n6 = nArray3[n3];
n9 = nArray4[n3++];
}
for (n2 = n15; n2 >= 0; --n2) {
this.filletHODSegment2(nArray3, nArray4, n3, this.dYq[n2], this.dXq[n2]);
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
n6 = nArray3[n3];
n9 = nArray4[n3++];
}
if (n6 != nArray7[2] || n9 != nArray8[2]) {
nArray3[n3] = nArray7[2];
nArray4[n3++] = nArray8[2];
}
if (n == 0) {
return n3;
}
nArray7[0] = n6 = nArray7[2];
nArray8[0] = n9 = nArray8[2];
nArray7[1] = nArray[n4];
nArray8[1] = nArray2[n4++];
}
}
}
@@ -50,6 +50,19 @@ public class GddmCoordinateTransform {
private double transformX = 1.0;
private double transformY = 1.0;
// Aspect Ratio parameters (P, Q, R, S) per IBM Host On-Demand (HODDecoder / GSAP)
public static final int DEFAULT_P = 1;
public static final int DEFAULT_Q = 1;
public static final int DEFAULT_R = 0;
public static final int DEFAULT_S = 0;
private int paramP = DEFAULT_P;
private int paramQ = DEFAULT_Q;
private int paramR = DEFAULT_R;
private int paramS = DEFAULT_S;
private double scaleX = 1.0;
private double scaleY = 1.0;
public GddmCoordinateTransform() {
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
}
@@ -270,4 +283,68 @@ public class GddmCoordinateTransform {
public int convertYtoRow(int y) {
return (defaultCharHeight > 0) ? (y / defaultCharHeight) + 1 : 1;
}
// =========================================================================
// Aspect Ratio & Coordinate Inversion (Phase 17.2)
// =========================================================================
/**
* Sets GOCA aspect ratio parameters P, Q, R, S and updates scaling factors.
* ScaleX = P/Q, ScaleY = R/S. Defaults: P=1, Q=1, R=0, S=0.
*/
public void setAspectRatio(int p, int q, int r, int s) {
this.paramP = p;
this.paramQ = q;
this.paramR = r;
this.paramS = s;
this.scaleX = (q != 0) ? ((double) p / (double) q) : 1.0;
this.scaleY = (s != 0) ? ((double) r / (double) s) : 1.0;
if (this.scaleX <= 0.0) this.scaleX = 1.0;
if (this.scaleY <= 0.0) this.scaleY = 1.0;
}
/**
* Resets aspect ratio parameters to defaults: P=1, Q=1, R=0, S=0.
*/
public void resetAspectRatio() {
setAspectRatio(DEFAULT_P, DEFAULT_Q, DEFAULT_R, DEFAULT_S);
}
public int getParamP() { return paramP; }
public int getParamQ() { return paramQ; }
public int getParamR() { return paramR; }
public int getParamS() { return paramS; }
public double getScaleX() { return scaleX; }
public double getScaleY() { return scaleY; }
/**
* Inverts GOCA Cartesian Y (positive upward from bottom-left) to raster Y (top-down):
* Y_screen = (originY - gocaY) * ScaleY.
*/
public int mapGocaYToScreen(int originY, int gocaY) {
return (int) Math.round((originY - gocaY) * scaleY);
}
/**
* Inverts GOCA Cartesian Y to raster screen Y using the transform's default origin YMax.
*/
public int mapGocaYToScreen(int gocaY) {
return mapGocaYToScreen(getYMax(), gocaY);
}
/**
* Inverts raster Y (top-down) back to GOCA Cartesian Y (positive upward from bottom-left):
* Y_goca = originY - (screenY / ScaleY).
*/
public int mapScreenYToGoca(int originY, int screenY) {
return originY - (scaleY > 0 ? (int) Math.round(screenY / scaleY) : screenY);
}
/**
* Inverts raster screen Y back to GOCA Cartesian Y using the transform's default origin YMax.
*/
public int mapScreenYToGoca(int screenY) {
return mapScreenYToGoca(getYMax(), screenY);
}
}
@@ -40,10 +40,15 @@ public class GocaDecoder {
private double charHeight = 16.0;
private int charSet = 0;
private int charPrecision = GocaConstants.CP_STRING;
private int arcParamP = 1;
private int arcParamQ = 0;
private int arcParamR = 0;
private int arcParamS = 1;
public static final int DEFAULT_ARC_P = 1;
public static final int DEFAULT_ARC_Q = 1;
public static final int DEFAULT_ARC_R = 0;
public static final int DEFAULT_ARC_S = 0;
private int arcParamP = DEFAULT_ARC_P;
private int arcParamQ = DEFAULT_ARC_Q;
private int arcParamR = DEFAULT_ARC_R;
private int arcParamS = DEFAULT_ARC_S;
private boolean segChained = false;
private boolean segDynamic = false;
@@ -77,6 +82,7 @@ public class GocaDecoder {
private int imgHeight = 0;
private int imgBitDepth = GocaConstants.BPP_1;
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
private int imgMultiplier = 1;
private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
@@ -368,7 +374,15 @@ public class GocaDecoder {
inImage = false;
imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
imgMultiplier = 1;
imgBuffer.clear();
arcParamP = DEFAULT_ARC_P;
arcParamQ = DEFAULT_ARC_Q;
arcParamR = DEFAULT_ARC_R;
arcParamS = DEFAULT_ARC_S;
if (plane != null && plane.getTransform() != null) {
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
}
}
private byte[] partialOrderBuffer = new byte[0];
@@ -567,7 +581,10 @@ public class GocaDecoder {
int w = 0, h = 0;
int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
int imult = 1;
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
imult = readCoord(inputData, idx + 2);
if (imult <= 0) imult = 1;
w = readCoord(inputData, idx + 4);
h = readCoord(inputData, idx + 6);
if (payloadLen >= 7) {
@@ -592,7 +609,7 @@ public class GocaDecoder {
compression = inputData[idx + 7] & 0xFF;
}
}
beginImage(curX, curY, w, h, bitDepth, compression);
beginImage(curX, curY, w, h, bitDepth, compression, imult);
idx += orderLen;
break;
}
@@ -672,13 +689,25 @@ public class GocaDecoder {
idx += orderLen;
break;
}
case GocaConstants.G_GSAP: { // Arc Parameters (0x22)
case GocaConstants.G_GSAP: { // Arc Parameters / Set Aspect Ratio (0x22)
if (idx + 9 < end) {
arcParamP = readCoord(inputData, idx + 2);
arcParamQ = readCoord(inputData, idx + 4);
arcParamR = readCoord(inputData, idx + 6);
arcParamS = readCoord(inputData, idx + 8);
} else {
arcParamP = DEFAULT_ARC_P;
arcParamQ = DEFAULT_ARC_Q;
arcParamR = DEFAULT_ARC_R;
arcParamS = DEFAULT_ARC_S;
}
if (plane != null && plane.getTransform() != null) {
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
}
logger.info(String.format("GOCA GSAP: P=%d Q=%d R=%d S=%d (ScaleX=%.2f ScaleY=%.2f)",
arcParamP, arcParamQ, arcParamR, arcParamS,
(arcParamQ != 0 ? (double) arcParamP / arcParamQ : 1.0),
(arcParamS != 0 ? (double) arcParamR / arcParamS : 1.0)));
idx += orderLen;
break;
}
@@ -1012,7 +1041,10 @@ public class GocaDecoder {
int w, h;
int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
int imult = 1;
if (payloadLen >= 10) {
imult = readCoord(inputData, idx + 6);
if (imult <= 0) imult = 1;
w = readCoord(inputData, idx + 8);
h = readCoord(inputData, idx + 10);
if (payloadLen >= 11) {
@@ -1037,7 +1069,7 @@ public class GocaDecoder {
compression = inputData[idx + 11] & 0xFF;
}
}
beginImage(x, y, w, h, bitDepth, compression);
beginImage(x, y, w, h, bitDepth, compression, imult);
}
idx += orderLen;
break;
@@ -1309,10 +1341,14 @@ public class GocaDecoder {
}
private void beginImage(int x, int y, int w, int h) {
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED, 1);
}
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
beginImage(x, y, w, h, bitDepth, compression, 1);
}
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression, int multiplier) {
this.inImage = true;
this.imgX = x;
this.imgY = y;
@@ -1320,6 +1356,7 @@ public class GocaDecoder {
this.imgHeight = h;
this.imgBitDepth = bitDepth;
this.imgCompression = compression;
this.imgMultiplier = (multiplier > 0) ? multiplier : 1;
this.imgBuffer.clear();
}
@@ -1335,7 +1372,7 @@ public class GocaDecoder {
int px = plane.mapX(imgX);
int py = plane.mapY(imgY);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression, imgMultiplier);
inImage = false;
imgBuffer.clear();
}
@@ -1429,38 +1466,191 @@ public class GocaDecoder {
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
int pos = off;
int end = off + len;
if (isFull) {
// GFARC (0xC7) or GCFARC (0x87) Full Arc / Ellipse
int centerX = curX;
int centerY = curY;
if (!fromCurPos && pos + 4 <= end) {
centerX = readCoord(data, pos);
centerY = readCoord(data, pos + 2);
pos += 4;
}
double multiplier = 1.0;
if (pos + 2 <= end) {
int intPart = data[pos];
int fracPart = data[pos + 1] & 0xFF;
multiplier = intPart + fracPart / 256.0;
if (multiplier <= 0.0) multiplier = 1.0;
pos += 2;
}
double dP = arcParamP * multiplier;
double dQ = arcParamQ * multiplier;
double dR = arcParamR * multiplier;
double dS = arcParamS * multiplier;
int[] arcX = new int[256];
int[] arcY = new int[256];
int nPts = HODEllipse.getHODEllipse(dP, dQ, dR, dS, arcX, arcY, centerX, centerY);
for (int i = 0; i < nPts; i++) {
trackPoint(arcX[i], arcY[i]);
}
if (inArea) {
for (int i = 0; i < nPts; i++) {
if (i == 0 && currentPolyPts == 0) {
addAreaLineStart(arcX[i], arcY[i]);
} else {
addAreaPoint(arcX[i], arcY[i]);
}
}
} else {
for (int i = 0; i < nPts - 1; i++) {
plane.drawLine(plane.mapXDouble(arcX[i]), plane.mapYDouble(arcY[i]),
plane.mapXDouble(arcX[i + 1]), plane.mapYDouble(arcY[i + 1]),
curColor, lineType, lineWidth);
}
}
curX = centerX;
curY = centerY;
return;
}
// Partial Arc: GARC (0xC6) or GCARC (0x86)
// 3-point circular arc definition per GOCA specs: start, intermediate, end
int x0 = curX;
int y0 = curY;
boolean has3Point = false;
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
if (!fromCurPos && len >= 12 && pos + 12 <= end) {
x0 = readCoord(data, pos);
y0 = readCoord(data, pos + 2);
x1 = readCoord(data, pos + 4);
y1 = readCoord(data, pos + 6);
x2 = readCoord(data, pos + 8);
y2 = readCoord(data, pos + 10);
has3Point = true;
} else if (fromCurPos && len >= 8 && pos + 8 <= end) {
x1 = readCoord(data, pos);
y1 = readCoord(data, pos + 2);
x2 = readCoord(data, pos + 4);
y2 = readCoord(data, pos + 6);
has3Point = true;
} else if (!fromCurPos && len >= 8 && pos + 8 <= end) {
x1 = readCoord(data, pos);
y1 = readCoord(data, pos + 2);
x2 = readCoord(data, pos + 4);
y2 = readCoord(data, pos + 6);
has3Point = true;
}
if (has3Point) {
double d = 2.0 * (x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1));
if (Math.abs(d) < 1e-6) {
// Collinear points: draw straight line segments P0 -> P1 -> P2
trackPoint(x0, y0);
trackPoint(x1, y1);
trackPoint(x2, y2);
if (inArea) {
if (currentPolyPts == 0) addAreaLineStart(x0, y0);
addAreaPoint(x1, y1);
addAreaPoint(x2, y2);
} else {
plane.drawLine(plane.mapXDouble(x0), plane.mapYDouble(y0),
plane.mapXDouble(x1), plane.mapYDouble(y1),
curColor, lineType, lineWidth);
plane.drawLine(plane.mapXDouble(x1), plane.mapYDouble(y1),
plane.mapXDouble(x2), plane.mapYDouble(y2),
curColor, lineType, lineWidth);
}
curX = x2;
curY = y2;
return;
}
double x0sq = (double) x0 * x0 + (double) y0 * y0;
double x1sq = (double) x1 * x1 + (double) y1 * y1;
double x2sq = (double) x2 * x2 + (double) y2 * y2;
double ccx = (x0sq * (y1 - y2) + x1sq * (y2 - y0) + x2sq * (y0 - y1)) / d;
double ccy = (x0sq * (x2 - x1) + x1sq * (x0 - x2) + x2sq * (x1 - x0)) / d;
double rad = Math.hypot(x0 - ccx, y0 - ccy);
double theta0 = Math.atan2(y0 - ccy, x0 - ccx);
double theta1 = Math.atan2(y1 - ccy, x1 - ccx);
double theta2 = Math.atan2(y2 - ccy, x2 - ccx);
if (theta0 < 0) theta0 += 2 * Math.PI;
if (theta1 < 0) theta1 += 2 * Math.PI;
if (theta2 < 0) theta2 += 2 * Math.PI;
double d01 = theta1 - theta0;
while (d01 < 0) d01 += 2 * Math.PI;
double d02 = theta2 - theta0;
while (d02 < 0) d02 += 2 * Math.PI;
double sweep = (d01 < d02) ? d02 : (d02 - 2 * Math.PI);
int steps = Math.max(8, (int) Math.round(Math.abs(Math.toDegrees(sweep)) / 5.0));
int prevX = x0;
int prevY = y0;
trackPoint(x0, y0);
if (inArea && currentPolyPts == 0) {
addAreaLineStart(x0, y0);
}
for (int s = 1; s <= steps; s++) {
double t = (double) s / steps;
double angle = theta0 + t * sweep;
int px = (s == steps) ? x2 : (int) Math.round(ccx + rad * Math.cos(angle));
int py = (s == steps) ? y2 : (int) Math.round(ccy + rad * Math.sin(angle));
trackPoint(px, py);
if (inArea) {
addAreaPoint(px, py);
} else {
plane.drawLine(plane.mapXDouble(prevX), plane.mapYDouble(prevY),
plane.mapXDouble(px), plane.mapYDouble(py),
curColor, lineType, lineWidth);
}
prevX = px;
prevY = py;
}
curX = x2;
curY = y2;
return;
}
// Fallback: sweep angle parameter decoding
int centerX = curX;
int centerY = curY;
// For absolute variants, read the center point from data
if (!fromCurPos && pos + 4 <= off + len) {
if (!fromCurPos && pos + 4 <= end) {
centerX = readCoord(data, pos);
centerY = readCoord(data, pos + 2);
pos += 4;
}
// Read the multiplier (integer.fraction format)
double multiplier = 1.0;
if (pos + 2 <= off + len) {
int intPart = data[pos]; // signed byte
if (pos + 2 <= end) {
int intPart = data[pos];
int fracPart = data[pos + 1] & 0xFF;
multiplier = intPart + fracPart / 256.0;
if (multiplier == 0.0) multiplier = 1.0;
pos += 2;
}
// Compute semi-axes from arc parameters scaled by multiplier
// P,Q and R,S are conjugate semi-diameter vectors
double pScaled = arcParamP * multiplier;
double qScaled = arcParamQ * multiplier;
double rScaled = arcParamR * multiplier;
double sScaled = arcParamS * multiplier;
// Semi-axis lengths in GOCA coordinate space
double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
// If arc parameters are at default (1,0,0,1) and curPos != center, use distance to center as radius
double dist = Math.hypot(curX - centerX, curY - centerY);
if (semiAxis1 <= 1.01 && dist > 1.0) {
semiAxis1 = dist * multiplier;
@@ -1470,7 +1660,6 @@ public class GocaDecoder {
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
// Map to pixel space with double precision
double cx = plane.mapXDouble(centerX);
double cy = plane.mapYDouble(centerY);
double rx = Math.abs(plane.mapXDouble(semiAxis1) - plane.mapXDouble(0));
@@ -1478,38 +1667,24 @@ public class GocaDecoder {
if (rx <= 0.0) rx = Math.max(1.0, semiAxis1);
if (ry <= 0.0) ry = Math.max(1.0, semiAxis2);
// For partial arcs, determine start angle and sweep angle
double startAngleDeg = 0.0;
double sweepAngleDeg = 360.0;
if (!isFull) {
if (pos + 4 <= off + len) {
// Sweep start: integer.fraction of full revolution
int startInt = data[pos];
int startFrac = data[pos + 1] & 0xFF;
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
pos += 2;
// Sweep angle: integer.fraction of full revolution
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
} else if (pos + 2 <= off + len) {
// Single sweep parameter
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
} else {
// No sweep data — if we have a current point, start there and sweep full circle
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
sweepAngleDeg = 360.0;
if (pos + 4 <= end) {
int startInt = data[pos];
int startFrac = data[pos + 1] & 0xFF;
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
pos += 2;
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
} else if (pos + 2 <= end) {
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
}
@@ -1533,7 +1708,7 @@ public class GocaDecoder {
}
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull);
curColor, lineType, lineWidth, false);
curX = centerX;
curY = centerY;
@@ -1543,63 +1718,62 @@ public class GocaDecoder {
int pos = off;
int end = off + len;
List<Double> ptsX = new ArrayList<>();
List<Double> ptsY = new ArrayList<>();
List<Integer> gocaPtsX = new ArrayList<>();
List<Integer> gocaPtsY = new ArrayList<>();
if (fromCurPos) {
trackPoint(curX, curY);
ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY));
gocaPtsX.add(curX);
gocaPtsY.add(curY);
if (inArea && currentPolyPts == 0) {
addAreaLineStart(curX, curY);
}
}
while (pos + 4 <= end) {
int x = readCoord(data, pos);
int y = readCoord(data, pos + 2);
trackPoint(x, y);
ptsX.add(plane.mapXDouble(x));
ptsY.add(plane.mapYDouble(y));
gocaPtsX.add(x);
gocaPtsY.add(y);
if (inArea && ptsX.size() == 1 && currentPolyPts == 0) {
addAreaLineStart(x, y);
}
curX = x;
curY = y;
pos += 4;
}
if (ptsX.size() >= 2) {
int n = ptsX.size();
double[] px = new double[n];
double[] py = new double[n];
for (int i = 0; i < n; i++) {
px[i] = ptsX.get(i);
py[i] = ptsY.get(i);
}
int n = gocaPtsX.size();
if (n < 2) return;
int[] ctrlX = new int[n];
int[] ctrlY = new int[n];
for (int i = 0; i < n; i++) {
ctrlX[i] = gocaPtsX.get(i);
ctrlY[i] = gocaPtsY.get(i);
trackPoint(ctrlX[i], ctrlY[i]);
}
FilletPts fillet = new FilletPts();
int req = fillet.getPointsRequired(n);
int[] outX = new int[Math.max(req, n * 32)];
int[] outY = new int[Math.max(req, n * 32)];
int nPoints = fillet.getFilletPoints(ctrlX, ctrlY, n, outX, outY);
if (nPoints > 0) {
for (int i = 0; i < nPoints; i++) {
trackPoint(outX[i], outY[i]);
}
if (inArea) {
int gn = gocaPtsX.size();
int[] gx = new int[gn];
int[] gy = new int[gn];
for (int i = 0; i < gn; i++) {
gx[i] = gocaPtsX.get(i);
gy[i] = gocaPtsY.get(i);
}
int[][] curve = FilletPts.calculate(gx, gy, gn, 16);
for (int i = 1; i < curve[0].length; i++) {
addAreaPoint(curve[0][i], curve[1][i]);
for (int i = 0; i < nPoints; i++) {
if (i == 0 && currentPolyPts == 0) {
addAreaLineStart(outX[0], outY[0]);
} else {
addAreaPoint(outX[i], outY[i]);
}
}
} else {
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
for (int i = 0; i < nPoints - 1; i++) {
plane.drawLine(plane.mapXDouble(outX[i]), plane.mapYDouble(outY[i]),
plane.mapXDouble(outX[i + 1]), plane.mapYDouble(outY[i + 1]),
curColor, lineType, lineWidth);
}
}
}
curX = ctrlX[n - 1];
curY = ctrlY[n - 1];
}
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
@@ -1824,6 +1998,15 @@ public class GocaDecoder {
resetDefaults();
}
public synchronized int getArcParamP() { return arcParamP; }
public synchronized int getArcParamQ() { return arcParamQ; }
public synchronized int getArcParamR() { return arcParamR; }
public synchronized int getArcParamS() { return arcParamS; }
public synchronized int getImageMultiplier() { return imgMultiplier; }
public synchronized boolean hasActiveGraphics() {
return graphicsCursorActive || (plane != null && plane.hasContent());
}
private int readCoord(byte[] data, int off) {
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
}
@@ -1221,12 +1221,12 @@ public class GraphicsPlane implements PixelBuffer {
for (int i = 0; i < pLen - 1; i++) {
drawLine((double) px[offset + i], (double) py[offset + i],
(double) px[offset + i + 1], (double) py[offset + i + 1],
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
boundaryColorArgb, lineType, lineWidth);
}
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
(double) px[offset], (double) py[offset],
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
boundaryColorArgb, lineType, lineWidth);
}
}
offset += pLen;
@@ -1417,6 +1417,16 @@ public class GraphicsPlane implements PixelBuffer {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
}
public synchronized void drawVectorString(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
}
public synchronized void drawVectorString(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
}
@@ -1530,8 +1540,17 @@ public class GraphicsPlane implements PixelBuffer {
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
int bitDepth, int compressionMode) {
drawImage(x, y, width, height, imageData, fgColorArgb, bitDepth, compressionMode, 1);
}
/**
* Draws bitmap image with support for bit depth, RLE decompression, and image multiplier (imult).
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
int bitDepth, int compressionMode, int multiplier) {
if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int m = (multiplier > 0) ? multiplier : 1;
byte[] rawData = imageData;
if (compressionMode == GocaConstants.IMG_RLE) {
@@ -1549,7 +1568,15 @@ public class GraphicsPlane implements PixelBuffer {
if (byteIdx < rawData.length) {
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
if (m <= 1) {
setPixel(x + col, y + row, fgColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, fgColor);
}
}
}
}
}
}
@@ -1572,7 +1599,15 @@ public class GraphicsPlane implements PixelBuffer {
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
default: pixelColor = fgColor; break;
}
setPixel(x + col, y + row, pixelColor);
if (m <= 1) {
setPixel(x + col, y + row, pixelColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
}
}
}
}
}
}
@@ -1588,7 +1623,16 @@ public class GraphicsPlane implements PixelBuffer {
int shift = (1 - (col % 2)) * 4;
int val = (rawData[byteIdx] >> shift) & 0x0F;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
int pixelColor = GocaConstants.getGocaColorArgb(val);
if (m <= 1) {
setPixel(x + col, y + row, pixelColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
}
}
}
}
}
}
@@ -1602,7 +1646,16 @@ public class GraphicsPlane implements PixelBuffer {
if (byteIdx < rawData.length) {
int val = rawData[byteIdx] & 0xFF;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
int pixelColor = GocaConstants.getGocaColorArgb(val);
if (m <= 1) {
setPixel(x + col, y + row, pixelColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
}
}
}
}
}
}
@@ -0,0 +1,151 @@
package haus.nightmare.lib3270j.graphics;
/**
* Calculates intermediate points for IBM GOCA ellipses and arcs using conjugate semi-axes P, Q, R, S.
* Matches IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODEllipse and HODGraphUtil).
*/
public class HODEllipse {
protected static final double[] dXgxdq = new double[] {
1.0, 0.999847695, 0.999390827, 0.998629534, 0.99756405, 0.996194698,
0.994521895, 0.992546151, 0.990268068, 0.98768834, 0.984807753, 0.981627183,
0.9781476, 0.974370064, 0.970295726, 0.965925826, 0.961261695, 0.956304756,
0.951056516, 0.945518575, 0.93969262, 0.933580426, 0.927183854, 0.920504853,
0.913545457, 0.906307787, 0.898794046, 0.891006524, 0.882947592, 0.874619707,
0.866025403, 0.8571673, 0.848048096, 0.838670567, 0.829037572, 0.819152044,
0.809016994, 0.79863551, 0.788010753, 0.777145961, 0.766044443, 0.75470958,
0.743144825, 0.731353701, 0.7193398, 0.707106781
};
protected static final double[] dXgydq = new double[] {
0.0, 0.017452406, 0.034899496, 0.052335956, 0.069756473, 0.087155742,
0.104528463, 0.121869343, 0.139173101, 0.156434465, 0.173648177, 0.190808995,
0.20791169, 0.224951054, 0.241921895, 0.258819045, 0.275637355, 0.292371704,
0.309016994, 0.325568154, 0.342020143, 0.358367949, 0.374606593, 0.390731128,
0.406736643, 0.422618261, 0.438371146, 0.453990499, 0.469471562, 0.48480962,
0.5, 0.515038074, 0.529919264, 0.544639035, 0.559192903, 0.573576436,
0.587785252, 0.601815023, 0.615661475, 0.629320391, 0.642787609, 0.656059029,
0.669130606, 0.68199836, 0.69465837, 0.707106781
};
protected double dP;
protected double dQ;
protected double dR;
protected double dS;
protected int iP;
protected int iQ;
protected int iR;
protected int iS;
protected double dRad;
protected double dCenterX;
protected double dCenterY;
protected double[] dXq = new double[9];
protected double[] dYq = new double[9];
public HODEllipse(double p, double q, double r, double s) {
this.dP = p;
this.dQ = q;
this.dR = r;
this.dS = s;
this.iP = (int) Math.round(p);
this.iQ = (int) Math.round(q);
this.iR = (int) Math.round(r);
this.iS = (int) Math.round(s);
}
public int getHODPoints(int[] outX, int[] outY, int centerX, int centerY) {
if (outX == null || outY == null || outX.length < 128 || outY.length < 128) {
return 0;
}
int ptIdx = 0;
int n5 = Math.max(Math.abs(this.iP) + Math.abs(this.iS), Math.abs(this.iR) + Math.abs(this.iQ));
this.dRad = n5;
this.dCenterX = centerX;
this.dCenterY = centerY;
if (n5 == 0) {
outX[0] = centerX;
outY[0] = centerY;
return 1;
}
int n6 = this.bres(this.dRad);
int lastX = this.iP + centerX;
int lastY = this.iS + centerY;
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], -this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], -this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], -this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], -this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
outX[ptIdx] = outX[0];
outY[ptIdx++] = outY[0];
return ptIdx;
}
protected int bres(double rad) {
for (int i = 0; i < 9; ++i) {
this.dXq[i] = rad * dXgxdq[i * 5 + 2];
this.dYq[i] = rad * dXgydq[i * 5 + 2];
}
return 8;
}
protected void filletHODSegment(int[] outX, int[] outY, int idx, double d, double d2) {
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
outY[idx] = (int) Math.round(-(this.dS * d / this.dRad + this.dQ * d2 / this.dRad) + this.dCenterY);
}
protected void filletHODSegment2(int[] outX, int[] outY, int idx, double d, double d2) {
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
outY[idx] = (int) Math.round(this.dS * d / this.dRad + this.dQ * d2 / this.dRad + this.dCenterY);
}
public static int getHODEllipse(double p, double q, double r, double s, int[] outX, int[] outY, int cx, int cy) {
HODEllipse ellipse = new HODEllipse(p, q, r, s);
return ellipse.getHODPoints(outX, outY, cx, cy);
}
}
@@ -18,11 +18,12 @@ public class ProgramSymbolManager {
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
private int defaultCellWidth = 9;
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
private int defaultCellHeight = 16; // Standard IBM 9x16 Programmed Symbol cell matrix
public void setDefaultCellDimensions(int width, int height) {
this.defaultCellWidth = (width > 0) ? width : 9;
this.defaultCellHeight = (height > 0) ? height : 12;
this.defaultCellHeight = (height > 0) ? height : 16;
}
public int getDefaultCellWidth() {
@@ -194,20 +195,24 @@ public class ProgramSymbolManager {
boolean isTriplePlane = (rws >= 4 && rws <= 7);
ProgramSymbolSet set = sets[setIndex];
if (clearAll) {
set.clear();
}
int extHeaderLen = 0;
int cellWidth = defaultCellWidth;
int cellHeight = defaultCellHeight;
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
int cellWidth = (loadFormat == 1 || rws < 8) ? 9 : defaultCellWidth;
int cellHeight = (loadFormat == 1 || rws < 8) ? 16 : defaultCellHeight;
int colorPlane = 0; // 0 = all planes, 1 = Blue, 2 = Red, 4 = Green
if (hasExtHeader && data.length > 4) {
extHeaderLen = data[4] & 0xFF;
if (extHeaderLen > 3 && data.length > 6) {
int lw = data[6] & 0xFF;
if (lw > 0) cellWidth = lw;
if (lw > 0 && loadFormat != 1) cellWidth = lw;
}
if (extHeaderLen > 4 && data.length > 7) {
int lh = data[7] & 0xFF;
if (lh > 0) cellHeight = lh;
if (lh > 0 && loadFormat != 1) cellHeight = lh;
}
if (extHeaderLen >= 6 && data.length > 9) {
colorPlane = data[9] & 0xFF;
@@ -223,55 +228,34 @@ public class ProgramSymbolManager {
int remaining = data.length - offset;
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (sliceBytes <= 0) sliceBytes = 18;
int bytesPerSymbol;
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
bytesPerSymbol = sliceBytes * 3;
} else {
bytesPerSymbol = sliceBytes;
}
int bytesPerSymbol = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (bytesPerSymbol <= 0) bytesPerSymbol = 18;
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
byte[] pixelData = new byte[cellWidth * cellHeight];
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
// If loading a specific color plane (colorPlane != 0), merge with existing ONLY IF existing was not all-planes
if (isTriplePlane && colorPlane != 0 && existing != null && !existing.isAllPlanesLoaded()
&& existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0,
Math.min(existing.getPixelData().length, pixelData.length));
}
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
// 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
}
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
boolean allPlanes = (colorPlane == 0);
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane, allPlanes));
codeIndex++;
offset += bytesPerSymbol;
remaining -= bytesPerSymbol;
}
if (clearAll) {
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
set.clearSlot(i);
}
}
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
@@ -62,6 +62,7 @@ public class ProgramSymbolSet {
private final int height;
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
private final boolean isTriplePlane;
private final boolean allPlanesLoaded;
private int[] cachedRgbArray;
private int cachedFgRgb = -1;
private int cachedBgRgb = -1;
@@ -73,10 +74,19 @@ public class ProgramSymbolSet {
private int cachedScaledBgRgb = -1;
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
this(width, height, pixelData, isTriplePlane, false);
}
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane, boolean allPlanesLoaded) {
this.width = width > 0 ? width : 9;
this.height = height > 0 ? height : 16;
this.pixelData = pixelData;
this.isTriplePlane = isTriplePlane;
this.allPlanesLoaded = allPlanesLoaded;
}
public boolean isAllPlanesLoaded() {
return allPlanesLoaded;
}
public int getWidth() {
@@ -183,18 +193,18 @@ public class ProgramSymbolSet {
} else if (!isTriplePlane) {
rgbArray[idx] = fgArgb;
} else {
// Triple-Plane RGB composite aligned with IBM Host On-Demand 16-color GOCA palette:
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
// Triple-Plane RGB composite aligned with IBM 3179G / HoD GOCA palette:
// val bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
int colorMask = val & 0x07;
int colorArgb;
switch (colorMask) {
case 1: colorArgb = GocaConstants.GOCA_COLORS[2]; break; // Red
case 2: colorArgb = GocaConstants.GOCA_COLORS[4]; break; // Green
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue (CUSTOMBLUE)
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow (Red + Green)
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink (Red + Blue)
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise (Green + Blue)
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White (Red + Green + Blue)
default: colorArgb = bgArgb; break;
}
rgbArray[idx] = colorArgb;
@@ -846,6 +846,8 @@ public class InputProcessor {
if (graphicsPlane != null) {
graphicsPlane.clear();
}
lastAid = aidCode;
setKeyboardLocked(true);
byte[] data = new byte[] { (byte) aidCode };
sendAidResponse(data);
return;
@@ -948,6 +950,15 @@ public class InputProcessor {
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
*/
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
boolean hasGraphics = gocaDecoder != null && (gocaDecoder.isGraphicsCursorActive() ||
(gocaDecoder.getGraphicsPlane() != null && gocaDecoder.getGraphicsPlane().hasContent()));
if (hasGraphics) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
sendGraphicMouseAid(gx, gy, aidCode, button, isShift, isCtrl);
return;
}
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
return;
}
@@ -957,41 +968,51 @@ public class InputProcessor {
setKeyboardLocked(true);
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
int row = cursorAddr / cols;
int col = cursorAddr % cols;
byte[] rma = buildReadModifiedInboundData(aidCode, false);
try {
out.write(rma);
} catch (java.io.IOException ignored) {}
sendAidResponse(out.toByteArray());
}
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, row, col, button, true, isShift, isCtrl
);
StringBuilder sfHex = new StringBuilder();
for (byte b : sf) {
sfHex.append(String.format("%02X ", b & 0xFF));
}
log.info(String.format(
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
));
// Structured Field AID (0x88) + 56-byte Graphic Input SF
out.write(AID_SF);
try {
out.write(sf);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
out.write(rma);
} catch (java.io.IOException ignored) {}
} else {
byte[] rma = buildReadModifiedInboundData(aidCode, false);
try {
out.write(rma);
} catch (java.io.IOException ignored) {}
/**
* Transmits a mouse / light-pen graphic input structured field AID with explicit GOCA coordinates.
*/
public void sendGraphicMouseAid(int gx, int gy, int aidCode, int button, boolean isShift, boolean isCtrl) {
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
return;
}
if (isKeyboardLocked()) {
return;
}
setKeyboardLocked(true);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
int row = cursorAddr / cols;
int col = cursorAddr % cols;
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, row, col, button, true, isShift, isCtrl
);
StringBuilder sfHex = new StringBuilder();
for (byte b : sf) {
sfHex.append(String.format("%02X ", b & 0xFF));
}
log.info(String.format(
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
));
// Structured Field AID (0x88) + 56-byte Graphic Input SF
out.write(AID_SF);
try {
out.write(sf);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
out.write(rma);
} catch (java.io.IOException ignored) {}
sendAidResponse(out.toByteArray());
}
@@ -1030,7 +1051,12 @@ public class InputProcessor {
screen.setCursorAddress(address);
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
// Per IBM 3270 Reference (GA23-0059), light-pen selection has no effect
// on protected fields with a space or null designator character.
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa)) {
return false;
}
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
@@ -1448,6 +1448,11 @@ public class TelnetFSM {
* Send a 3270 data record, with TN3270E header if in TN3270E mode.
*/
public void send3270Data(byte[] data) {
if (data != null && data.length >= 2 && (data[data.length - 2] & 0xFF) == IAC && (data[data.length - 1] & 0xFF) == EOR) {
byte[] trimmed = new byte[data.length - 2];
System.arraycopy(data, 0, trimmed, 0, trimmed.length);
data = trimmed;
}
if (tn3270eNegotiated) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
// TN3270E header
@@ -376,6 +376,14 @@ public class Xfer3270 implements FileTransferInterface {
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
config.setClear(this.bSendClear);
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
config.setOptionNew(this.option_NEW);
config.setOptionNoso(this.option_NOSO);
if (this.option_UNICODE) {
config.setUnicode(true);
config.setUnicodeType(this.unicodeType);
}
config.setDftBufferSize(bmtuSize);
if (options != null && !options.isEmpty()) {
config.parseOptions(options);
@@ -387,7 +395,38 @@ public class Xfer3270 implements FileTransferInterface {
public void getFile(String command, String hostFile, String localFile, int mode,
String options, ECLXferListener listener) throws ECLErr {
FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile);
getFile(hostFile, MVS_TSO, localFile, mode, statusAdapter, options);
setOptionFlags(options);
if (mode == BINARY) {
this.option_ASCII = false;
} else {
this.option_ASCII = true;
}
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE);
config.setLocalFilename(localFile);
config.setHostFilename(hostFile);
config.setHostType(FTConfig.HostType.TSO);
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
config.setClear(this.bSendClear);
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
config.setOptionNew(this.option_NEW);
config.setOptionNoso(this.option_NOSO);
if (command != null && !command.trim().isEmpty()) {
config.setProgramName(command.trim());
}
if (this.option_UNICODE) {
config.setUnicode(true);
config.setUnicodeType(this.unicodeType);
}
config.setDftBufferSize(bmtuSize);
if (options != null && !options.isEmpty()) {
config.parseOptions(options);
}
executeTransfer(config, statusAdapter, localFile, hostFile);
}
@Override
@@ -408,6 +447,15 @@ public class Xfer3270 implements FileTransferInterface {
(hostType == CICS ? FTConfig.HostType.CICS : FTConfig.HostType.TSO));
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
config.setClear(this.bSendClear);
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
config.setOptionNew(this.option_NEW);
config.setOptionNoso(this.option_NOSO);
if (this.option_UNICODE) {
config.setUnicode(true);
config.setUnicodeType(this.unicodeType);
}
config.setRecfm(recfm);
config.setLrecl(String.valueOf(lrecl));
config.setBlksize(String.valueOf(blksize));
@@ -429,7 +477,48 @@ public class Xfer3270 implements FileTransferInterface {
public void putFile(String command, String hostFile, String localFile, int mode,
String options, ECLXferListener listener) throws ECLErr {
FileTransferStatusInterface statusAdapter = createStatusAdapter(listener, localFile, hostFile);
putFile(localFile, MVS_TSO, hostFile, mode, statusAdapter, options);
setOptionFlags(options);
if (mode == BINARY) {
this.option_ASCII = false;
} else {
this.option_ASCII = true;
}
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setLocalFilename(localFile);
config.setHostFilename(hostFile);
config.setHostType(FTConfig.HostType.TSO);
config.setTransferMode(this.option_ASCII ? FTConfig.TransferMode.ASCII : FTConfig.TransferMode.BINARY);
config.setCrAction(this.option_CRLF ? FTConfig.CrAction.REMOVE : FTConfig.CrAction.KEEP);
config.setExistAction(this.option_APPEND ? FTConfig.ExistAction.APPEND : FTConfig.ExistAction.REPLACE);
config.setClear(this.bSendClear);
config.setDbcs(this.currentCodePage != null && this.currentCodePage.isDBCS());
config.setOptionNew(this.option_NEW);
config.setOptionNoso(this.option_NOSO);
if (command != null && !command.trim().isEmpty()) {
config.setProgramName(command.trim());
}
if (this.option_UNICODE) {
config.setUnicode(true);
config.setUnicodeType(this.unicodeType);
}
config.setRecfm(recfm);
config.setLrecl(String.valueOf(lrecl));
config.setBlksize(String.valueOf(blksize));
config.setDftBufferSize(bmtuSize);
if (primarySpace > 0) {
config.setSpace(primarySpace + (secondarySpace > 0 ? "," + secondarySpace : ""));
}
if ("TRACKS".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.TRACKS);
else if ("CYLINDERS".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.CYLINDERS);
else if ("AVBLOCK".equalsIgnoreCase(spaceUnits)) config.setUnits(FTConfig.AllocationUnit.AVBLOCK);
if (options != null && !options.isEmpty()) {
config.parseOptions(options);
}
executeTransfer(config, statusAdapter, localFile, hostFile);
}
private void executeTransfer(FTConfig config, FileTransferStatusInterface status,