IND$FILE receiving
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,8 @@ import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import org.pubvm.j3270.ui.ConnectDialog;
|
||||
import org.pubvm.j3270.ui.StatusBar;
|
||||
import org.pubvm.j3270.ui.TerminalPanel;
|
||||
import org.pubvm.j3270.ft.FileTransfer;
|
||||
import org.pubvm.j3270.ft.FileTransferDialog;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
@@ -31,6 +33,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
private String lastHost = "";
|
||||
private int lastPort = 23;
|
||||
|
||||
private FileTransfer fileTransfer;
|
||||
|
||||
public J3270App() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
@@ -125,6 +129,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
client.reset();
|
||||
terminalPanel.repaint();
|
||||
}));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
// Help menu
|
||||
@@ -154,6 +160,69 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
return item;
|
||||
}
|
||||
|
||||
private void showFileTransferDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
||||
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileTransfer == null) {
|
||||
fileTransfer = new FileTransfer(client, new FileTransfer.FileTransferCallback() {
|
||||
private ProgressMonitor monitor;
|
||||
|
||||
@Override
|
||||
public void onTransferStarted() {
|
||||
monitor = new ProgressMonitor(J3270App.this, "Starting IND$FILE transfer...",
|
||||
"Waiting for host...", 0, 100);
|
||||
monitor.setMillisToDecideToPopup(0);
|
||||
monitor.setMillisToPopup(0);
|
||||
|
||||
// Start a timer to check for manual cancellation
|
||||
Timer cancelCheckTimer = new Timer(500, e -> {
|
||||
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
((Timer)e.getSource()).stop();
|
||||
}
|
||||
});
|
||||
cancelCheckTimer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferRunning() {
|
||||
if (monitor != null) {
|
||||
monitor.setNote("Transferring data...");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBytesTransferred(long bytes) {
|
||||
if (monitor != null) {
|
||||
monitor.setNote(bytes + " bytes transferred");
|
||||
// We don't always know max size in IND$FILE, so we just pulse/update note
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferComplete(String message) {
|
||||
if (monitor != null) monitor.close();
|
||||
monitor = null;
|
||||
JOptionPane.showMessageDialog(J3270App.this, message, "Transfer Complete", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferAborted(String error) {
|
||||
if (monitor != null) monitor.close();
|
||||
monitor = null;
|
||||
JOptionPane.showMessageDialog(J3270App.this, "Transfer Failed: " + error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FileTransferDialog dialog = new FileTransferDialog(this, fileTransfer);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
// ========== Connection management ==========
|
||||
|
||||
private void showConnectDialog() {
|
||||
@@ -221,6 +290,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
private void disconnect() {
|
||||
if (client != null) {
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
client.disconnect();
|
||||
client = null;
|
||||
terminalPanel.setClient(null);
|
||||
@@ -280,6 +353,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
if (client != null) {
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.onScreenUpdated();
|
||||
}
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
|
||||
@@ -115,6 +115,9 @@ public class Telnet3270Client {
|
||||
/** Get the input processor for keyboard operations. */
|
||||
public InputProcessor getInputProcessor() { return inputProcessor; }
|
||||
|
||||
/** Get the data stream processor. */
|
||||
public DataStreamProcessor getDataStreamProcessor() { return dsProcessor; }
|
||||
|
||||
/** Get the connection config. */
|
||||
public ConnectionConfig getConfig() { return config; }
|
||||
|
||||
@@ -130,6 +133,11 @@ public class Telnet3270Client {
|
||||
}
|
||||
}
|
||||
|
||||
/** Emulate input of a string, pressing Enter on newlines. */
|
||||
public void emulateInput(String s) {
|
||||
inputProcessor.emulateInput(s);
|
||||
}
|
||||
|
||||
/** Send Enter key. */
|
||||
public void sendEnter() {
|
||||
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
|
||||
@@ -30,6 +30,9 @@ public class DataStreamProcessor {
|
||||
|
||||
// Callback for sending data back to host
|
||||
private OutputSender outputSender;
|
||||
|
||||
// File Transfer DFT handler
|
||||
private org.lib3270j.ft.FTDft ftDft;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
@@ -48,6 +51,10 @@ public class DataStreamProcessor {
|
||||
public void setOutputSender(OutputSender sender) {
|
||||
this.outputSender = sender;
|
||||
}
|
||||
|
||||
public void setFTDft(org.lib3270j.ft.FTDft ftDft) {
|
||||
this.ftDft = ftDft;
|
||||
}
|
||||
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.add(l);
|
||||
@@ -698,6 +705,13 @@ public class DataStreamProcessor {
|
||||
processRecord(data, pos + 4, fieldLen - 4, false);
|
||||
}
|
||||
break;
|
||||
case SF_TRANSFER_DATA:
|
||||
if (ftDft != null) {
|
||||
ftDft.processStructuredField(data, pos, fieldLen);
|
||||
} else {
|
||||
log.fine("SF_TRANSFER_DATA received but ftDft is null");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
|
||||
break;
|
||||
|
||||
@@ -31,6 +31,7 @@ public class QueryReplyBuilder {
|
||||
QR_COLOR, // 0x86
|
||||
QR_HIGHLIGHTING, // 0x87
|
||||
QR_REPLY_MODES, // 0x88
|
||||
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
|
||||
QR_IMP_PART, // 0xa6
|
||||
};
|
||||
|
||||
@@ -68,6 +69,10 @@ public class QueryReplyBuilder {
|
||||
// Reply Modes
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
|
||||
// Distributed Data Management (DFT File Transfer)
|
||||
// Hardcoded limit of 4096 is standard; the client can configure it during transfer.
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
|
||||
// Implicit Partition
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
@@ -189,6 +194,19 @@ public class QueryReplyBuilder {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
}
|
||||
|
||||
private byte[] buildDdm(int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x00); // reserved
|
||||
out.write((bufferSize >> 8) & 0xFF); // inbound length limit (INLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write((bufferSize >> 8) & 0xFF); // outbound length limit (OUTLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write(0x01); // NSS = 01
|
||||
out.write(0x01); // DDMSS = 01
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||
// Implicit partition sizes, 2 self-defining parameters
|
||||
|
||||
@@ -319,4 +319,113 @@ public class InputProcessor {
|
||||
keyboardLocked = false;
|
||||
insertMode = false;
|
||||
}
|
||||
|
||||
// ========== File Transfer Support ==========
|
||||
|
||||
/**
|
||||
* Send an AID code, bypassing the keyboard lock.
|
||||
* Used internally by the file transfer protocol to send Enter/PF keys
|
||||
* during a transfer when the keyboard is normally locked.
|
||||
*/
|
||||
public void sendAidForFT(int aidCode) {
|
||||
boolean wasLocked = keyboardLocked;
|
||||
keyboardLocked = false;
|
||||
sendAid(aidCode);
|
||||
keyboardLocked = wasLocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type a string into the current cursor position and press Enter.
|
||||
* Used by IND$FILE to inject the transfer command.
|
||||
* Equivalent to emulate_input() in x3270.
|
||||
*/
|
||||
public void emulateInput(String text) {
|
||||
// Type each character
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char ch = text.charAt(i);
|
||||
if (ch == '\n') {
|
||||
// Newline means press Enter
|
||||
sendAid(AID_ENTER);
|
||||
return;
|
||||
}
|
||||
typeCharacter(ch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase the current field and return its capacity.
|
||||
* Used by IND$FILE to prepare the field for the transfer command.
|
||||
* Equivalent to kybd_prime() in x3270.
|
||||
*
|
||||
* Returns:
|
||||
* - The number of characters that can fit in the field, or
|
||||
* - -1 if the keyboard is locked
|
||||
* - -2 if not in 3270 mode
|
||||
* - -3 if no input field found
|
||||
*/
|
||||
public int kybdPrime() {
|
||||
if (keyboardLocked) return -1;
|
||||
|
||||
if (!screen.isFormatted()) {
|
||||
// Unformatted screen — use entire buffer
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(0);
|
||||
for (int i = 0; i < size; i++) {
|
||||
screen.getCell(i).ec = 0;
|
||||
screen.getCell(i).ucs4 = 0;
|
||||
}
|
||||
screen.markAllChanged();
|
||||
return size;
|
||||
}
|
||||
|
||||
// Find the field at cursor and erase it
|
||||
int curAddr = screen.getCursorAddress();
|
||||
byte faVal = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
// Try to find first unprotected field
|
||||
curAddr = screen.findNextUnprotected(curAddr);
|
||||
faVal = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
return -3; // No unprotected field
|
||||
}
|
||||
}
|
||||
|
||||
// Move to start of field
|
||||
int faAddr = screen.findFieldAttribute(curAddr);
|
||||
int fieldStart = screen.incrementAddress(faAddr);
|
||||
screen.setCursorAddress(fieldStart);
|
||||
|
||||
// Count field length and erase
|
||||
int fieldLen = 0;
|
||||
int pos = fieldStart;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
screen.getCell(pos).ec = 0;
|
||||
screen.getCell(pos).ucs4 = 0;
|
||||
fieldLen++;
|
||||
pos = screen.incrementAddress(pos);
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
|
||||
// Set MDT
|
||||
if (faAddr >= 0) {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
return fieldLen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a structured field response directly.
|
||||
* Used by DFT mode file transfer.
|
||||
*/
|
||||
public void sendStructuredFieldData(byte[] data) {
|
||||
fsm.send3270Data(data);
|
||||
}
|
||||
|
||||
/** Get a reference to the TelnetFSM for direct SF operations. */
|
||||
public TelnetFSM getTelnetFSM() {
|
||||
return fsm;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -326,4 +326,59 @@ public class ScreenBuffer {
|
||||
public ExtendedAttribute getDefaultFieldAttribute() {
|
||||
return defaultFA;
|
||||
}
|
||||
|
||||
// ========== Low-level cell access for file transfer (CUT mode) ==========
|
||||
|
||||
/**
|
||||
* Write an EBCDIC byte to a cell at the given buffer address.
|
||||
* Used by CUT mode file transfer to place data into the screen buffer.
|
||||
*/
|
||||
public void setCell(int baddr, int ebcdicByte) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return;
|
||||
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
|
||||
buffer[baddr].ucs4 = 0; // will be set on display refresh
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an EBCDIC byte with extended attribute flags (cs=0).
|
||||
* Equivalent to ctlr_add() in x3270.
|
||||
*/
|
||||
public void setCellWithCS(int baddr, int ebcdicByte, int cs) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return;
|
||||
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
|
||||
buffer[baddr].cs = (byte) (cs & 0xFF);
|
||||
buffer[baddr].ucs4 = 0;
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the field attribute byte at a given buffer address.
|
||||
* Used by CUT mode to hide data fields during transfer.
|
||||
*/
|
||||
public void setCellFA(int baddr, byte fa) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return;
|
||||
buffer[baddr].fa = fa;
|
||||
buffer[baddr].ic = 1; // mark as field attribute
|
||||
formatted = true;
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get just the EBCDIC code byte from a cell.
|
||||
* Convenience method for file transfer data extraction.
|
||||
*/
|
||||
public int getCellEC(int baddr) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
|
||||
return buffer[baddr].ec & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field attribute byte at a cell (the fa byte, not the ic field).
|
||||
* For CUT mode checking of field attribute modifications.
|
||||
*/
|
||||
public byte getCellFAByte(int baddr) {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
|
||||
return buffer[baddr].fa;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user