diff --git a/bugs.txt b/bugs.txt index 8b13789..e69de29 100644 --- a/bugs.txt +++ b/bugs.txt @@ -1 +0,0 @@ - diff --git a/build/j3270/org/pubvm/j3270/J3270App$1.class b/build/j3270/org/pubvm/j3270/J3270App$1.class index e8a51ce..8ca98af 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App$1.class and b/build/j3270/org/pubvm/j3270/J3270App$1.class differ diff --git a/build/j3270/org/pubvm/j3270/J3270App$2.class b/build/j3270/org/pubvm/j3270/J3270App$2.class index de1d926..5113d3f 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App$2.class and b/build/j3270/org/pubvm/j3270/J3270App$2.class differ diff --git a/build/j3270/org/pubvm/j3270/J3270App.class b/build/j3270/org/pubvm/j3270/J3270App.class index 3f7ee53..fe40d61 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App.class and b/build/j3270/org/pubvm/j3270/J3270App.class differ diff --git a/build/lib3270j/org/lib3270j/Telnet3270Client.class b/build/lib3270j/org/lib3270j/Telnet3270Client.class index 9525283..27c9753 100644 Binary files a/build/lib3270j/org/lib3270j/Telnet3270Client.class and b/build/lib3270j/org/lib3270j/Telnet3270Client.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class index 95f3fc0..725d46d 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class and b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class index b5fbdce..00aab4e 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class and b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class differ diff --git a/build/lib3270j/org/lib3270j/input/InputProcessor.class b/build/lib3270j/org/lib3270j/input/InputProcessor.class index 68143e5..0158a88 100644 Binary files a/build/lib3270j/org/lib3270j/input/InputProcessor.class and b/build/lib3270j/org/lib3270j/input/InputProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class b/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class index f531834..2dbf12c 100644 Binary files a/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class and b/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class differ diff --git a/j3270/src/main/java/org/pubvm/j3270/J3270App.java b/j3270/src/main/java/org/pubvm/j3270/J3270App.java index 84f255a..d9e3345 100644 --- a/j3270/src/main/java/org/pubvm/j3270/J3270App.java +++ b/j3270/src/main/java/org/pubvm/j3270/J3270App.java @@ -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(); }); diff --git a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java index 0d1dc31..06d879d 100644 --- a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java +++ b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java @@ -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); diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java index eac671a..b2ecec3 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -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; diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java index 4498b93..74c63b9 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java @@ -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 diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java index 99d9ea6..0d13427 100644 --- a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -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; + } } + diff --git a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java index e1cec28..658a14f 100644 --- a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java @@ -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; + } }