Files
j3270/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java
T
rudi 46a022d86b
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
Add bugfixes from discord
2026-09-10 09:24:39 -04:00

601 lines
24 KiB
Java

package haus.nightmare.lib3270j.xfer3270;
import haus.nightmare.lib3270j.charset.CodePage;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.ecl.*;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryParser;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.xfer.FileTransferHostDirectoryInterface;
import haus.nightmare.lib3270j.xfer.FileTransferInterface;
import haus.nightmare.lib3270j.xfer.FileTransferStatusInterface;
import java.net.URL;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Core 3270 File Transfer Controller conforming to Host On-Demand specifications.
*
* Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options,
* dynamic MTU buffering, host/local dataset name mappings, directory queries,
* and Unicode transfer modes.
*/
public class Xfer3270 implements FileTransferInterface {
private static final Logger log = Logger.getLogger(Xfer3270.class.getName());
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
public static final String UNICODE_UCS2_STR = "UCS2";
public static final String UNICODE_UTF8_STR = "UTF8";
public static final String UNICODE_UTF_8_STR = "UTF-8";
public static final int UNICODE_UCS2 = 0;
public static final int UNICODE_UTF8 = 1;
public static final String HOST_FILE_ERROR = "HOST_FILE_ERROR";
public static final int MIN_MTU_SIZE = 256;
public static final int MAX_MTU_SIZE = 32767;
public static final int DefaultUploadBufferSize = 2048;
// Session references
private ECLSession session;
private ScreenBuffer screen;
private InputProcessor input;
private DataStreamProcessor dsProcessor;
private EbcdicTranslator translator;
private CodePage currentCodePage;
private ECLXfer delegateXfer;
// State and Configuration
private int bmtuSize = 2500;
public int TimeOutValue = 30;
private long bytesTransferred = 0L;
private String sReasonTerminated = "";
private boolean bSendClear = true;
private boolean bCancelRequested = false;
// HoD Option Flags
public boolean option_ASCII = false;
public boolean option_SO = false;
public boolean option_NOSO = false;
public boolean option_BLANK = false;
public boolean option_USER = false;
public boolean option_APPEND = false;
public boolean option_NEW = false;
public boolean option_CRLF = false;
public boolean option_UNICODE = false;
public int unicodeType = UNICODE_UCS2;
// Mainframe dataset allocation parameters
private String recfm = "DEFAULT";
private int lrecl = 0;
private int blksize = 0;
private int primarySpace = 0;
private int secondarySpace = 0;
private String spaceUnits = "DEFAULT";
private int avblock = 0;
// Constructors
public Xfer3270() {
this.translator = new EbcdicTranslator();
this.currentCodePage = CodePageRegistry.getDefault();
}
public Xfer3270(ECLSession session) {
this(session, null);
}
public Xfer3270(ECLSession session, URL url) {
this.session = session;
if (session != null) {
this.delegateXfer = session.GetXfer();
}
this.translator = new EbcdicTranslator();
this.currentCodePage = CodePageRegistry.getDefault();
}
public Xfer3270(ECLXfer xfer) {
this.delegateXfer = xfer;
this.translator = new EbcdicTranslator();
this.currentCodePage = CodePageRegistry.getDefault();
}
public Xfer3270(ScreenBuffer screen, InputProcessor input,
DataStreamProcessor dsProcessor, CodePage codePage) {
this.screen = screen;
this.input = input;
this.dsProcessor = dsProcessor;
this.currentCodePage = codePage != null ? codePage : CodePageRegistry.getDefault();
this.translator = new EbcdicTranslator(this.currentCodePage.getCodePageId());
this.delegateXfer = new ECLXfer(screen, input, dsProcessor, translator);
}
// ========== Option Flags Parser ==========
/**
* Parses TSO and VM option flags into internal state, conforming to HoD syntax.
* @param options Option flags string
*/
public void setOptionFlags(String options) {
this.option_ASCII = false;
this.option_SO = false;
this.option_NOSO = false;
this.option_BLANK = false;
this.option_USER = false;
this.option_APPEND = false;
this.option_NEW = false;
this.option_CRLF = false;
this.option_UNICODE = false;
this.unicodeType = UNICODE_UCS2;
this.recfm = "DEFAULT";
this.lrecl = 0;
this.blksize = 0;
this.primarySpace = 0;
this.secondarySpace = 0;
this.spaceUnits = "DEFAULT";
this.avblock = 0;
if (options == null || options.trim().isEmpty()) return;
String upper = options.toUpperCase();
if (upper.contains("ASCII")) this.option_ASCII = true;
if (upper.contains(" SO") || upper.contains("(SO")) this.option_SO = true;
if (upper.contains("NOSO")) this.option_NOSO = true;
if (upper.contains("BLANK")) this.option_BLANK = true;
if (upper.contains("USER")) this.option_USER = true;
if (upper.contains("APPEND")) this.option_APPEND = true;
if (upper.contains("NEW")) this.option_NEW = true;
if (upper.contains("CRLF") && !upper.contains("NOCRLF")) this.option_CRLF = true;
// UNICODE options: UNICODE or UNICODE(UTF-8) / UNICODE(UCS2)
int uIdx = upper.indexOf("UNICODE");
if (uIdx > -1) {
this.option_UNICODE = true;
int pStart = upper.indexOf('(', uIdx);
int pEnd = upper.indexOf(')', uIdx);
String uSub = "";
if (pStart > uIdx && pEnd > pStart) {
uSub = upper.substring(pStart + 1, pEnd);
}
if (uSub.contains(UNICODE_UTF8_STR) || uSub.contains(UNICODE_UTF_8_STR)) {
this.unicodeType = UNICODE_UTF8;
} else {
this.unicodeType = UNICODE_UCS2;
}
}
// Mainframe dataset parameters
Matcher recfmMatcher = RECFM_PATTERN.matcher(options);
if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase();
Matcher lreclMatcher = LRECL_PATTERN.matcher(options);
if (lreclMatcher.find()) {
try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {}
}
Matcher blkMatcher = BLK_PATTERN.matcher(options);
if (blkMatcher.find()) {
try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {}
}
Matcher spaceMatcher = SPACE_PATTERN.matcher(options);
if (spaceMatcher.find()) {
try {
this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
if (spaceMatcher.group(2) != null) {
this.secondarySpace = Integer.parseInt(spaceMatcher.group(2));
}
} catch (NumberFormatException ignored) {}
}
Matcher avbMatcher = AVB_PATTERN.matcher(options);
if (avbMatcher.find()) {
try {
this.avblock = Integer.parseInt(avbMatcher.group(1));
this.spaceUnits = "AVBLOCK";
} catch (NumberFormatException ignored) {}
}
if (upper.contains("TRACKS") || upper.contains("TRK")) {
this.spaceUnits = "TRACKS";
} else if (upper.contains("CYLINDERS") || upper.contains("CYL")) {
this.spaceUnits = "CYLINDERS";
}
Matcher mtuMatcher = MTU_PATTERN.matcher(options);
if (mtuMatcher.find()) {
try {
SetMTUSize(Integer.parseInt(mtuMatcher.group(1)));
} catch (NumberFormatException ignored) {}
}
}
// ========== File Name Formatting ==========
@Override
public String getHostFileName(String string, int n) {
if (string == null) return n == VM_CMS ? "NONE NONE" : "";
String trimmed = string.trim();
if (trimmed.isEmpty()) {
return n == VM_CMS ? "NONE NONE" : "";
}
String result = "";
StringTokenizer st = new StringTokenizer(trimmed, ". \t");
switch (n) {
case VM_CMS: { // 0
String fn = st.hasMoreTokens() ? st.nextToken() : "";
String ft = st.hasMoreTokens() ? st.nextToken() : "";
if (fn.length() > 8) fn = fn.substring(0, 8);
if (ft.length() > 8) ft = ft.substring(0, 8);
if (fn.isEmpty()) fn = "NONE";
if (ft.isEmpty()) ft = "NONE";
result = fn + " " + ft;
break;
}
case MVS_TSO: { // 1
StringBuilder sb = new StringBuilder();
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (token.length() > 8) token = token.substring(0, 8);
if (sb.length() > 0) sb.append(".");
sb.append(token);
}
result = sb.toString();
break;
}
case CICS: { // 2
String token = st.nextToken();
result = token.length() > 8 ? token.substring(0, 8) : token;
break;
}
default:
result = string.trim();
break;
}
return result;
}
@Override
public String getLocalFileName(String string, int n) {
if (string == null || string.trim().isEmpty()) return "";
String trimmed = string.trim();
String result = "";
switch (n) {
case VM_CMS: { // 0
StringTokenizer st = new StringTokenizer(trimmed, " ");
String fn = st.hasMoreTokens() ? st.nextToken() : "NONE";
String ft = st.hasMoreTokens() ? st.nextToken() : "NONE";
result = fn + "." + ft;
break;
}
case MVS_TSO: { // 1
if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length() >= 2) {
result = trimmed.substring(1, trimmed.length() - 1);
} else {
result = trimmed;
}
break;
}
default:
result = trimmed;
break;
}
return result;
}
// ========== State Inspection ==========
public String stateIs(int n) {
switch (n) {
case 0: return "Idle";
case 1: return "W4DirectoryList";
case 2: return "W4GetFile";
case 3: return "W4PutFile";
case 12: return "W4Data";
case 19: return "UploadComplete";
case 20: return "DnloadComplete";
case 21: return "Quiting";
default: return "Unknown (" + n + ")";
}
}
public boolean isTransferActive() {
return delegateXfer != null && delegateXfer.isTransferActive();
}
public long getBytesTransferred() {
if (delegateXfer != null) return delegateXfer.getBytesTransferred();
return bytesTransferred;
}
// ========== Buffer / MTU Management ==========
public void SetMTUSize(int n) {
if (n < MIN_MTU_SIZE) n = MIN_MTU_SIZE;
if (n > MAX_MTU_SIZE) n = MAX_MTU_SIZE;
this.bmtuSize = n;
if (delegateXfer != null) {
delegateXfer.setMTUSize(this.bmtuSize);
}
}
public int GetMTUSize() {
if (delegateXfer != null) return delegateXfer.getMTUSize();
return bmtuSize;
}
public void setMTUSize(int n) { SetMTUSize(n); }
public int getMTUSize() { return GetMTUSize(); }
public void setClear(boolean bl) {
this.bSendClear = bl;
}
public void resendInboundDataBufferToHost() {
if (delegateXfer != null) {
delegateXfer.resendInboundDataBufferToHost();
}
}
// ========== File Transfer Operations ==========
@Override
public void getFile(String hostFile, int hostType, String localFile, int transferMode,
FileTransferStatusInterface status, String options) throws ECLErr {
setOptionFlags(options);
if (transferMode == 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(hostType == VM_CMS ? FTConfig.HostType.CMS :
(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.setDftBufferSize(bmtuSize);
if (options != null && !options.isEmpty()) {
config.parseOptions(options);
}
executeTransfer(config, status, localFile, hostFile);
}
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);
}
@Override
public void putFile(String localFile, int hostType, String hostFile, int transferMode,
FileTransferStatusInterface status, String options) throws ECLErr {
setOptionFlags(options);
if (transferMode == 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(hostType == VM_CMS ? FTConfig.HostType.CMS :
(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.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, status, localFile, hostFile);
}
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);
}
private void executeTransfer(FTConfig config, FileTransferStatusInterface status,
String locFile, String hstFile) throws ECLErr {
if (delegateXfer == null) {
throw new ECLErr("Xfer3270", "ECL0148", "Session or ECLXfer delegate not initialized");
}
if (status != null) {
java.io.File f = new java.io.File(locFile);
status.setFileInfo(locFile, f.exists() ? f.length() : -1L);
status.startTransfer();
}
ECLXferListener transferListener = new ECLXferListener() {
@Override
public void xferEvent(ECLXferEvent event) {
if (status == null) return;
long bytes = event.getBytesTransferred();
bytesTransferred = bytes;
status.bytesTransfered(bytes);
long total = event.getTotalBytes();
int pct = (total > 0) ? (int) ((bytes * 100) / total) : -1;
status.onProgress(bytes, total, pct);
if (event.isCompleted()) {
status.transferComplete();
} else if (event.isAborted()) {
sReasonTerminated = event.getMessage();
}
}
};
delegateXfer.addXferListener(transferListener);
try {
int rc;
if (config.isReceive()) {
rc = delegateXfer.ReceiveFile(config.getLocalFilename(), config.getHostFilename(), config.buildCommand());
} else {
rc = delegateXfer.SendFile(config.getLocalFilename(), config.getHostFilename(), config.buildCommand());
}
if (rc != 0) {
throw new ECLErr("Xfer3270", "ECL0150", "Transfer initiation failed with code " + rc);
}
} finally {
delegateXfer.removeXferListener(transferListener);
}
}
private FileTransferStatusInterface createStatusAdapter(ECLXferListener listener, String loc, String hst) {
if (listener == null) return null;
return new FileTransferStatusInterface() {
@Override
public void setFileInfo(String name, long size) {
listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_STARTED, 0, size, 0, "Transfer initiated", loc, hst));
}
@Override
public void startTransfer() {
listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_STARTED, 0, 0, 0, "Transfer started", loc, hst));
}
@Override
public void bytesTransfered(long bytes) {
listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_PROGRESS, bytes, 0, 0, bytes + " bytes transferred", loc, hst));
}
@Override
public void transferComplete() {
listener.xferEvent(new ECLXferEvent(Xfer3270.this, ECLXferEvent.XFER_COMPLETED, bytesTransferred, 0, 0, "Transfer complete", loc, hst));
}
};
}
// ========== Directory Query Operations ==========
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Vector getFiles(String hostQuery, int hostType, int timeout,
Vector localFiles, Vector hostFiles,
FileTransferHostDirectoryInterface callback) throws ECLErr {
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_PROCESSING);
Vector result = new Vector();
try {
if (hostType == VM_CMS) {
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING);
List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(hostQuery);
for (CMSDirectoryEntry entry : cmsEntries) {
String hName = entry.getFilename() + " " + entry.getFiletype();
String lName = entry.getFilename() + "." + entry.getFiletype();
if (hostFiles != null) hostFiles.addElement(hName);
if (localFiles != null) localFiles.addElement(lName);
result.addElement(entry);
}
if (callback != null) {
callback.onDirectoryLoaded(cmsEntries);
callback.setStatus(cmsEntries.isEmpty() ?
FileTransferHostDirectoryInterface.STATUS_EMPTY :
FileTransferHostDirectoryInterface.STATUS_OK);
}
} else {
if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING);
List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(hostQuery);
for (TSODirectoryEntry entry : tsoEntries) {
String name = entry.getName();
if (hostFiles != null) hostFiles.addElement(name);
if (localFiles != null) localFiles.addElement(name);
result.addElement(entry);
}
if (callback != null) {
callback.onDirectoryLoaded(tsoEntries);
callback.setStatus(tsoEntries.isEmpty() ?
FileTransferHostDirectoryInterface.STATUS_EMPTY :
FileTransferHostDirectoryInterface.STATUS_OK);
}
}
} catch (Exception e) {
if (callback != null) {
callback.onDirectoryError(e.getMessage());
callback.setStatus(FileTransferHostDirectoryInterface.STATUS_EMPTY);
}
throw new ECLErr("Xfer3270", "ECL0149", "Directory retrieval failed: " + e.getMessage());
}
return result;
}
@Override
public void cancelTransfer() {
this.bCancelRequested = true;
if (delegateXfer != null) {
delegateXfer.cancelTransfer();
}
}
@Override
public void setCodePage(CodePage codePage) {
this.currentCodePage = codePage;
if (delegateXfer != null && codePage != null) {
if (translator != null) {
translator.setCodePage(codePage.getCodePageId());
}
}
}
@Override
public void setCodePage(String codePageName) {
CodePage cp = CodePageRegistry.get(codePageName);
if (cp != null) {
setCodePage(cp);
} else if (translator != null) {
translator.setCodePage(codePageName);
}
}
@Override
public String getErrorMessage(Exception ex) {
if (ex != null && ex.getMessage() != null && !ex.getMessage().isEmpty()) {
return ex.getMessage();
}
return sReasonTerminated.isEmpty() ? "Unknown file transfer error" : sReasonTerminated;
}
public ECLXfer getDelegateXfer() {
return delegateXfer;
}
}