PdfFinderPanel.java

package edu.jiangxin.apktoolbox.pdf.finder;

import edu.jiangxin.apktoolbox.pdf.PdfUtils;
import edu.jiangxin.apktoolbox.swing.extend.EasyPanel;
import edu.jiangxin.apktoolbox.swing.extend.FileListPanel;
import edu.jiangxin.apktoolbox.utils.Constants;
import edu.jiangxin.apktoolbox.utils.DateUtils;
import edu.jiangxin.apktoolbox.utils.FileUtils;
import edu.jiangxin.apktoolbox.utils.RevealFileUtils;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.Serial;
import java.util.*;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

public class PdfFinderPanel extends EasyPanel {

    @Serial
    private static final long serialVersionUID = 1L;

    private JTabbedPane tabbedPane;

    private JPanel mainPanel;

    private FileListPanel fileListPanel;

    private JRadioButton scannedRadioButton;

    private JRadioButton encryptedRadioButton;

    private JRadioButton nonOutlineRadioButton;

    private JRadioButton hasAnnotationsRadioButton;

    private JSpinner thresholdSpinner;

    private JCheckBox isRecursiveSearched;

    private JPanel resultPanel;

    private JTable resultTable;

    private DefaultTableModel resultTableModel;

    private JButton searchButton;
    private JButton cancelButton;

    private JProgressBar progressBar;

    private JMenuItem openDirMenuItem;

    private JMenuItem copyFilesMenuItem;

    private SearchThread searchThread;

    final private List<File> resultFileList = new ArrayList<>();


    @Override
    public void initUI() {
        tabbedPane = new JTabbedPane();
        add(tabbedPane);

        createMainPanel();
        tabbedPane.addTab("Option", null, mainPanel, "Show Search Options");

        createResultPanel();
        tabbedPane.addTab("Result", null, resultPanel, "Show Search Result");
    }

