From 8286af19e6f9d0d650a87a4f234baa2bcbb48237 Mon Sep 17 00:00:00 2001 From: Rudi Date: Thu, 24 Sep 2026 01:28:55 +0000 Subject: [PATCH] IND$FILE Browse! --- .../java/haus/nightmare/j3270/J3270App.java | 41 ++- .../j3270/ft/FileTransferDialog.java | 33 +- .../j3270/ui/HostDirectoryDialog.java | 326 +++++++++++++++--- .../nightmare/j3270/ui/TerminalPanel.java | 11 +- .../ft/FileTransferSessionAndCmsTest.java | 41 +++ .../j3270/ui/TerminalPanelSelectionTest.java | 21 ++ .../haus/nightmare/lib3270j/ft/FTConfig.java | 71 +++- .../lib3270j/ft/dir/CMSDirectoryParser.java | 21 +- .../lib3270j/ft/dir/TSODirectoryParser.java | 23 +- .../lib3270j/xfer/FileTransferFileObject.java | 11 + .../nightmare/lib3270j/xfer3270/Xfer3270.java | 67 +++- .../nightmare/lib3270j/ft/FTConfigTest.java | 62 ++++ .../lib3270j/ft/dir/DirectoryParserTest.java | 30 ++ 13 files changed, 664 insertions(+), 94 deletions(-) diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index 0b8eee3..68a9112 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -78,7 +78,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (client != null) { statusBar.updateStatus(); syncModeMenuItems(); - if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) { + if (!isModalDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) { if (!terminalPanel.hasFocus()) { terminalPanel.requestFocusInWindow(); } @@ -87,6 +87,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }); refreshTimer.start(); + setAutoRequestFocus(true); + addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { @@ -104,8 +106,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate @Override public void windowActivated(WindowEvent e) { - if (!isAnyChildDialogActive()) { - terminalPanel.requestFocusInWindow(); + if (!isModalDialogActive() && terminalPanel != null) { + terminalPanel.requestFocus(); + } + } + }); + + addWindowFocusListener(new WindowAdapter() { + @Override + public void windowGainedFocus(WindowEvent e) { + if (!isModalDialogActive() && terminalPanel != null) { + terminalPanel.requestFocus(); } } }); @@ -117,16 +128,20 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate Object src = me.getSource(); if (src instanceof Component) { Window win = SwingUtilities.getWindowAncestor((Component) src); - if (win == J3270App.this && !isAnyChildDialogActive()) { + if (win == J3270App.this && !isModalDialogActive()) { if (!J3270App.this.isFocused() || !J3270App.this.isActive()) { J3270App.this.toFront(); - J3270App.this.requestFocus(); } boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu) || SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null || SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null; if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) { - terminalPanel.requestFocusInWindow(); + terminalPanel.requestFocus(); + SwingUtilities.invokeLater(() -> { + if (terminalPanel != null && !terminalPanel.hasFocus()) { + terminalPanel.requestFocus(); + } + }); } } } @@ -134,19 +149,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }, AWTEvent.MOUSE_EVENT_MASK); } - private boolean isAnyChildDialogActive() { - Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow(); - if (focusedWindow != null && focusedWindow != this) { - return true; - } - if (findDialog != null && findDialog.isVisible()) return true; - if (scriptDialog != null && scriptDialog.isVisible()) return true; - if (macroDialog != null && macroDialog.isVisible()) return true; - if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true; - if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true; - if (screenHistoryDialog != null && screenHistoryDialog.isVisible()) return true; + private boolean isModalDialogActive() { for (Window owned : getOwnedWindows()) { - if (owned != null && owned.isVisible()) { + if (owned instanceof Dialog && owned.isVisible() && ((Dialog) owned).isModal()) { return true; } } diff --git a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java index 8416355..589db47 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java @@ -28,6 +28,8 @@ public class FileTransferDialog extends JDialog { public String lrecl = ""; public String blksize = ""; public String space = ""; + public FTConfig.AllocationUnit units = FTConfig.AllocationUnit.TRACKS; + public String dirBlocks = ""; public String options = ""; } @@ -82,6 +84,8 @@ public class FileTransferDialog extends JDialog { private JTextField lreclField; private JTextField blksizeField; private JTextField spaceField; + private JComboBox unitsCombo; + private JTextField dirField; // VM specific private JTextField optionsField; @@ -220,7 +224,7 @@ public class FileTransferDialog extends JDialog { formPanel.add(createRow("File Options:", optionsPanel2)); // Host-specific options panel (TSO dataset allocation) - JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5)); + JPanel hostOptsPanel = new JPanel(new GridLayout(3, 4, 5, 5)); hostOptsPanel.setOpaque(false); hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)")); @@ -236,10 +240,24 @@ public class FileTransferDialog extends JDialog { blksizeField = new JTextField(5); hostOptsPanel.add(blksizeField); - hostOptsPanel.add(new JLabel("SPACE:")); + hostOptsPanel.add(new JLabel("Units:")); + unitsCombo = new JComboBox<>(new FTConfig.AllocationUnit[]{ + FTConfig.AllocationUnit.TRACKS, + FTConfig.AllocationUnit.CYLINDERS, + FTConfig.AllocationUnit.AVBLOCK, + FTConfig.AllocationUnit.DEFAULT + }); + unitsCombo.setSelectedItem(FTConfig.AllocationUnit.TRACKS); + hostOptsPanel.add(unitsCombo); + + hostOptsPanel.add(new JLabel("SPACE (Pri,Sec):")); spaceField = new JTextField(15); hostOptsPanel.add(spaceField); + hostOptsPanel.add(new JLabel("Directory (PDS):")); + dirField = new JTextField(5); + hostOptsPanel.add(dirField); + formPanel.add(Box.createRigidArea(new Dimension(0, 10))); formPanel.add(hostOptsPanel); @@ -306,7 +324,9 @@ public class FileTransferDialog extends JDialog { if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm); if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl); if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize); + if (lastTransferState.units != null) unitsCombo.setSelectedItem(lastTransferState.units); if (lastTransferState.space != null) spaceField.setText(lastTransferState.space); + if (lastTransferState.dirBlocks != null) dirField.setText(lastTransferState.dirBlocks); if (lastTransferState.options != null) optionsField.setText(lastTransferState.options); } } @@ -327,7 +347,9 @@ public class FileTransferDialog extends JDialog { recfmField.setEnabled(isSend && isTso); lreclField.setEnabled(isSend && isTso); blksizeField.setEnabled(isSend && isTso); + unitsCombo.setEnabled(isSend && isTso); spaceField.setEnabled(isSend && isTso); + dirField.setEnabled(isSend && isTso); optionsField.setEnabled(isCms); } @@ -410,7 +432,10 @@ public class FileTransferDialog extends JDialog { config.setRecfm(recfmField.getText().trim()); config.setLrecl(lreclField.getText().trim()); config.setBlksize(blksizeField.getText().trim()); + FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem(); + config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT); config.setSpace(spaceField.getText().trim()); + config.setDirectoryBlocks(dirField.getText().trim()); config.setOptions(optionsField.getText().trim()); recordSessionTransfer(); @@ -439,7 +464,9 @@ public class FileTransferDialog extends JDialog { state.recfm = recfmField.getText().trim(); state.lrecl = lreclField.getText().trim(); state.blksize = blksizeField.getText().trim(); + state.units = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem(); state.space = spaceField.getText().trim(); + state.dirBlocks = dirField.getText().trim(); state.options = optionsField.getText().trim(); lastTransferState = state; } @@ -459,7 +486,9 @@ public class FileTransferDialog extends JDialog { public JTextField getRecfmField() { return recfmField; } public JTextField getLreclField() { return lreclField; } public JTextField getBlksizeField() { return blksizeField; } + public JComboBox getUnitsCombo() { return unitsCombo; } public JTextField getSpaceField() { return spaceField; } + public JTextField getDirField() { return dirField; } public JTextField getOptionsField() { return optionsField; } private void applyTheme(Container container) { diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java index 852fb11..be94a64 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java @@ -3,15 +3,19 @@ package haus.nightmare.j3270.ui; import haus.nightmare.lib3270j.Telnet3270Client; import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry; +import haus.nightmare.lib3270j.ft.dir.CMSDirectoryParser; import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry; import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry; +import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser; import javax.swing.*; import javax.swing.border.EmptyBorder; -import javax.swing.border.LineBorder; import javax.swing.table.DefaultTableModel; import javax.swing.table.JTableHeader; import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; @@ -19,6 +23,7 @@ import java.util.List; /** * Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments. + * Aligned with IBM Host On-Demand presentation space queries and clipboard parsing. */ public class HostDirectoryDialog extends JDialog { @@ -27,6 +32,7 @@ public class HostDirectoryDialog extends JDialog { private JComboBox hostTypeCombo; private JTextField queryField; + private JButton queryHostBtn; private JTable table; private DefaultTableModel tableModel; private JLabel statusLabel; @@ -34,6 +40,7 @@ public class HostDirectoryDialog extends JDialog { private String selectedHostFile = null; private boolean confirmed = false; + private final List allEntries = new ArrayList<>(); private final List currentEntries = new ArrayList<>(); public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) { @@ -43,7 +50,7 @@ public class HostDirectoryDialog extends JDialog { AppIcon.applyTo(this); buildUI(initialQuery); - setSize(700, 420); + setSize(780, 460); setLocationRelativeTo(parent); } @@ -69,10 +76,18 @@ public class HostDirectoryDialog extends JDialog { if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS); else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO); ThemeManager.styleComboBox(hostTypeCombo); + hostTypeCombo.addActionListener(e -> { + setupTableColumns(); + if (client != null && client.getConnectionState().isFullSession()) { + queryHost(); + } else { + parseCurrentScreen(); + } + }); topPanel.add(hostTypeCombo, gbc); gbc.gridx = 2; - JLabel queryLabel = new JLabel("Query Pattern / Text:"); + JLabel queryLabel = new JLabel("Filter / Pattern:"); queryLabel.setFont(font); topPanel.add(queryLabel, gbc); @@ -81,15 +96,24 @@ public class HostDirectoryDialog extends JDialog { queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); queryField.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextField(queryField); + queryField.addActionListener(e -> onFilterOrParse()); topPanel.add(queryField, gbc); gbc.gridx = 4; gbc.weightx = 0; - JButton parseBtn = new JButton("Query / Parse"); - ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT); - parseBtn.setFont(font); - parseBtn.addActionListener(e -> runQuery()); - topPanel.add(parseBtn, gbc); + JButton filterBtn = new JButton("Filter / Parse"); + ThemeManager.styleButton(filterBtn, ThemeManager.ButtonVariant.DEFAULT); + filterBtn.setFont(font); + filterBtn.addActionListener(e -> onFilterOrParse()); + topPanel.add(filterBtn, gbc); + + gbc.gridx = 5; + queryHostBtn = new JButton("Query Host"); + ThemeManager.styleButton(queryHostBtn, ThemeManager.ButtonVariant.DEFAULT); + queryHostBtn.setFont(font); + queryHostBtn.setToolTipText("Send LISTCAT or LISTFILE query to active host session"); + queryHostBtn.addActionListener(e -> queryHost()); + topPanel.add(queryHostBtn, gbc); mainPanel.add(topPanel, BorderLayout.NORTH); @@ -129,11 +153,18 @@ public class HostDirectoryDialog extends JDialog { JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); btnPanel.setOpaque(false); - JButton pasteScreenBtn = new JButton("Parse Current Screen"); + JButton pasteScreenBtn = new JButton("Parse Screen"); ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT); pasteScreenBtn.setFont(font); + pasteScreenBtn.setToolTipText("Parse items currently visible on the 3270 presentation space"); pasteScreenBtn.addActionListener(e -> parseCurrentScreen()); + JButton pasteClipBtn = new JButton("Paste Clipboard"); + ThemeManager.styleButton(pasteClipBtn, ThemeManager.ButtonVariant.DEFAULT); + pasteClipBtn.setFont(font); + pasteClipBtn.setToolTipText("Parse host filenames copied to the system clipboard (HoD compatible)"); + pasteClipBtn.addActionListener(e -> parseClipboard()); + JButton selectBtn = new JButton("Select Dataset"); ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY); selectBtn.setFont(font); @@ -148,6 +179,7 @@ public class HostDirectoryDialog extends JDialog { }); btnPanel.add(pasteScreenBtn); + btnPanel.add(pasteClipBtn); btnPanel.add(Box.createHorizontalStrut(10)); btnPanel.add(selectBtn); btnPanel.add(cancelBtn); @@ -158,12 +190,18 @@ public class HostDirectoryDialog extends JDialog { setContentPane(mainPanel); ThemeManager.applyThemeToWindow(this); - // Initial setup + // Initial setup: columns and screen parse or host query setupTableColumns(); - if (initialQuery != null && !initialQuery.isEmpty()) { - runQuery(); + if (initialQuery != null && !initialQuery.trim().isEmpty()) { + queryField.setText(initialQuery.trim()); + } + if (client != null && client.getConnectionState().isFullSession()) { + queryHost(); } else { parseCurrentScreen(); + if (initialQuery != null && !initialQuery.trim().isEmpty()) { + applyFilter(); + } } } @@ -181,69 +219,241 @@ public class HostDirectoryDialog extends JDialog { } } - private void runQuery() { - setupTableColumns(); - currentEntries.clear(); + private void onFilterOrParse() { String query = queryField.getText().trim(); - FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); + // If text contains multiple lines, treat it as pasted raw text to parse + if (query.contains("\n") || query.contains("\r") || query.toUpperCase().contains("NONVSAM") + || query.toUpperCase().contains("IN-CAT") || query.toUpperCase().contains("FILENAME")) { + parseText(query, "query text"); + } else if (allEntries.isEmpty() && client != null && client.getConnectionState().isFullSession()) { + queryHost(); + } else { + applyFilter(); + } + } - if (client != null && client.getXfer() != null) { - if (hostType == FTConfig.HostType.CMS) { - List cmsEntries = client.getXfer().getCmsDirectory(query); - currentEntries.addAll(cmsEntries); - for (CMSDirectoryEntry entry : cmsEntries) { - tableModel.addRow(new Object[]{ - entry.getFilename(), entry.getFiletype(), entry.getFilemode(), - entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime() - }); - } - } else { - List tsoEntries = client.getXfer().getTsoDirectory(query); - currentEntries.addAll(tsoEntries); - for (TSODirectoryEntry entry : tsoEntries) { - tableModel.addRow(new Object[]{ - entry.getDatasetName(), entry.getVolume(), entry.getRecfm(), - entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents() - }); - } - } + private void queryHost() { + if (client == null || !client.getConnectionState().isFullSession()) { + statusLabel.setText("No active session to query."); + return; } - statusLabel.setText(currentEntries.size() + " entries found."); + String pattern = queryField.getText().trim(); + FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); + statusLabel.setText("Querying host (" + hostType + ")..."); + if (queryHostBtn != null) queryHostBtn.setEnabled(false); + + SwingWorker worker = new SwingWorker<>() { + @Override + protected String doInBackground() throws Exception { + if (client.getPS() == null) return ""; + + // 1. Clear screen and wait for host to unlock keyboard + client.getPS().SendKeys("[clear]"); + waitForKeyboardUnlock(client, 3000); + + // 2. Format query command + if (hostType == FTConfig.HostType.CMS) { + String cmsFilter; + if (pattern.isEmpty()) { + cmsFilter = "* * *"; + } else { + String clean = pattern.replace("'", "").replace("\"", "").trim(); + if (clean.contains(".")) { + int dot = clean.indexOf('.'); + cmsFilter = clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " *"; + } else if (clean.split("\\s+").length == 1) { + cmsFilter = clean + " * *"; + } else { + cmsFilter = clean; + } + } + client.getPS().SendKeys("LISTFILE " + cmsFilter + " ( nohead alloc date[enter]"); + } else { + if (pattern.isEmpty()) { + client.getPS().SendKeys("LISTCAT[enter]"); + } else { + String dsnPattern = pattern.trim(); + if (dsnPattern.startsWith("'") && dsnPattern.endsWith("'") && dsnPattern.length() > 2) { + dsnPattern = dsnPattern.substring(1, dsnPattern.length() - 1); + } + int paren = dsnPattern.indexOf('('); + if (paren > 0) { + dsnPattern = dsnPattern.substring(0, paren); + } + client.getPS().SendKeys("LISTCAT LEVEL(" + dsnPattern + ")[enter]"); + } + } + + // 3. Wait for host to process command and unlock keyboard + Thread.sleep(100); + waitForKeyboardUnlock(client, 5000); + + // 4. Collect pages, handling MORE... on CMS or *** on TSO + StringBuilder allPages = new StringBuilder(); + int maxPages = 20; + for (int page = 0; page < maxPages; page++) { + int rows = client.getPS().getRows(); + int cols = client.getPS().getCols(); + String screen = client.getPS().copyString(0, 0, rows - 1, cols - 1); + allPages.append(screen).append("\n"); + + boolean hasMore = false; + if (hostType == FTConfig.HostType.CMS) { + if (screen.contains("MORE...") || screen.contains("HOLDING")) { + hasMore = true; + client.getPS().SendKeys("[enter]"); + } + } else { + if (screen.contains("***")) { + hasMore = true; + client.getPS().SendKeys("[enter]"); + } + } + + if (hasMore) { + Thread.sleep(100); + waitForKeyboardUnlock(client, 3000); + } else { + break; + } + } + return allPages.toString(); + } + + @Override + protected void done() { + if (queryHostBtn != null) queryHostBtn.setEnabled(true); + try { + String screenText = get(); + parseText(screenText, "host query"); + if (!pattern.isEmpty() && !allEntries.isEmpty()) { + applyFilter(); + } + } catch (Exception ex) { + statusLabel.setText("Host query failed: " + ex.getMessage()); + } + } + }; + worker.execute(); + } + + private static boolean waitForKeyboardUnlock(Telnet3270Client client, long timeoutMs) { + if (client == null) return false; + long deadline = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() < deadline) { + boolean locked = false; + if (client.getInputProcessor() != null && client.getInputProcessor().isKeyboardLocked()) { + locked = true; + } + if (client.getOIA() != null && (client.getOIA().isKeyLocked() || client.getOIA().isXSystem())) { + locked = true; + } + if (!locked) { + return true; + } + try { + Thread.sleep(40); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; } private void parseCurrentScreen() { - if (client == null || !client.getConnectionState().isFullSession()) { + if (client == null || !client.getConnectionState().isFullSession() || client.getPS() == null) { statusLabel.setText("No active session to parse."); return; } + int rows = client.getPS().getRows(); + int cols = client.getPS().getCols(); + String screenText = client.getPS().copyString(0, 0, rows - 1, cols - 1); + parseText(screenText, "presentation space"); + } + + private void parseClipboard() { + try { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable contents = clipboard.getContents(null); + if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { + String clipText = (String) contents.getTransferData(DataFlavor.stringFlavor); + if (clipText != null && !clipText.trim().isEmpty()) { + parseText(clipText, "clipboard"); + return; + } + } + statusLabel.setText("Clipboard contains no text."); + } catch (Exception ex) { + statusLabel.setText("Error reading clipboard: " + ex.getMessage()); + } + } + + private void parseText(String text, String source) { setupTableColumns(); - currentEntries.clear(); + allEntries.clear(); FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); - String screenText = client.getPS().getString(0, client.getPS().getSize()); if (hostType == FTConfig.HostType.CMS) { - List cmsEntries = client.getXfer().getCmsDirectory(screenText); - currentEntries.addAll(cmsEntries); - for (CMSDirectoryEntry entry : cmsEntries) { - tableModel.addRow(new Object[]{ - entry.getFilename(), entry.getFiletype(), entry.getFilemode(), - entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime() - }); - } + List cmsEntries = CMSDirectoryParser.parse(text); + allEntries.addAll(cmsEntries); } else { - List tsoEntries = client.getXfer().getTsoDirectory(screenText); - currentEntries.addAll(tsoEntries); - for (TSODirectoryEntry entry : tsoEntries) { + List tsoEntries = TSODirectoryParser.parse(text); + allEntries.addAll(tsoEntries); + } + + applyFilter(); + if (allEntries.isEmpty()) { + statusLabel.setText("0 entries found from " + source + ". Use 'Query Host' or 'Paste Clipboard'."); + } else { + statusLabel.setText("Parsed " + allEntries.size() + " entries from " + source + "."); + } + } + + private void applyFilter() { + tableModel.setRowCount(0); + currentEntries.clear(); + String filter = queryField.getText().trim(); + String filterUpper = filter.toUpperCase(); + FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); + + for (HostDirectoryEntry entry : allEntries) { + if (!filter.isEmpty() && !matchesFilter(entry.getName().toUpperCase(), filterUpper)) { + continue; + } + currentEntries.add(entry); + if (hostType == FTConfig.HostType.CMS && entry instanceof CMSDirectoryEntry) { + CMSDirectoryEntry cms = (CMSDirectoryEntry) entry; tableModel.addRow(new Object[]{ - entry.getDatasetName(), entry.getVolume(), entry.getRecfm(), - entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents() + cms.getFilename(), cms.getFiletype(), cms.getFilemode(), + cms.getRecfm(), cms.getLrecl(), cms.getNumRecords(), cms.getDate() + " " + cms.getTime() + }); + } else if (entry instanceof TSODirectoryEntry) { + TSODirectoryEntry tso = (TSODirectoryEntry) entry; + tableModel.addRow(new Object[]{ + tso.getDatasetName(), tso.getVolume(), tso.getRecfm(), + tso.getLrecl(), tso.getBlksize(), tso.getTracksAllocated(), tso.getExtents() }); } } - statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space."); + if (!allEntries.isEmpty()) { + statusLabel.setText(currentEntries.size() + " entries shown (" + allEntries.size() + " total)."); + } + } + + private boolean matchesFilter(String name, String filter) { + String cleanFilter = filter.replace("'", "").trim(); + if (cleanFilter.isEmpty() || cleanFilter.equals("*") || cleanFilter.equals("* * *") || cleanFilter.equals("*.*")) { + return true; + } + if (cleanFilter.contains("*")) { + String regex = "\\Q" + cleanFilter.replace("*", "\\E.*\\Q") + "\\E"; + return name.matches(regex); + } + return name.contains(cleanFilter); } private void onConfirmSelection() { @@ -267,4 +477,12 @@ public class HostDirectoryDialog extends JDialog { public String getSelectedHostFile() { return selectedHostFile; } + + public List getAllEntries() { + return allEntries; + } + + public List getCurrentEntries() { + return currentEntries; + } } diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java index c6e2d06..3ac69b3 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -236,11 +236,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I boolean wasPanelUnfocused = !hasFocus(); isActivationClick = wasWindowInactive || wasPanelUnfocused; - if (wasWindowInactive) { + if (win != null && wasWindowInactive) { win.toFront(); - win.requestFocus(); } requestFocus(); + SwingUtilities.invokeLater(() -> { + if (!hasFocus()) { + requestFocus(); + } + }); // Right-click context menu if (SwingUtilities.isRightMouseButton(e)) { @@ -433,6 +437,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I @Override public void mouseReleased(MouseEvent e) { + if (!hasFocus()) { + requestFocus(); + } if (isDragging) { isDragging = false; if (activeDragHandle != DragHandle.NONE) { diff --git a/j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java index 76164c2..7abaa2f 100644 --- a/j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java +++ b/j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java @@ -99,4 +99,45 @@ public class FileTransferSessionAndCmsTest { // Headless environment; verified through state retention } } + + @Test + public void testPdsSessionStatePersistence() { + FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState(); + state.hostType = FTConfig.HostType.TSO; + state.isSend = true; + state.hostFile = "'USER.PDS.DATA'"; + state.units = FTConfig.AllocationUnit.CYLINDERS; + state.space = "10,5"; + state.dirBlocks = "20"; + + FileTransferDialog.setLastTransferState(state); + + FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState(); + assertNotNull(retrieved); + assertEquals(FTConfig.AllocationUnit.CYLINDERS, retrieved.units); + assertEquals("10,5", retrieved.space); + assertEquals("20", retrieved.dirBlocks); + + try { + FileTransferDialog dialog = new FileTransferDialog(null, null); + assertEquals(FTConfig.AllocationUnit.CYLINDERS, dialog.getUnitsCombo().getSelectedItem()); + assertEquals("10,5", dialog.getSpaceField().getText()); + assertEquals("20", dialog.getDirField().getText()); + dialog.dispose(); + } catch (HeadlessException ignored) { + } + } + + @Test + public void testHostDirectoryDialogStructureAndFilter() { + try { + haus.nightmare.j3270.ui.HostDirectoryDialog dialog = + new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "TEST EXEC"); + assertNotNull(dialog); + assertFalse(dialog.isConfirmed()); + assertNull(dialog.getSelectedHostFile()); + dialog.dispose(); + } catch (HeadlessException ignored) { + } + } } diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/TerminalPanelSelectionTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/TerminalPanelSelectionTest.java index 7545a6e..82ac2de 100644 --- a/j3270/src/test/java/haus/nightmare/j3270/ui/TerminalPanelSelectionTest.java +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/TerminalPanelSelectionTest.java @@ -98,4 +98,25 @@ public class TerminalPanelSelectionTest { // Handled gracefully in headless environments } } + + @Test + @DisplayName("Mouse press and release on TerminalPanel requests focus") + public void testTerminalPanelMousePressRequestsFocus() { + try { + TerminalPanel panel = new TerminalPanel(); + assertTrue(panel.isFocusable()); + + MouseListener[] mouseListeners = panel.getMouseListeners(); + assertTrue(mouseListeners.length > 0); + + MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), + 0, 50, 50, 1, false, MouseEvent.BUTTON1); + mouseListeners[0].mousePressed(pressEvent); + + MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(), + 0, 50, 50, 1, false, MouseEvent.BUTTON1); + mouseListeners[0].mouseReleased(releaseEvent); + } catch (HeadlessException ignored) { + } + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java index c0008dd..dfa6a8c 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java @@ -9,10 +9,11 @@ import java.util.regex.Pattern; */ public class FTConfig { - private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?"); + private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FB|VB|VBS|FBS|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 SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?(?:[\\s,]+(\\d+))?\\)?"); + private static final Pattern DIR_PATTERN = Pattern.compile("(?i)(?:DIR|DIRECTORY)[\\s\\(]+(\\d+)\\)?"); private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?"); private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?"); private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?"); @@ -68,6 +69,7 @@ public class FTConfig { private int blksize = 0; private int primarySpace = 0; private int secondarySpace = 0; + private int directoryBlocks = 0; private int avblock = 0; private int dftBufferSize = FTConstants.DFT_BUF; private String otherOptions = null; @@ -172,6 +174,20 @@ public class FTConfig { public int getSecondarySpace() { return secondarySpace; } public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; } + public int getDirectoryBlocks() { return directoryBlocks; } + public void setDirectoryBlocks(int directoryBlocks) { this.directoryBlocks = Math.max(0, directoryBlocks); } + public void setDirectoryBlocks(String dir) { + if (dir == null || dir.trim().isEmpty()) { + this.directoryBlocks = 0; + return; + } + try { + this.directoryBlocks = Math.max(0, Integer.parseInt(dir.trim())); + } catch (NumberFormatException e) { + this.directoryBlocks = 0; + } + } + public int getAvblock() { return avblock; } public void setAvblock(int avblock) { this.avblock = avblock; } @@ -217,9 +233,9 @@ public class FTConfig { this.recfm = RecordFormat.DEFAULT; } else { String u = r.trim().toUpperCase(); - if (u.equals("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED; - else if (u.equals("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE; - else if (u.equals("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED; + if (u.startsWith("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED; + else if (u.startsWith("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE; + else if (u.startsWith("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED; else this.recfm = RecordFormat.DEFAULT; } } @@ -233,14 +249,23 @@ public class FTConfig { } public void setSpace(String s) { - // Space string like "10,5" for primary/secondary + // Space string like "10,5" or "10,5,10" for primary/secondary/directory if (s == null || s.trim().isEmpty()) return; String[] parts = s.split(","); try { - if (parts.length > 0) this.primarySpace = Integer.parseInt(parts[0].trim()); - if (parts.length > 1) this.secondarySpace = Integer.parseInt(parts[1].trim()); - } catch (NumberFormatException e) { - // ignore + if (parts.length > 0 && !parts[0].trim().isEmpty()) { + this.primarySpace = Integer.parseInt(parts[0].trim()); + if (this.units == AllocationUnit.DEFAULT) { + this.units = AllocationUnit.TRACKS; + } + } + if (parts.length > 1 && !parts[1].trim().isEmpty()) { + this.secondarySpace = Integer.parseInt(parts[1].trim()); + } + if (parts.length > 2 && !parts[2].trim().isEmpty()) { + this.directoryBlocks = Math.max(0, Integer.parseInt(parts[2].trim())); + } + } catch (NumberFormatException ignored) { } } @@ -279,6 +304,22 @@ public class FTConfig { if (spaceMatcher.group(2) != null) { this.secondarySpace = Integer.parseInt(spaceMatcher.group(2)); } + if (spaceMatcher.group(3) != null) { + this.directoryBlocks = Math.max(0, Integer.parseInt(spaceMatcher.group(3))); + } + if (this.units == AllocationUnit.DEFAULT) { + this.units = AllocationUnit.TRACKS; + } + } catch (NumberFormatException ignored) {} + } + + Matcher dirMatcher = DIR_PATTERN.matcher(trimmed); + if (dirMatcher.find()) { + try { + this.directoryBlocks = Math.max(0, Integer.parseInt(dirMatcher.group(1))); + if (this.units == AllocationUnit.DEFAULT) { + this.units = AllocationUnit.TRACKS; + } } catch (NumberFormatException ignored) {} } @@ -568,14 +609,18 @@ public class FTConfig { opts.append(" BLKSIZE(").append(blksize).append(")"); } } - if (units != AllocationUnit.DEFAULT) { + if (units != AllocationUnit.DEFAULT || primarySpace > 0 || directoryBlocks > 0) { + AllocationUnit effectiveUnits = (units != AllocationUnit.DEFAULT) ? units : AllocationUnit.TRACKS; if (opts.length() > 0) opts.append(" "); opts.append("SPACE(").append(primarySpace); - if (secondarySpace > 0) { + if (secondarySpace > 0 || directoryBlocks > 0) { opts.append(",").append(secondarySpace); } + if (directoryBlocks > 0) { + opts.append(",").append(directoryBlocks); + } opts.append(")"); - switch (units) { + switch (effectiveUnits) { case TRACKS: opts.append(" TRACKS"); break; case CYLINDERS: opts.append(" CYLINDERS"); break; case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/CMSDirectoryParser.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/CMSDirectoryParser.java index ae6ca5a..3c2352f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/CMSDirectoryParser.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/CMSDirectoryParser.java @@ -17,7 +17,7 @@ public class CMSDirectoryParser { return entries; } - String[] lines = text.split("\r?\n"); + String[] lines = splitIntoLines(text); for (String line : lines) { String trimmed = line.trim(); @@ -26,6 +26,7 @@ public class CMSDirectoryParser { String upper = trimmed.toUpperCase(); if (upper.startsWith("FILENAME") || upper.startsWith("DIRECTORY") || upper.startsWith("FILELIST")) continue; + if (upper.startsWith("READY;") || upper.equals("READY") || upper.startsWith("MORE...") || upper.startsWith("HOLDING")) continue; CMSDirectoryEntry entry = parseCmsLine(trimmed); if (entry != null) { @@ -36,6 +37,24 @@ public class CMSDirectoryParser { return entries; } + private static String[] splitIntoLines(String text) { + if (text.contains("\n") || text.contains("\r")) { + return text.split("\r?\n"); + } + if (text.length() >= 80) { + int lineLen = (text.length() % 132 == 0 && text.length() % 80 != 0) ? 132 : 80; + int numLines = (text.length() + lineLen - 1) / lineLen; + String[] lines = new String[numLines]; + for (int i = 0; i < numLines; i++) { + int start = i * lineLen; + int end = Math.min(start + lineLen, text.length()); + lines[i] = text.substring(start, end); + } + return lines; + } + return new String[]{text}; + } + private static CMSDirectoryEntry parseCmsLine(String line) { String[] tokens = line.split("\\s+"); if (tokens.length < 2) return null; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/TSODirectoryParser.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/TSODirectoryParser.java index 2ef2a33..67d482c 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/TSODirectoryParser.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/dir/TSODirectoryParser.java @@ -27,7 +27,7 @@ public class TSODirectoryParser { return entries; } - String[] lines = text.split("\r?\n"); + String[] lines = splitIntoLines(text); // Check if text is LISTCAT format if (text.toUpperCase().contains("NONVSAM") || text.toUpperCase().contains("IN-CAT")) { @@ -39,7 +39,8 @@ public class TSODirectoryParser { for (String line : lines) { String trimmed = line.trim(); if (trimmed.isEmpty()) continue; - if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**")) continue; + if (trimmed.startsWith("--") || trimmed.startsWith("==") || trimmed.startsWith("**") || trimmed.startsWith("***")) continue; + if (trimmed.equalsIgnoreCase("READY") || trimmed.toUpperCase().startsWith("READY;")) continue; if (trimmed.toUpperCase().startsWith("COMMAND") || trimmed.toUpperCase().startsWith("DSLIST") || trimmed.toUpperCase().startsWith("DATA SETS")) continue; TSODirectoryEntry entry = parseTabularLine(trimmed); @@ -51,6 +52,24 @@ public class TSODirectoryParser { return entries; } + private static String[] splitIntoLines(String text) { + if (text.contains("\n") || text.contains("\r")) { + return text.split("\r?\n"); + } + if (text.length() >= 80) { + int lineLen = (text.length() % 132 == 0 && text.length() % 80 != 0) ? 132 : 80; + int numLines = (text.length() + lineLen - 1) / lineLen; + String[] lines = new String[numLines]; + for (int i = 0; i < numLines; i++) { + int start = i * lineLen; + int end = Math.min(start + lineLen, text.length()); + lines[i] = text.substring(start, end); + } + return lines; + } + return new String[]{text}; + } + private static void parseListcat(String[] lines, List entries) { TSODirectoryEntry current = null; for (String line : lines) { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferFileObject.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferFileObject.java index 0c325c0..0d62b0a 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferFileObject.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer/FileTransferFileObject.java @@ -37,6 +37,7 @@ public class FileTransferFileObject implements Serializable { private int blksize = 0; private int primarySpace = 0; private int secondarySpace = 0; + private int directoryBlocks = 0; private int avblock = 0; private String options = ""; @@ -201,6 +202,14 @@ public class FileTransferFileObject implements Serializable { this.secondarySpace = secondarySpace; } + public int getDirectoryBlocks() { + return directoryBlocks; + } + + public void setDirectoryBlocks(int directoryBlocks) { + this.directoryBlocks = Math.max(0, directoryBlocks); + } + public int getAvblock() { return avblock; } @@ -241,6 +250,7 @@ public class FileTransferFileObject implements Serializable { config.setBlksize(blksize); config.setPrimarySpace(primarySpace); config.setSecondarySpace(secondarySpace); + config.setDirectoryBlocks(directoryBlocks); config.setAvblock(avblock); if (options != null && !options.isEmpty()) { config.parseOptions(options); @@ -263,6 +273,7 @@ public class FileTransferFileObject implements Serializable { this.blksize = config.getBlksize(); this.primarySpace = config.getPrimarySpace(); this.secondarySpace = config.getSecondarySpace(); + this.directoryBlocks = config.getDirectoryBlocks(); this.avblock = config.getAvblock(); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java index a1352fc..99a36f0 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java @@ -603,10 +603,73 @@ public class Xfer3270 implements FileTransferInterface { if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_PROCESSING); Vector result = new Vector(); + String dataToParse = hostQuery; + if (hostQuery == null || (!hostQuery.contains("\n") && !hostQuery.contains("\r") + && !hostQuery.toUpperCase().contains("NONVSAM") && !hostQuery.toUpperCase().contains("IN-CAT") + && !hostQuery.toUpperCase().contains("FORMAT"))) { + if (session != null && session.getPS() != null) { + try { + ECLPS ps = session.getPS(); + ps.SendKeys("[clear]"); + if (session.getOIA() != null) { + session.getOIA().waitForInput(3000); + } + if (hostType == VM_CMS) { + String filter = (hostQuery != null && !hostQuery.trim().isEmpty()) ? hostQuery.trim() : "* * *"; + ps.SendKeys("LISTFILE " + filter + " ( nohead alloc date[enter]"); + } else { + String filter = (hostQuery != null && !hostQuery.trim().isEmpty()) ? hostQuery.trim() : ""; + if (filter.isEmpty()) { + ps.SendKeys("LISTCAT[enter]"); + } else { + if (!filter.startsWith("'")) filter = "'" + filter + "'"; + ps.SendKeys("LISTCAT LEVEL(" + filter + ")[enter]"); + } + } + try { Thread.sleep(100); } catch (InterruptedException ignored) {} + if (session.getOIA() != null) { + session.getOIA().waitForInput(5000); + } + StringBuilder sbAll = new StringBuilder(); + for (int page = 0; page < 20; page++) { + int rows = ps.getRows(); + int cols = ps.getCols(); + String screen = ps.copyString(0, 0, rows - 1, cols - 1); + sbAll.append(screen).append("\n"); + + boolean hasMore = false; + if (hostType == VM_CMS) { + if (screen.contains("MORE...") || screen.contains("HOLDING")) { + hasMore = true; + ps.SendKeys("[enter]"); + } + } else { + if (screen.contains("***")) { + hasMore = true; + ps.SendKeys("[enter]"); + } + } + + if (hasMore) { + try { Thread.sleep(100); } catch (InterruptedException ignored) {} + if (session.getOIA() != null) { + session.getOIA().waitForInput(3000); + } + } else { + break; + } + } + dataToParse = sbAll.toString(); + } catch (Exception ignored) { + // Fall back to hostQuery + } + } + } + try { if (hostType == VM_CMS) { if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING); - List cmsEntries = CMSDirectoryParser.parse(hostQuery); + List cmsEntries = CMSDirectoryParser.parse(dataToParse); for (CMSDirectoryEntry entry : cmsEntries) { String hName = entry.getFilename() + " " + entry.getFiletype(); String lName = entry.getFilename() + "." + entry.getFiletype(); @@ -622,7 +685,7 @@ public class Xfer3270 implements FileTransferInterface { } } else { if (callback != null) callback.setStatus(FileTransferHostDirectoryInterface.STATUS_RECEIVING); - List tsoEntries = TSODirectoryParser.parse(hostQuery); + List tsoEntries = TSODirectoryParser.parse(dataToParse); for (TSODirectoryEntry entry : tsoEntries) { String name = entry.getName(); if (hostFiles != null) hostFiles.addElement(name); diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java index 9b5b495..508c954 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java @@ -125,4 +125,66 @@ public class FTConfigTest { String cmd = config.buildCommand(); assertEquals("IND$FILE PUT FILE01 (BINARY NOCRLF REPLACE", cmd); } + + @Test + public void testPdsAllocationCommand() { + FTConfig config = new FTConfig(); + config.setHostType(FTConfig.HostType.TSO); + config.setDirection(FTConfig.Direction.SEND); + config.setHostFilename("'USER.PDS.DATA'"); + config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrAction(FTConfig.CrAction.REMOVE); + config.setRecfm("FB"); + config.setLrecl("80"); + config.setBlksize("3120"); + config.setPrimarySpace(15); + config.setSecondarySpace(5); + config.setDirectoryBlocks(10); + config.setUnits(FTConfig.AllocationUnit.TRACKS); + + String cmd = config.buildCommand(); + assertEquals("IND$FILE PUT 'USER.PDS.DATA' ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(15,5,10) TRACKS", cmd); + } + + @Test + public void testPdsParseOptionsThreeParameters() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostFilename("SYS1.PARMLIB"); + config.parseOptions("TSO ASCII CRLF SPACE(20,10,15) TRACKS"); + + assertEquals(20, config.getPrimarySpace()); + assertEquals(10, config.getSecondarySpace()); + assertEquals(15, config.getDirectoryBlocks()); + assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits()); + assertEquals("IND$FILE PUT SYS1.PARMLIB ASCII CRLF SPACE(20,10,15) TRACKS", config.buildCommand()); + } + + @Test + public void testPdsParseOptionsWithDirToken() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostFilename("SYS1.PROCLIB"); + config.parseOptions("TSO ASCII CRLF SPACE(10,5) DIR(20) CYLINDERS"); + + assertEquals(10, config.getPrimarySpace()); + assertEquals(5, config.getSecondarySpace()); + assertEquals(20, config.getDirectoryBlocks()); + assertEquals(FTConfig.AllocationUnit.CYLINDERS, config.getUnits()); + assertEquals("IND$FILE PUT SYS1.PROCLIB ASCII CRLF SPACE(10,5,20) CYLINDERS", config.buildCommand()); + } + + @Test + public void testPdsSetSpaceWithThreeArguments() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostFilename("MY.PDS"); + config.setSpace("12,4,8"); + + assertEquals(12, config.getPrimarySpace()); + assertEquals(4, config.getSecondarySpace()); + assertEquals(8, config.getDirectoryBlocks()); + assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits()); + assertTrue(config.buildCommand().contains("SPACE(12,4,8) TRACKS")); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/dir/DirectoryParserTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/dir/DirectoryParserTest.java index dcf8407..502737f 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/dir/DirectoryParserTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/dir/DirectoryParserTest.java @@ -121,4 +121,34 @@ public class DirectoryParserTest { assertEquals("GENPASS", entries.get(1).getFilename()); assertEquals("REXX", entries.get(1).getFiletype()); } + + @Test + public void testContinuousScreenBufferParsingCms() { + // Continuous 80-character rows without newlines (as produced by unformatted 3270 presentation space) + String row1 = String.format("%-80s", "PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01"); + String row2 = String.format("%-80s", "TEST DATA A1 F 80 100 2 2026-08-01 09:15:00"); + String row3 = String.format("%-80s", "Ready; T=0.01/0.01 12:00:00"); + String continuousScreen = row1 + row2 + row3; + + List entries = CMSDirectoryParser.parse(continuousScreen); + assertEquals(2, entries.size(), "Should parse 2 CMS entries and ignore Ready;"); + assertEquals("PROFILE", entries.get(0).getFilename()); + assertEquals("TEST", entries.get(1).getFilename()); + } + + @Test + public void testContinuousScreenBufferParsingTso() { + // Continuous 80-character rows without newlines for ISPF DSLIST + String row1 = String.format("%-80s", "Dslist - Data Sets Matching USER.*"); + String row2 = String.format("%-80s", "Command - Enter \"/\" to select action Tracks Dsorg Recfm Volume"); + String row3 = String.format("%-80s", " USER.TEST.CNTL 15 PO FB TSO001"); + String row4 = String.format("%-80s", " USER.TEST.DATA 10 PS FB TSO001"); + String row5 = String.format("%-80s", "***"); + String continuousScreen = row1 + row2 + row3 + row4 + row5; + + List entries = TSODirectoryParser.parse(continuousScreen); + assertEquals(2, entries.size(), "Should parse 2 TSO entries and ignore ***"); + assertEquals("USER.TEST.CNTL", entries.get(0).getName()); + assertEquals("USER.TEST.DATA", entries.get(1).getName()); + } }