IND$FILE receiving

This commit is contained in:
2026-04-21 15:35:03 -04:00
parent bda8ea9530
commit 4ac3c6fa69
6 changed files with 2134 additions and 0 deletions
@@ -0,0 +1,236 @@
package org.pubvm.j3270.ft;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.ft.FTConfig;
import org.lib3270j.ft.FTConstants.FTState;
import org.lib3270j.ft.FTCut;
import org.lib3270j.ft.FTDft;
import java.io.File;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
/**
* High-level coordinator for IND$FILE file transfers.
* Manages the transfer state machine, timeouts, and coordinates between
* the GUI, Telnet3270Client, and the lower-level CUT/DFT protocol handlers.
*/
public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private static final Logger log = Logger.getLogger(FileTransfer.class.getName());
public interface FileTransferCallback {
void onTransferStarted();
void onTransferRunning();
void onBytesTransferred(long bytes);
void onTransferComplete(String message);
void onTransferAborted(String error);
}
private final Telnet3270Client client;
private final FileTransferCallback callback;
private FTConfig currentConfig;
private File localFile;
private FTState state = FTState.NONE;
private FTCut cutHandler;
private FTDft dftHandler;
private Timer timeoutTimer;
private static final long START_TIMEOUT_MS = 30000; // 30 seconds
public FileTransfer(Telnet3270Client client, FileTransferCallback callback) {
this.client = client;
this.callback = callback;
}
/**
* Start a new file transfer with the given configuration.
* @return null if started successfully, error message otherwise.
*/
public String startTransfer(FTConfig config) {
if (state != FTState.NONE) {
return "A transfer is already in progress.";
}
String validationError = config.validate();
if (validationError != null) {
return validationError;
}
this.currentConfig = config;
this.localFile = new File(config.getLocalFilename());
// Check overwrite
if (config.isReceive() && !config.isAppend() && !config.isOverwrite()) {
if (localFile.exists()) {
return "Local file already exists and overwrite is not permitted.";
}
}
// Initialize protocol handlers lazily
if (cutHandler == null) {
cutHandler = new FTCut(client.getScreenBuffer(), client.getInputProcessor(),
client.getTranslator(), this);
}
if (dftHandler == null) {
dftHandler = new FTDft(client.getInputProcessor(),
client.getTranslator(), this);
client.getDataStreamProcessor().setFTDft(dftHandler);
}
try {
cutHandler.initTransfer(localFile);
dftHandler.initTransfer(localFile);
} catch (IOException e) {
cleanupHandlers(false);
return "Failed to open local file: " + e.getMessage();
}
// Build and type the IND$FILE command
String command = config.buildCommand();
log.info("Starting IND$FILE transfer with command: " + command);
// Erase field and verify it can hold the command
int capacity = client.getInputProcessor().kybdPrime();
if (capacity < 0) {
cleanupHandlers(false);
switch (capacity) {
case -1: return "Keyboard is locked.";
case -3: return "No unprotected input field found.";
default: return "Cannot start transfer from current screen state.";
}
}
if (capacity < command.length()) {
cleanupHandlers(false);
return "Current input field is too small for the IND$FILE command (" + capacity + " chars max).";
}
setState(FTState.AWAIT_ACK);
client.emulateInput(command + "\n");
startTimeout();
callback.onTransferStarted();
return null;
}
public void cancel() {
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
log.info("User cancelled transfer");
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance
} else if (state != FTState.NONE) {
log.info("Forcing cancel from state " + state);
completeTransfer("Transfer cancelled.");
callback.onTransferAborted("Cancelled by user.");
}
}
/** Must be called after every screen update to drive CUT mode. */
public void onScreenUpdated() {
if (state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT) {
cutHandler.processScreenUpdate();
}
}
private void startTimeout() {
cancelTimeout();
timeoutTimer = new Timer("FTTimeout", true);
timeoutTimer.schedule(new TimerTask() {
@Override
public void run() {
SwingUtilities.invokeLater(() -> {
if (state == FTState.AWAIT_ACK) {
log.warning("Transfer start timeout");
completeTransfer("Transfer failed to start within 30 seconds.");
callback.onTransferAborted("Transfer start timeout.");
}
});
}
}, START_TIMEOUT_MS);
}
private void cancelTimeout() {
if (timeoutTimer != null) {
timeoutTimer.cancel();
timeoutTimer = null;
}
}
private void cleanupHandlers(boolean success) {
if (cutHandler != null) cutHandler.cleanup();
if (dftHandler != null) dftHandler.cleanup();
// Remove incomplete files if we were receiving and not appending
if (!success && currentConfig != null && currentConfig.isReceive() && !currentConfig.isAppend()) {
if (state != FTState.NONE && state != FTState.AWAIT_ACK && localFile != null && localFile.exists()) {
log.info("Cleaning up incomplete download: " + localFile.getAbsolutePath());
localFile.delete();
}
}
}
private void completeTransfer(String errorMessage) {
cancelTimeout();
boolean success = (errorMessage == null);
cleanupHandlers(success);
setState(FTState.NONE);
currentConfig = null;
}
// ========== FTCutListener / FTDftListener Callbacks ==========
@Override
public void onTransferRunning() {
cancelTimeout();
setState(FTState.RUNNING);
SwingUtilities.invokeLater(callback::onTransferRunning);
}
@Override
public void onTransferComplete(String errorMessage) {
completeTransfer(null);
SwingUtilities.invokeLater(() -> {
if (errorMessage == null) {
callback.onTransferComplete("Transfer complete.");
} else {
callback.onTransferAborted(errorMessage);
}
});
}
@Override
public void onTransferAborted(String errorMessage) {
completeTransfer(errorMessage);
SwingUtilities.invokeLater(() -> callback.onTransferAborted(errorMessage));
}
@Override
public void onBytesTransferred(long bytes) {
SwingUtilities.invokeLater(() -> callback.onBytesTransferred(bytes));
}
@Override
public FTState getCurrentState() {
return state;
}
@Override
public void setState(FTState state) {
this.state = state;
}
@Override
public FTConfig getConfig() {
return currentConfig;
}
@Override
public File getLocalFile() {
return localFile;
}
}
@@ -0,0 +1,305 @@
package org.pubvm.j3270.ft;
import org.lib3270j.ft.FTConfig;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.io.File;
public class FileTransferDialog extends JDialog {
private final FileTransfer coordinator;
private final Frame owner;
private JComboBox<FTConfig.HostType> hostTypeCombo;
private JRadioButton sendRadio;
private JRadioButton receiveRadio;
private JTextField localFileField;
private JButton browseLocalButton;
private JTextField hostFileField;
// Options
private JRadioButton asciiRadio;
private JRadioButton binaryRadio;
private JCheckBox crCheck;
private JCheckBox remapCheck;
private JCheckBox appendCheck;
private JCheckBox overwriteCheck;
// TSO specific
private JTextField recfmField;
private JTextField lreclField;
private JTextField blksizeField;
private JTextField spaceField;
// VM specific
private JTextField optionsField;
private JButton transferButton;
private JButton cancelButton;
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
super(owner, "File Transfer (IND$FILE)", false); // non-modal to see progress
this.owner = owner;
this.coordinator = coordinator;
buildUI();
pack();
setLocationRelativeTo(owner);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(new Color(25, 25, 25));
// Create form panel
JPanel formPanel = new JPanel();
formPanel.setLayout(new BoxLayout(formPanel, BoxLayout.Y_AXIS));
formPanel.setOpaque(false);
// Host Type
hostTypeCombo = new JComboBox<>(FTConfig.HostType.values());
formPanel.add(createRow("Host Environment:", hostTypeCombo));
// Direction
sendRadio = new JRadioButton("Send to Host");
receiveRadio = new JRadioButton("Receive from Host");
receiveRadio.setSelected(true);
ButtonGroup dirGroup = new ButtonGroup();
dirGroup.add(sendRadio);
dirGroup.add(receiveRadio);
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false);
dirPanel.add(receiveRadio);
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
dirPanel.add(sendRadio);
// Disable append/overwrite when sending
sendRadio.addActionListener(e -> updateOptionStates());
receiveRadio.addActionListener(e -> updateOptionStates());
formPanel.add(createRow("Direction:", dirPanel));
// Local File
localFileField = new JTextField(20);
browseLocalButton = new JButton("Browse...");
browseLocalButton.addActionListener(e -> browseLocalFile());
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
localPanel.setOpaque(false);
localPanel.add(localFileField, BorderLayout.CENTER);
localPanel.add(browseLocalButton, BorderLayout.EAST);
formPanel.add(createRow("Local File:", localPanel));
// Host File
hostFileField = new JTextField(20);
formPanel.add(createRow("Host File:", hostFileField));
// Mode
asciiRadio = new JRadioButton("ASCII");
asciiRadio.setSelected(true);
binaryRadio = new JRadioButton("Binary");
ButtonGroup modeGroup = new ButtonGroup();
modeGroup.add(asciiRadio);
modeGroup.add(binaryRadio);
JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
modePanel.setOpaque(false);
modePanel.add(asciiRadio);
modePanel.add(Box.createRigidArea(new Dimension(15, 0)));
modePanel.add(binaryRadio);
asciiRadio.addActionListener(e -> updateOptionStates());
binaryRadio.addActionListener(e -> updateOptionStates());
formPanel.add(createRow("Transfer Mode:", modePanel));
// Options (checkboxes)
crCheck = new JCheckBox("Add/Remove CR");
crCheck.setSelected(true);
remapCheck = new JCheckBox("Remap Character Set");
remapCheck.setSelected(true);
appendCheck = new JCheckBox("Append to file");
overwriteCheck = new JCheckBox("Overwrite existing");
JPanel optionsPanel1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel1.setOpaque(false);
optionsPanel1.add(crCheck);
optionsPanel1.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel1.add(remapCheck);
JPanel optionsPanel2 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel2.setOpaque(false);
optionsPanel2.add(appendCheck);
optionsPanel2.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel2.add(overwriteCheck);
formPanel.add(createRow("Text Options:", optionsPanel1));
formPanel.add(createRow("File Options:", optionsPanel2));
// Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"TSO Allocation Options (Send Only)"));
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
hostOptsPanel.add(new JLabel("RECFM:"));
recfmField = new JTextField(5);
hostOptsPanel.add(recfmField);
hostOptsPanel.add(new JLabel("LRECL:"));
lreclField = new JTextField(5);
hostOptsPanel.add(lreclField);
hostOptsPanel.add(new JLabel("BLKSIZE:"));
blksizeField = new JTextField(5);
hostOptsPanel.add(blksizeField);
hostOptsPanel.add(new JLabel("SPACE:"));
spaceField = new JTextField(15);
hostOptsPanel.add(spaceField);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(hostOptsPanel);
// CMS options
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
vmOptsPanel.setOpaque(false);
vmOptsPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"CMS Options"));
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
optionsField = new JTextField(20);
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
vmOptsPanel.add(optionsField, BorderLayout.CENTER);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(vmOptsPanel);
mainPanel.add(formPanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setOpaque(false);
transferButton = new JButton("Start Transfer");
transferButton.addActionListener(e -> startTransfer());
cancelButton = new JButton("Close");
cancelButton.addActionListener(e -> dispose());
buttonPanel.add(cancelButton);
buttonPanel.add(transferButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
// Theme styling for components
applyTheme(mainPanel);
// Initialize state
updateOptionStates();
}
private void updateOptionStates() {
boolean isSend = sendRadio.isSelected();
boolean isAscii = asciiRadio.isSelected();
appendCheck.setEnabled(!isSend);
overwriteCheck.setEnabled(!isSend);
crCheck.setEnabled(isAscii);
remapCheck.setEnabled(isAscii);
recfmField.setEnabled(isSend);
lreclField.setEnabled(isSend);
blksizeField.setEnabled(isSend);
spaceField.setEnabled(isSend);
}
private JPanel createRow(String labelText, Component comp) {
JPanel row = new JPanel(new BorderLayout(10, 0));
row.setOpaque(false);
JLabel label = new JLabel(labelText);
label.setPreferredSize(new Dimension(130, 25));
row.add(label, BorderLayout.WEST);
row.add(comp, BorderLayout.CENTER);
row.setBorder(new EmptyBorder(0, 0, 5, 0));
return row;
}
private void browseLocalFile() {
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
// Auto-fill host file if empty
if (hostFileField.getText().trim().isEmpty()) {
hostFileField.setText(chooser.getSelectedFile().getName());
}
}
}
private void startTransfer() {
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
"Error", JOptionPane.ERROR_MESSAGE);
return;
}
FTConfig config = new FTConfig();
config.setHostType((FTConfig.HostType) hostTypeCombo.getSelectedItem());
config.setLocalFilename(localFileField.getText().trim());
config.setHostFilename(hostFileField.getText().trim());
config.setReceive(receiveRadio.isSelected());
config.setAscii(asciiRadio.isSelected());
config.setCrFlag(crCheck.isSelected());
config.setRemapFlag(remapCheck.isSelected());
config.setAppend(appendCheck.isSelected());
config.setOverwrite(overwriteCheck.isSelected());
config.setRecfm(recfmField.getText().trim());
config.setLrecl(lreclField.getText().trim());
config.setBlksize(blksizeField.getText().trim());
config.setSpace(spaceField.getText().trim());
config.setOptions(optionsField.getText().trim());
String error = coordinator.startTransfer(config);
if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
} else {
// Success, close dialog (progress will be shown separately)
dispose();
}
}
private void applyTheme(Container container) {
Color fg = new Color(200, 200, 200);
Color bg = new Color(40, 40, 40);
for (Component c : container.getComponents()) {
c.setForeground(fg);
if (c instanceof JTextField || c instanceof JComboBox) {
c.setBackground(bg);
if (c instanceof JTextField) {
((JTextField) c).setCaretColor(fg);
((JTextField) c).setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 5, 2, 5)));
}
} else if (c instanceof JButton) {
c.setBackground(new Color(50, 50, 50));
c.setFocusable(false);
} else if (c instanceof JRadioButton || c instanceof JCheckBox) {
((JComponent) c).setOpaque(false);
c.setFocusable(false);
}
if (c instanceof Container) {
applyTheme((Container) c);
}
}
}
}