    private void createMainPanel() {
        mainPanel = new JPanel();
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));

        fileListPanel = new FileListPanel();

        JPanel checkOptionPanel = new JPanel();
        checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS));
        checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options"));

        ButtonGroup buttonGroup = new ButtonGroup();
        ItemListener itemListener = new RadioButtonItemListener();

        scannedRadioButton = new JRadioButton("查找扫描的PDF文件");
        scannedRadioButton.setSelected(true);
        scannedRadioButton.addItemListener(itemListener);
        buttonGroup.add(scannedRadioButton);

        encryptedRadioButton = new JRadioButton("查找加密的PDF文件");
        encryptedRadioButton.addItemListener(itemListener);
        buttonGroup.add(encryptedRadioButton);

        nonOutlineRadioButton = new JRadioButton("查找没有目录的PDF文件");
        nonOutlineRadioButton.addItemListener(itemListener);
        buttonGroup.add(nonOutlineRadioButton);

        hasAnnotationsRadioButton = new JRadioButton("查找有注释的PDF文件");
        hasAnnotationsRadioButton.addItemListener(itemListener);
        buttonGroup.add(hasAnnotationsRadioButton);

        JPanel typePanel = new JPanel();
        typePanel.setLayout(new FlowLayout(FlowLayout.LEFT,10,3));
        typePanel.add(scannedRadioButton);
        typePanel.add(encryptedRadioButton);
        typePanel.add(nonOutlineRadioButton);
        typePanel.add(hasAnnotationsRadioButton);

        JLabel thresholdLabel = new JLabel("Threshold: ");
        thresholdSpinner = new JSpinner();
        thresholdSpinner.setModel(new SpinnerNumberModel(1, 0, 100, 1));

        checkOptionPanel.add(typePanel);
        checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
        checkOptionPanel.add(thresholdLabel);
        checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
        checkOptionPanel.add(thresholdSpinner);
        checkOptionPanel.add(Box.createHorizontalGlue());

        JPanel searchOptionPanel = new JPanel();
        searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS));
        searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Search Options"));

        isRecursiveSearched = new JCheckBox("Recursive");
        isRecursiveSearched.setSelected(true);
        searchOptionPanel.add(isRecursiveSearched);
        searchOptionPanel.add(Box.createHorizontalGlue());

        JPanel operationPanel = new JPanel();
        operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS));
        operationPanel.setBorder(BorderFactory.createTitledBorder("Operations"));

        JPanel buttonPanel = new JPanel();
        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));

        searchButton = new JButton("Search");
        cancelButton = new JButton("Cancel");
        cancelButton.setEnabled(false);
        searchButton.addActionListener(new OperationButtonActionListener());
        cancelButton.addActionListener(new OperationButtonActionListener());
        operationPanel.add(searchButton);
        operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
        operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
        operationPanel.add(cancelButton);
        operationPanel.add(Box.createHorizontalGlue());

        progressBar = new JProgressBar();
        progressBar.setStringPainted(true);
        progressBar.setString("Ready");

        mainPanel.add(fileListPanel);
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
        mainPanel.add(checkOptionPanel);
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
        mainPanel.add(searchOptionPanel);
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
        mainPanel.add(operationPanel);
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
        mainPanel.add(progressBar);
    }

    private void createResultPanel() {
        resultPanel = new JPanel();
        resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS));

        resultTableModel = new PdfFilesTableModel(new Vector<>(), PdfFilesConstants.COLUMN_NAMES);
        resultTable = new JTable(resultTableModel);

        resultTable.setDefaultRenderer(Vector.class, new PdfFilesTableCellRenderer());

        for (int i = 0; i < resultTable.getColumnCount(); i++) {
            resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new PdfFilesTableCellRenderer());
        }

        resultTable.addMouseListener(new MyMouseListener());

        resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);

        JScrollPane scrollPane = new JScrollPane(resultTable);
        resultPanel.add(scrollPane);
    }

    private void processFile(File file) {
        if (scannedRadioButton.isSelected()) {
            int threshold = (Integer) thresholdSpinner.getValue();
            if (PdfUtils.isScannedPdf(file, threshold)) {
                resultFileList.add(file);
            }
        } else if (encryptedRadioButton.isSelected()) {
            if (PdfUtils.isEncryptedPdf(file)) {
                resultFileList.add(file);
            }
        } else if (nonOutlineRadioButton.isSelected()) {
            if (PdfUtils.isNonOutlinePdf(file)) {
                resultFileList.add(file);
            }
        } else if (hasAnnotationsRadioButton.isSelected()) {
            if (PdfUtils.hasAnnotations(file)) {
                resultFileList.add(file);
            }
        } else {
            logger.error("Invalid option selected");
        }
    }

    class MyMouseListener extends MouseAdapter {
        @Override
        public void mouseReleased(MouseEvent e) {
            super.mouseReleased(e);
            int r = resultTable.rowAtPoint(e.getPoint());
            if (r >= 0 && r < resultTable.getRowCount()) {
                if (!resultTable.isRowSelected(r)) {
                    resultTable.setRowSelectionInterval(r, r);
                }
            } else {
                resultTable.clearSelection();
            }
            int[] rowsIndex = resultTable.getSelectedRows();
            if (rowsIndex == null || rowsIndex.length == 0) {
                return;
            }
            if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
                JPopupMenu popupmenu = new JPopupMenu();
                MyMenuActionListener menuActionListener = new MyMenuActionListener();

                if (rowsIndex.length == 1) {
                    openDirMenuItem = new JMenuItem("Open parent folder of this file");
                    openDirMenuItem.addActionListener(menuActionListener);
                    popupmenu.add(openDirMenuItem);
                    popupmenu.addSeparator();
                }

                copyFilesMenuItem = new JMenuItem("Copy selected files to...");
                copyFilesMenuItem.addActionListener(menuActionListener);
                popupmenu.add(copyFilesMenuItem);

                popupmenu.show(e.getComponent(), e.getX(), e.getY());
            }
        }
    }

    class MyMenuActionListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent actionEvent) {
            Object source = actionEvent.getSource();
            if (source.equals(openDirMenuItem)) {
                onOpenDir();
            } else if (source.equals(copyFilesMenuItem)) {
                onCopyFiles();
            } else {
                logger.error("invalid source");
            }
        }

        private void onOpenDir() {
            int rowIndex = resultTable.getSelectedRow();
            String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
            File parent = new File(parentPath);
            RevealFileUtils.revealDirectory(parent);
        }

        private void onCopyFiles() {
            int[] selectedRows = resultTable.getSelectedRows();
            if (selectedRows.length == 0) {
                JOptionPane.showMessageDialog(PdfFinderPanel.this, "No rows selected", "Error", JOptionPane.ERROR_MESSAGE);
                return;
            }
            List<File> filesToCopy = new ArrayList<>();
            for (int rowIndex : selectedRows) {
                String filePath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_NAME).getModelIndex()).toString();
                String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
                File file = new File(parentPath, filePath);
                filesToCopy.add(file);
            }
            JFileChooser fileChooser = new JFileChooser();
            fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
            fileChooser.setDialogTitle("Select Target Directory");
            int returnValue = fileChooser.showOpenDialog(PdfFinderPanel.this);
            if (returnValue != JFileChooser.APPROVE_OPTION) {
                return;
            }
            File targetDir = fileChooser.getSelectedFile();
            for (File file : filesToCopy) {
                try {
                    org.apache.commons.io.FileUtils.copyFileToDirectory(file, targetDir);
                } catch (Exception e) {
                    logger.error("Copy file failed: " + file.getAbsolutePath(), e);
                }
            }
        }


    }

    class OperationButtonActionListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            Object source = e.getSource();
            if (source.equals(searchButton)) {
                searchButton.setEnabled(false);
                cancelButton.setEnabled(true);
                searchThread = new SearchThread(isRecursiveSearched.isSelected());
                searchThread.start();
            } else if (source.equals(cancelButton)) {
                searchButton.setEnabled(true);
                cancelButton.setEnabled(false);
                if (searchThread.isAlive()) {
                    searchThread.interrupt();
                    searchThread.executorService.shutdownNow();
                }
            }

        }
    }

    private void showResult() {
        SwingUtilities.invokeLater(() -> {
            int index = 0;
            for (File file : resultFileList) {
                index++;
                Vector<Object> rowData = getRowVector(index, file);
                resultTableModel.addRow(rowData);
            }
            tabbedPane.setSelectedIndex(1);
        });
    }

    private Vector<Object> getRowVector(int index, File file) {
        Vector<Object> rowData = new Vector<>();
        rowData.add(index);
        rowData.add(file.getParent());
        rowData.add(file.getName());
        rowData.add(FileUtils.sizeOfInHumanFormat(file));
        rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified()));
        return rowData;
    }

    class SearchThread extends Thread {
        public final ExecutorService executorService;
        private final AtomicInteger processedFiles = new AtomicInteger(0);
        private int totalFiles = 0;
        private final boolean isRecursiveSearched;

        public SearchThread(boolean isRecursiveSearched) {
            super();
            this.isRecursiveSearched = isRecursiveSearched;
            this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

            SwingUtilities.invokeLater(() -> {
                progressBar.setValue(0);
                progressBar.setString("Starting search...");
            });
        }

        @Override
        public void run() {
            try {
                resultFileList.clear();
                SwingUtilities.invokeLater(() -> resultTableModel.setRowCount(0));

                List<File> fileList = fileListPanel.getFileList();
                Set<File> fileSet = new TreeSet<>();
                String[] extensions = new String[]{"pdf", "PDF"};
                for (File file : fileList) {
                    fileSet.addAll(FileUtils.listFiles(file, extensions, isRecursiveSearched));
                }

                List<Future<?>> futures = new ArrayList<>();
                totalFiles = fileSet.size();
                updateProgress();

                for (File file : fileSet) {
                    if (Thread.currentThread().isInterrupted()) {
                        return;
                    }
                    futures.add(executorService.submit(() -> {
                        if (Thread.currentThread().isInterrupted()) {
                            return null;
                        }
                        processFile(file);
                        incrementProcessedFiles();
                        return null;
                    }));
                }

                // Wait for all tasks to complete
                for (Future<?> future : futures) {
                    try {
                        future.get();
                    } catch (InterruptedException e) {
                        logger.error("Search interrupted", e);
                        Thread.currentThread().interrupt(); // Restore interrupted status
                        return;
                    }
                }

                showResult();
            } catch (Exception e) {
                logger.error("Search failed", e);
                SwingUtilities.invokeLater(() -> progressBar.setString("Search failed"));
            } finally {
                executorService.shutdown();
                SwingUtilities.invokeLater(() -> {
                    searchButton.setEnabled(true);
                    cancelButton.setEnabled(false);
                });
            }
        }

        private void incrementProcessedFiles() {
            processedFiles.incrementAndGet();
            updateProgress();
        }

        private void updateProgress() {
            if (totalFiles > 0) {
                SwingUtilities.invokeLater(() -> {
                    int processed = processedFiles.get();
                    int percentage = (int) ((processed * 100.0) / totalFiles);
                    progressBar.setValue(percentage);
                    progressBar.setString(String.format("Processing: %d/%d files (%d%%)", processed, totalFiles, percentage));
                });
            }
        }
    }

    class RadioButtonItemListener implements ItemListener {
        @Override
        public void itemStateChanged(ItemEvent e) {
            thresholdSpinner.setEnabled(scannedRadioButton.isSelected());
        }
    }
}