/* Objective: Since Windows 10 File Explorer search seems messed-up on my laptop (and computers of at least some others reporting online) since late 2019, try to make something for finding files/folders on my laptop while waiting for a fix! First just look at file/folder names (might include option to match text within files later) v5_i2 20Jan2020: --Altered to make case sensitivity for the search term optional via a toggling button. Items that might be added/addressed later: --Possible to allow a search to be aborted (while displaying items found up to that point) without closing the program window (& have the button text toggle to a message indicating this option)? Maybe investigate use of SwingWorker. --Maybe try to add an option to include text from within at least some text-encoding file types in search. --Maybe try to address known issue that searches with search-in-subfolders enabled from some start paths near the root, e.g. C:\Users, and even C:\Users\[my user name]\Documents on my system, terminate prematurely. (However, though the user does not receive feedback and may not realise that not all files which should be found are, the program does not crash and will respond normally to a 'regular' subsequent search.) Cause = AccessDeniedException thrown due to denial of access to some folders. Might not be able to handle this from Files.find(...), as used currently, or Files.walk(...), so perhaps try to instead use walkFileTree + FileVisitor. */ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; import java.awt.Insets; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Stream; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.SwingUtilities; class FindFileOrFolder_v5_i2 { String missingStartMessage = ""; String missingTargetMessage = ""; int walkDepth = 1; // --specifies how many subdirectory levels to go down when collecting files to access // initialized to default 1 (do not look in subdirectories) // --v5_i2 added... boolean caseSensitive; // whether search term is treated as case-sensitive (default is no) // Declaring fields for GUI components... JLabel startFolderJLabel = new JLabel(" Enter the path of the folder from within which you want to (start your) search..."); JTextArea startFolderTextArea = new JTextArea(2, 60); // input to specify foolder within which to (start) searching JButton depthButton = new JButton("Search in subfolders of the starting folder also"); // --v5: wiil put in centre of panelForStartComponents JPanel panelForStartComponents = new JPanel(new BorderLayout()); // to hold the above 3 components JLabel targetNameJLabel = new JLabel(" Enter the name or partial name of a file or folder you want to find..."); JTextArea targetNameTextArea = new JTextArea(2, 60); // input to specify file/folder names for which to search JButton caseButton = new JButton("Make search case-sensitive"); // --v5: added; wiil put in centre of panelForTargetComponents JPanel panelForTargetComponents = new JPanel(new BorderLayout()); // to hold the above 3 components JPanel panelForInputs = new JPanel(new BorderLayout()); // to hold panelForStartComponents, panelForTargetComponents JButton findButton = new JButton("Find files/folders"); JTextArea displayTextArea = new JTextArea(40, 100); // displays output, i.e. paths for files/folders found JPanel panelForFindButtonAndOutput = new JPanel(new BorderLayout()); // to hold the above pair of components JFrame frame = new JFrame("FindFileOrFolder"); // to hold all above (sub)panels FindFileOrFolder_v5_i2() // constructor, called when main method runs { // --v2_i5: as well as addition of caseButton and associated rearrangements to accomodate it // some statements moved around to place in order of componts in GUI frame... panelForStartComponents.add(startFolderJLabel, BorderLayout.NORTH); startFolderTextArea.setLineWrap(true); startFolderTextArea.setMargin(new Insets(5, 5, 5, 5)); // panelForStartComponents.add(new JScrollPane(startFolderTextArea), BorderLayout.SOUTH); // --v5_i2 moved to... panelForStartComponents.add(new JScrollPane(startFolderTextArea), BorderLayout.CENTER); depthButton.addActionListener(actionEvent -> { if (walkDepth == 1) { walkDepth = Integer.MAX_VALUE; depthButton.setText("Search in the starting folder only"); } else // (walkDepth is Integer.MAX_VALUE) { walkDepth = 1; depthButton.setText("Search in subfolders of the starting folder also"); } } ); // toggles walk dept between no-subfolders (starting state) and all-subfolders // panelForInputs.add(depthButton, BorderLayout.CENTER); // --v5_i2 moved to... panelForStartComponents.add(depthButton, BorderLayout.SOUTH); panelForInputs.add(panelForStartComponents, BorderLayout.NORTH); panelForTargetComponents.add(targetNameJLabel, BorderLayout.NORTH); targetNameTextArea.setLineWrap(true); targetNameTextArea.setMargin(new Insets(5, 5, 5, 5)); // --V5_i2 changed... // panelForTargetComponents.add(new JScrollPane(targetNameTextArea), BorderLayout.SOUTH); panelForTargetComponents.add(new JScrollPane(targetNameTextArea), BorderLayout.CENTER); // --v5_i2 added... caseButton.addActionListener(actionEvent -> { if (caseSensitive == false) { caseSensitive = true; caseButton.setText("Make search case-insentitive again"); } else // (caseSensitive is true) { caseSensitive = false; caseButton.setText("Make search case-sentitive again"); } } ); // toggles search between case-insensitive (starting state) and case-sensitive panelForTargetComponents.add(caseButton, BorderLayout.SOUTH); panelForInputs.add(panelForTargetComponents, BorderLayout.SOUTH); findButton.addActionListener(actionEvent -> { // (the 'actionPerformed' method body of the implicit ActionListner...) String startText = startFolderTextArea.getText().trim(); Path startPath = null; // path to folder within which to (start) searching boolean startPathValid = true; try { startPath = Paths.get(startText); // ...for some start inputs on this call } catch (Exception e) // to handle possible InvalidPathException { startPathValid = false; } Path validatedStartPath = startPath; // (need an effectively final variable for use later in a lambda) if (startPathValid) // even if real path input, want to check that it a folder (cf a file), so reassign to... { startPathValid = Files.isDirectory(startPath); // (as empty string arg seems to generate Path regarded // as valid (root/current folder?), so for now including check re startText.isEmpty() below) } // --v2_i2 alterations for case sensitivity option... // final String target = targetNameTextArea.getText(); // (partial) names of files/folders for which to search String targetText = targetNameTextArea.getText(); // (partial) names of files/folders for which to search final String target = caseSensitive ? targetText : targetText.toLowerCase(); // if (startText.isEmpty() || !startPathValid) { missingStartMessage = "No valid start path supplied" + "\n"; } if (target.isEmpty()) { missingTargetMessage = "No search term supplied" + "\n"; } if (!startText.isEmpty() && startPathValid && !target.isEmpty()) { // only run process below if target text and valid start path have been supplied (avoid wasteful processing) // first, clear any previous message or search results and display a message to say the search is in progress displayTextArea.setForeground(Color.GREEN); displayTextArea.setFont(new Font("SERIF", Font.BOLD, 20)); displayTextArea.setText("Search in progress..."); SwingUtilities.invokeLater(() -> // (want the above progress' message to appear first if search takes a while) { displayTextArea.setForeground(Color.BLACK); displayTextArea.setFont(null);// this seems to work to restore the font to default (as desired) displayTextArea.setText(null); // clear any previous text before displaying the results try { Stream targetStream = Files.find(validatedStartPath, walkDepth, (p,a) -> !isHiddenHandler(p) && // to exclude hidden (temporary etc) files // --v5_i2 alteration...now diverting the upper-lower-case issue to the // toStringHandled method and tweaked target variable for user option // toStringHandled(p.getFileName()).toLowerCase().contains(target.toLowerCase())); toStringHandled(p.getFileName()).contains(target)); // --v5_i2 alteration: do not need to use toStringHandled here actually...just as well as now may alter case... // targetStream.forEach(p -> displayTextArea.append(toStringHandled(p) + "\n")); // print paths found in output/display area targetStream.forEach(p -> displayTextArea.append(p + "\n")); // print paths found in output/display area if (displayTextArea.getText().equals("")) { displayTextArea.setForeground(Color.BLUE); displayTextArea.setText("No results found"); } // (seems inelegant, but have not thought of a way to do directly from the stream code yet) } catch (IOException ex) { if (ex.getClass().getName().equals("java.nio.file.NoSuchFileException")) { missingInputsMessage(); } // (probably not needed now, though, as should not get to try clause without valid start path) } } ); } else { missingInputsMessage(); } } ); findButton.setMargin(new Insets(10, 10, 10, 10)); findButton.setFont(new Font("SansSerif", Font.BOLD, 20)); panelForFindButtonAndOutput.add(findButton, BorderLayout.NORTH); displayTextArea.setEditable(false); displayTextArea.setLineWrap(true); displayTextArea.setWrapStyleWord(true); displayTextArea.setMargin(new Insets(5, 5, 5, 5)); panelForFindButtonAndOutput.add(new JScrollPane(displayTextArea), BorderLayout.SOUTH); frame.add(panelForInputs, BorderLayout.NORTH); frame.add(panelForFindButtonAndOutput, BorderLayout.SOUTH); frame.setResizable(false); // (buttons disappear if user drags frame bottom up, // while if it's dragged down, the extra space just appears as a gap between the panels, // so just hard-coding big displayTextArea for the moment; // looking briefly online, see descriptions/code for how to make rezizable by dragging, but not trivial frame.setVisible(true); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); // (may need to keep this positioned last) } void missingInputsMessage() // puts message in display area if one or both user inputs missing/incorrect { displayTextArea.setForeground(Color.red); displayTextArea.setText(missingStartMessage + missingTargetMessage); missingStartMessage = ""; // reset for future clicks missingTargetMessage = ""; // (ditto) } boolean isHiddenHandler (Path p) // as Files.isHidden(...) throws checked exception... { // ...easier to handle it externally than in stream coded in findButton's addActionListener above boolean result = false; try { result = Files.isHidden(p); } catch (IOException ex) { System.out.println(ex); } return result; } String toStringHandled (Path p) // as Path's toString() can throw NullPointerException... { // ...easier to handle it externally than in stream coded in findButton's addActionListener above String result = ""; try { result = p.toString(); // p is filename for start path, and is null if that is root, e.g. C:\ } catch (NullPointerException npEx) // ...in which case NullPointerException is thrown { displayTextArea.setText("Note: As you are starting from the root, " + "the search may terminate early if subfolders without access permission are encountered. " + "(This is a known issue, and may also affect searches starting further down the hierarchy, " + "in which case you will not see feedback, unfortunately)." + " May be addressed in a subsequent version of the program." + "\n\n"); } // --v5_i2 addition... if (!caseSensitive) // if user has chosen case-sensitive option, this does not happen... { result = result.toLowerCase(); // ...search term made all-lowercase } return result; } public static void main(String[] args) { new FindFileOrFolder_v5_i2(); } }