Рекурсивный перебор каталогов + прокрутка JScrollPane

:

Добрый день, уважаемые посетители.

Это моя первая статья в данном разделе. В ней я хочу уделить внимание особенностям работы с файловой системой в Java, алгоритму рекурсивного перебора каталогов и файлов, а также механизму реализации автоматической прокрутки JScrollPane во время динамического выведения текста на JTextArea.

N.B. Код проекта можно скачать здесь:

Рекурсивный вызов функции, т.е. вызов функцией самой себя из собственного тела, используется нечасто, однако во многих случаях он существенно упрощает задачу, помогая избавится от циклов, а в некоторых случаях просто незаменим.

Рассмотрим простейший пример с вычислением факториала, то есть произведения 1*2*3*4*…*n, где n - натуральное число. Можно взять быка за рога и забабахать вот такой цикл:

int result = 1;
if (n == 0 || n == 1){
    System.out.println(result);
} else {
    for (int i = 2; i <= n; i++){
        result *= i;
    }
}
System.out.println(result);

Как видим, использование циклов усложняет текст программы, повышает вероятность ошибок, требует дополнительных переменных (и памяти).

Вместо этого можно использовать рекурсию, и обойтись всего 7 строками:

int factorial(int n){
    if (n == 0){
        return 1;
    } else{
        return n * factorial(n-1);
    }
}

Известно, что самое лучшее решение - это простейшее решение. Действительно, цикл гораздо нагляднее и понятен интуитивно. Рекурсия же сложнее для понимания и восприятия. Однако, мы не ищем лёгких путей и стараемся написать красивый и правильный код, а значит должны использовать весь арсенал языка Java.

Давайте посмотрим, как можно использовать рекурсию для последовательного перебора всех файлов или каталогов на жёстком диске. Ниже я привожу коды классов, из которых состоит программа. Главный класс не представляет особого интереса, поскольку он отвечает лишь за создание главного окна.

Код главного класса:

package filewalker;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * Главный класс программы
 * Здесь происходит только отрисовка окна приложения
 * @author rad1kal
*/
public class FileWalker{
    private static JFrame frame;
    static MainPanel panel;
    
    public static void main(String[] args) {
        Dimension ScreenSize = Toolkit.getDefaultToolkit().getScreenSize();
        int x = ScreenSize.width / 2 - 600;
        int y = ScreenSize.height / 2 - 300;
        panel = new MainPanel();
        frame = new JFrame("FileWalker: рекурсивный поиск каталогов/файлов");
        frame.setLocation(x, y);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(1200, 600));
        frame.pack();
        frame.add(panel);
        frame.setVisible(true);
        
        //реализуем слушатель клавиатуры средствами ActionMap
        
        ActionMap am = frame.getRootPane().getActionMap();
        InputMap im = frame.getRootPane().getInputMap(
                JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "QUIT");
        am.put("QUIT", new AbstractAction(){
            @Override
            public void actionPerformed(ActionEvent e){
                frame.dispose();
                System.exit(0);
            }
        });
    }
}

Код главной панели, на которой находятся элементы управления. Здесь реализован пример использования менеджера компоновки GridBagLayout.

package filewalker;

import java.awt.*;
import java.awt.event.*;
import java.io.FileNotFoundException;
import javax.swing.*;

/**
 * Класс, описывающий главную панель
 * @author rad1kal
 * @version 1.0
 */
class MainPanel extends JPanel{
    private ScrollPane outputField;
    private JTextField directoryInputField;

    MainPanel(){
        super();
        setLayout(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        
        JLabel label = new JLabel("Введите путь к корневому каталогу:");
        c.insets = new Insets(10,20,0,0);
        c.anchor = GridBagConstraints.WEST;
        add(label, c);
        
        directoryInputField = new JTextField();
        c.fill = GridBagConstraints.HORIZONTAL;
        c.anchor = GridBagConstraints.CENTER;
        c.gridy = 1;
        c.insets = new Insets(10, 20, 0, 20);
        add(directoryInputField, c);
        
        JButton button = new JButton("Вывести все подкаталоги/файлы");
        button.addActionListener(new MainPanel.ButtonListener());
        button.setFocusable(false);
        c.gridy = 2;
        c.insets = new Insets(10, 0, 10, 0);
        c.anchor = GridBagConstraints.CENTER;
        c.fill = 0;
        add(button, c);
        
        outputField = new ScrollPane();
        c.insets = new Insets(0, 5, 0, 5);
        c.gridy = 3;
        c.weightx = 1;
        c.weighty = 10;
        c.fill = GridBagConstraints.BOTH;
        add(outputField, c);
        
        label = new JLabel("Нажмите Esc для завершения работы.");
        c.anchor = GridBagConstraints.CENTER;
        c.insets = new Insets(10,20,10,0);
        c.gridy = 4;
        c.weighty = 0;
        add(label, c);
    }
    
    /**
     * Обработчик нажатия кнопки
     * @author rad1kal
     */
    private class ButtonListener implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e){
            outputField.clearAll();
            String str = directoryInputField.getText();
            Walker walker;
            try{
                walker = new Walker(str, outputField);
            } catch (FileNotFoundException ex){
                outputField.showWarningMessage();
                return;
            }
            Thread t = new Thread(walker);
            t.start();
        }
    }
}

