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