/* 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 ...update: starting that here (v6) v6_z 26Jan2020 (v6 with new feature funished) --Addded a button that allows the user to choose whether to include search within .txt and .csv files (Also added a new panel, howDeepPanel, to allow that button to be placed to right of depthButton plus boolean lookInside and associated logic to call fileContainsTarget(...) only when true etc) (--Also renamed preexisting GUI panels, and applied a few cosmetic tweaks) Next: Try to use Apache POI to add docx to file types that can be searched? 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.GridLayout; import java.awt.Insets; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Stream; import javax.swing.BorderFactory; 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; import javax.swing.border.EmptyBorder; class FindFileOrFolder_v6_z { 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) boolean caseSensitive; // whether search term is treated as case-sensitive (default is no) // --v6_ii3 added... boolean lookInside; // whether to search for target inside txt and cvs files also // Declaring fields for GUI components... JLabel startFolderLabel = 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 also in subfolders of the starting folder"); // --v6_ii3 added... JButton withinFileButton = new JButton("Search also text within following file types: txt, cvs"); // JPanel howDeepPanel = new JPanel(new BorderLayout()); // to hold the depthButton and withinFileButton // ...buttons arranged either side with big empty centre section, so tried... // JPanel howDeepPanel = new JPanel(new FlowLayout()); // or, since FlowLayout is default... // JPanel howDeepPanel = new JPanel(); // ...buttons displayed at centre, with big empty side areas, so tried... JPanel howDeepPanel = new JPanel(new GridLayout()); // ...yes, what I wanted: equal-width buttons filling the panel JPanel wherePanel = new JPanel(new BorderLayout()); // to hold the above 3 components JLabel targetNameLabel = 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"); JPanel whatPanel = new JPanel(new BorderLayout()); // to hold the above 3 components JPanel inputsPanel = new JPanel(new BorderLayout()); // to hold wherePanel, whatPanel JButton findButton = new JButton("Find files/folders"); JTextArea displayTextArea = new JTextArea(40, 100); // displays output, i.e. paths for files/folders found JPanel findAndShowPanel = new JPanel(new BorderLayout()); // to hold the above pair of components JFrame frame = new JFrame("FindFileOrFolder"); // to hold all above (sub)panels FindFileOrFolder_v6_z() // constructor, called when main method runs { // --v3_x added, to make background showing through in contained label lighter than default light grey // (alternatively, could set the JLabel opaque and set this backgroung for it independantly) wherePanel.setBackground(new Color(245, 245, 245)); startFolderLabel.setBorder(new EmptyBorder(10, 10, 0, 0)); // (--v3_x added) wherePanel.add(startFolderLabel, BorderLayout.NORTH); startFolderTextArea.setLineWrap(true); startFolderTextArea.setMargin(new Insets(5, 10, 5, 10)); // --v3_x altered to soften line between text area and its label JScrollPane startSP = new JScrollPane(startFolderTextArea); startSP.setBorder(BorderFactory.createMatteBorder(2, 0, 0, 0, new Color(245, 245, 245))); // startSP.setBorder(BorderFactory.createMatteBorder(2, 0, 0, 0, new Color(240, 240, 240))); // wherePanel.add(new JScrollPane(startFolderTextArea), BorderLayout.CENTER); wherePanel.add(startSP, 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 // wherePanel.add(depthButton, BorderLayout.SOUTH); // --V6_ii3 changed to... howDeepPanel.add(depthButton, BorderLayout.WEST); // --v6_ii3 added... withinFileButton.addActionListener(actionEvent -> { if (lookInside == false) // (could rewrite as !lookInside) { lookInside = true; withinFileButton.setText("Revert to not searching also text within following file types: txt, cvs"); } else // (lookInside is true) { lookInside = false; withinFileButton.setText("Revert to searching also text within following file types: txt, cvs"); } } ); // toggles between searching just file names and text within any txt/cvs files also howDeepPanel.add(withinFileButton, BorderLayout.EAST); // --v6_ii3 added... wherePanel.add(howDeepPanel, BorderLayout.SOUTH); inputsPanel.add(wherePanel, BorderLayout.NORTH); whatPanel.setBackground(new Color(245, 245, 245)); // --v6_z added (comment as for wherePanel above) targetNameLabel.setBorder(new EmptyBorder(10, 10, 0, 0)); // (--v3_x added) whatPanel.add(targetNameLabel, BorderLayout.NORTH); targetNameTextArea.setLineWrap(true); targetNameTextArea.setMargin(new Insets(5, 10, 5, 10)); // --v3_x altered to soften line between text area and its label JScrollPane targetNameSP = new JScrollPane(targetNameTextArea); targetNameSP.setBorder(BorderFactory.createMatteBorder(2, 0, 0, 0, new Color(245, 245, 245))); // whatPanel.add(new JScrollPane(targetNameTextArea), BorderLayout.CENTER); whatPanel.add(targetNameSP, BorderLayout.CENTER); 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 whatPanel.add(caseButton, BorderLayout.SOUTH); inputsPanel.add(whatPanel, 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) } 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 toStringHandled(p.getFileName()).contains(target) // target in file/folder name... // || fileContainsTarget(p, target) )); // or within file text // v6_ii3 changed to... || (lookInside ? fileContainsTarget(p, target) : false) )); // ...or, if user wants inclusion, within text files // (Note that if I cahnged from Files.find(...) to Files.walk(...) // with subsequent filtering of returned stream I could do single lookInside // check by wrapping filter containing fileContainsTarget() predicate // with if-statement like done for linesFromFile.map(...) call in // fileContainsTarget method below - sig more efficient or not?) 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)); findAndShowPanel.add(findButton, BorderLayout.NORTH); displayTextArea.setEditable(false); displayTextArea.setLineWrap(true); displayTextArea.setWrapStyleWord(true); displayTextArea.setMargin(new Insets(5, 5, 5, 5)); findAndShowPanel.add(new JScrollPane(displayTextArea), BorderLayout.SOUTH); frame.add(inputsPanel, BorderLayout.NORTH); frame.add(findAndShowPanel, 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"); } if (!caseSensitive) // if user has chosen case-sensitive option, this does not happen... { result = result.toLowerCase(); // ...search term made all-lowercase } return result; } boolean fileContainsTarget (Path p, String target) { Stream linesFromFile = Stream.empty(); boolean targetFound = false; try { String fileName = (p.getFileName().toString().toLowerCase()); if ( ( fileName.endsWith(".txt") || // defining file types in which to search... fileName.endsWith(".csv") // ...and could add any other 'UTF text' types if there are any ) && Files.isReadable(p) // (have not found I actually need this, but testing has been limited) ) { linesFromFile = Files.lines(p, StandardCharsets.ISO_8859_1); // Note for future reference: adding second arg StandardCharsets.ISO_8859_1 may avoid // MalformedInputException being thrown for non-UTF-encoded files, // e.g. docx, xlsx, pdf, which I am not including here as only 'gibberish' symbols are displayed of course if (!caseSensitive) { linesFromFile = linesFromFile.map(s -> s.toLowerCase()); // or could use String::toLowerCase I think } targetFound = linesFromFile.anyMatch(s -> s.contains(target)); // General note(s): As anyMatch(...) will return as soon as a match is found (if any) // it will not waste resources/time processing subsequent file lines. // Order in which lines is processed not important, // so might investigate later if making stream parallel improves speed } } catch (IOException ex) { System.out.println(ex); } return targetFound; } public static void main(String[] args) { new FindFileOrFolder_v6_z(); } }