Класс, реализующий основной функционал программы. На нём остановимся подробнее. Итак, мы хотим перебрать все каталоги/файлы, начиная с корневого. Зачем это нужно? Вот возможные варианты ответа:
  • мы хотим найти некоторые данные в файле, но не знаем в каком именно каталоге и файлы они находятся;

  • мы хотим считать информацию из всех/строго определённых файлов;

  • мы хотим выполнить некоторые действия с каждым/строго определённым файлом/каталогом и т.п.

В моём случае я разрабатываю приложение для поиска информации в большом массиве текстовых файлов. Многие пользователи отметят, что я занимаюсь велосипедостроением и трачу время зря, ведь есть TotalCommander с уже встроенным функционалом. Доля истины в этом есть, однако TotalCommander не всегда корректно обрабатывает *.docx и *xlsx файлы, а также прочие XML-документы. Также отмечу, что при минимальном усилии, данную программу можно приспособить для рекурсивного поиска ссылок в веб-страницах, тем более что тема уже поднималась на форуме.

Итак, вот код поисковика:

package filewalker;

import java.io.File;
import java.io.FileNotFoundException;
import javax.swing.SwingUtilities;

/**
 * Данный класс содержит методы для рекурсивного перебора каталогов,
 * начиная с корневого (указывается пользователем).
 * @author rad1kal
 * @version 2.0
 */
public class Walker implements Runnable{
    public File rootDirectory;
    private ScrollPane outputField;
    
    /**
     * Инициализирует поля RootDirectoryName. Класс бросает исключение
     * FileNotFoundException, когда введен неправильный путь к каталогу.
     * @param rootDirectoryPath путь к корневому каталогу.
     * @param outputField ссылка на панель вывода.
     */
    Walker(String rootDirectoryPath, ScrollPane outputField) throws FileNotFoundException{
        this.outputField = outputField;
        File file = new File(rootDirectoryPath);
        if (file.exists() && file.isDirectory())
            rootDirectory = file;
        else
            throw new FileNotFoundException();
    }
    
    /**
     * Запускает поиск в отдельном потоке.
     * Это необходимо для динамического вывода данных на панель.
     */
    @Override
    public void run(){
        scanDirectory(rootDirectory);
    }
    
    /**
     * Поиск всех каталогов и файлов в папке.
     * @param directory 
     */
    void scanDirectory(File directory){
        File[] files = directory.listFiles();
        if (files != null){
            for (File f : files){
                final String path = f.getAbsolutePath();
                //потокобезопасный вывод происходит здесь
                if (SwingUtilities.isEventDispatchThread()){
                    outputField.append(path);
                } else {
                    SwingUtilities.invokeLater(new Runnable(){
                        @Override
                        public void run(){
                            outputField.append(path);
                        }
                    });
                }
                //рекурсивный вызов для просмотра вложенных каталогов
                if (f.isDirectory() && !f.isHidden()){
                    scanDirectory(f);
                } 
            }
        }
    }
}

Думаю, здесь всё понятно. В конструкторе создается и проверяется ссылка на корневой каталог и если ссылка ошибочна, то далее в методе scanDirectory() получаем список всех файлов и подкаталогов, и для каждого подкаталога метод вызывается снова. Метод поиска запускается в отдельном потоке, что дает возможность реализовать анимированное прокручивание полосы прокрутки в JScrollPane. Обратите внимание на то, как реализовано потокобезопасное взаимодействие с компонентами Swing:
if (SwingUtilities.isEventDispatchThread()){
                    outputField.append(path);
                } else {
                    SwingUtilities.invokeLater(new Runnable(){
                        @Override
                        public void run(){
                            outputField.append(path);
                        }
                    });
                }

здесь метод append() вызывается из потока Event Dispatching Thread, назначением которого является обработка событий связанных с графическим интерфейсом. Добавляя строку на панель вывода мы создаем событие, требующее перерисовки интерфейса. Вызов Event Dispatching Thread позволяет синхронизировать получение строк и их вывод.
Класс, наследующий JScrollPane:
package filewalker;

import javax.swing.JScrollPane;
import javax.swing.JTextArea;

/**
 * Класс, реализующий JScrollPane и содержащий метод
 * для добавления строк и прокручивания полосы прокрутки ScrollBar
 * @author rad1kal
 * @version 1.0
 */
class ScrollPane extends JScrollPane{
    private static JTextArea jta = new JTextArea();
    private final String WARNING_MESSAGE =
            "Вы ввели неверный путь или он ссылается на регулярный файл.";
    
    ScrollPane(){
        super(jta);
        setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }
    
    /**
     * Переопределённый метод, в который добавлена прокрутка VerticalScrollBar.
     * @param s добавляемая строка.
     */
    void append(String s){
        jta.append(s.concat("\n"));
        //Прокручивает полосу прокрутки.
        getVerticalScrollBar().setValue(getVerticalScrollBar().getMaximum());
    }
    
    /**
     * Метод очищает панель вывода.
     */
    void clearAll(){
        jta.setText("");
    }
    
    /**
     * Метод выводит предупреждение о неверном пути к корневому каталогу.
     */
    void showWarningMessage(){
        jta.append(WARNING_MESSAGE);
    }
}

Спасибо за ваше внимание, все вопросы, пожелания и замечания пишите в личку. Прошу не судить строго, ведь это моя первая статья. :)