View Javadoc
1   package edu.jiangxin.apktoolbox.pdf.stat;
2   
3   import edu.jiangxin.apktoolbox.pdf.PdfUtils;
4   import edu.jiangxin.apktoolbox.swing.extend.EasyPanel;
5   import edu.jiangxin.apktoolbox.swing.extend.FileListPanel;
6   import edu.jiangxin.apktoolbox.utils.Constants;
7   import edu.jiangxin.apktoolbox.utils.DateUtils;
8   import edu.jiangxin.apktoolbox.utils.ExcelExporter;
9   import edu.jiangxin.apktoolbox.utils.FileUtils;
10  
11  import javax.swing.*;
12  import javax.swing.table.DefaultTableModel;
13  import java.awt.*;
14  import java.awt.event.ActionEvent;
15  import java.awt.event.ActionListener;
16  import java.awt.event.MouseAdapter;
17  import java.awt.event.MouseEvent;
18  import java.io.File;
19  import java.io.Serial;
20  import java.util.*;
21  import java.util.List;
22  import java.util.concurrent.CopyOnWriteArrayList;
23  import java.util.concurrent.ExecutorService;
24  import java.util.concurrent.Executors;
25  import java.util.concurrent.Future;
26  import java.util.concurrent.atomic.AtomicInteger;
27  import java.util.concurrent.atomic.AtomicLong;
28  
29  public class PdfStatPanel extends EasyPanel {
30  
31      @Serial
32      private static final long serialVersionUID = 1L;
33  
34      private JTabbedPane tabbedPane;
35  
36      private JPanel mainPanel;
37  
38      private FileListPanel fileListPanel;
39  
40      private JRadioButton pageCountRadioButton;
41  
42      private JCheckBox isRecursiveSearched;
43  
44      private JPanel resultPanel;
45  
46      private JTable resultTable;
47  
48      private DefaultTableModel resultTableModel;
49  
50      private JLabel statInfoLabel;
51  
52      private JButton statButton;
53      private JButton cancelButton;
54  
55      private JProgressBar progressBar;
56  
57      private transient SearchThread searchThread;
58  
59      private final AtomicLong totalFileSize = new AtomicLong(0);
60  
61      private final AtomicInteger totalPageCount = new AtomicInteger(0);
62  
63      private transient final List<Vector<Object>> resultFileList = new CopyOnWriteArrayList<>();
64  
65  
66      @Override
67      public void initUI() {
68          tabbedPane = new JTabbedPane();
69          add(tabbedPane);
70  
71          createMainPanel();
72          tabbedPane.addTab("Option", null, mainPanel, "Show Stat Options");
73  
74          createResultPanel();
75          tabbedPane.addTab("Result", null, resultPanel, "Show Stat Result");
76      }
77  
78      private void createMainPanel() {
79          mainPanel = new JPanel();
80          mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
81  
82          fileListPanel = new FileListPanel();
83          fileListPanel.initialize();
84  
85          JPanel checkOptionPanel = new JPanel();
86          checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS));
87          checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options"));
88  
89          ButtonGroup buttonGroup = new ButtonGroup();
90  
91          pageCountRadioButton = new JRadioButton("统计页数");
92          pageCountRadioButton.setSelected(true);
93          pageCountRadioButton.setEnabled(false);
94          buttonGroup.add(pageCountRadioButton);
95  
96          JPanel typePanel = new JPanel();
97          typePanel.setLayout(new FlowLayout(FlowLayout.LEFT,10,3));
98          typePanel.add(pageCountRadioButton);
99  
100         checkOptionPanel.add(typePanel);
101         checkOptionPanel.add(Box.createHorizontalGlue());
102 
103         JPanel searchOptionPanel = new JPanel();
104         searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS));
105         searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Stat Options"));
106 
107         isRecursiveSearched = new JCheckBox("Recursive");
108         isRecursiveSearched.setSelected(true);
109         searchOptionPanel.add(isRecursiveSearched);
110         searchOptionPanel.add(Box.createHorizontalGlue());
111 
112         JPanel operationPanel = new JPanel();
113         operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS));
114         operationPanel.setBorder(BorderFactory.createTitledBorder("Operations"));
115 
116         JPanel buttonPanel = new JPanel();
117         buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
118 
119         statButton = new JButton("Stat");
120         cancelButton = new JButton("Cancel");
121         cancelButton.setEnabled(false);
122         statButton.addActionListener(new OperationButtonActionListener());
123         cancelButton.addActionListener(new OperationButtonActionListener());
124         operationPanel.add(statButton);
125         operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
126         operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
127         operationPanel.add(cancelButton);
128         operationPanel.add(Box.createHorizontalGlue());
129 
130         progressBar = new JProgressBar();
131         progressBar.setStringPainted(true);
132         progressBar.setString("Ready");
133 
134         mainPanel.add(fileListPanel);
135         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
136         mainPanel.add(checkOptionPanel);
137         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
138         mainPanel.add(searchOptionPanel);
139         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
140         mainPanel.add(operationPanel);
141         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
142         mainPanel.add(progressBar);
143     }
144 
145     private void createResultPanel() {
146         resultPanel = new JPanel();
147         resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS));
148 
149         resultTableModel = new PdfFilesTableModel(new Vector<>(), PdfFilesConstants.COLUMN_NAMES);
150         resultTable = new JTable(resultTableModel);
151         resultTable.setDefaultRenderer(Vector.class, new PdfFilesTableCellRenderer());
152         for (int i = 0; i < resultTable.getColumnCount(); i++) {
153             resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new PdfFilesTableCellRenderer());
154         }
155         resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
156         resultTable.addMouseListener(new MouseAdapter() {
157             @Override
158             public void mouseReleased(MouseEvent e) {
159                 if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
160                     JPopupMenu popupmenu = new JPopupMenu();
161                     JMenuItem exportMenuItem = new JMenuItem("导出到 Excel");
162                     exportMenuItem.addActionListener(ev ->
163                         ExcelExporter.export(resultTableModel, "pdf_stat_export.xlsx", PdfStatPanel.this));
164                     popupmenu.add(exportMenuItem);
165                     popupmenu.show(e.getComponent(), e.getX(), e.getY());
166                 }
167             }
168         });
169         JScrollPane scrollPane = new JScrollPane(resultTable);
170 
171         JPanel statInfoPanel = new JPanel();
172         statInfoPanel.setLayout(new BoxLayout(statInfoPanel, BoxLayout.X_AXIS));
173         statInfoLabel = new JLabel("");
174         statInfoPanel.add(statInfoLabel);
175         statInfoPanel.add(Box.createHorizontalGlue());
176 
177         resultPanel.add(scrollPane);
178         resultPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
179         resultPanel.add(statInfoPanel);
180     }
181 
182     private void processFile(File file) {
183         if (pageCountRadioButton.isSelected()) {
184             Vector<Object> fileVector = getRowVector(file);
185             resultFileList.add(fileVector);
186         } else {
187             logger.error("Invalid option selected");
188         }
189     }
190 
191     class OperationButtonActionListener implements ActionListener {
192         @Override
193         public void actionPerformed(ActionEvent e) {
194             Object source = e.getSource();
195             if (source.equals(statButton)) {
196                 statButton.setEnabled(false);
197                 cancelButton.setEnabled(true);
198                 searchThread = new SearchThread(isRecursiveSearched.isSelected());
199                 searchThread.start();
200             } else if (source.equals(cancelButton)) {
201                 statButton.setEnabled(true);
202                 cancelButton.setEnabled(false);
203                 if (searchThread.isAlive()) {
204                     searchThread.interrupt();
205                     searchThread.executorService.shutdownNow();
206                 }
207             }
208 
209         }
210     }
211 
212     private void showResult() {
213         SwingUtilities.invokeLater(() -> {
214             int index = 0;
215             for (Vector<Object> file : resultFileList) {
216                 file.add(0, ++index);
217                 resultTableModel.addRow(file);
218             }
219             tabbedPane.setSelectedIndex(1);
220             statInfoLabel.setText("Page Count: " + totalPageCount.get() + ", Total Size: " + FileUtils.sizeOfInHumanFormat(totalFileSize.get()));
221         });
222     }
223 
224     private Vector<Object> getRowVector(File file) {
225         Vector<Object> rowData = new Vector<>();
226         rowData.add(file.getParent());
227         rowData.add(file.getName());
228         totalFileSize.addAndGet(file.length());
229         rowData.add(FileUtils.sizeOfInHumanFormat(file));
230         rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified()));
231         int pageCount = PdfUtils.getPageCount(file);
232         totalPageCount.addAndGet(pageCount);
233         rowData.add(pageCount);
234         return rowData;
235     }
236 
237     class SearchThread extends Thread {
238         public final ExecutorService executorService;
239         private final AtomicInteger processedFiles = new AtomicInteger(0);
240         private int totalFiles = 0;
241         private final boolean isRecursiveSearched;
242 
243         public SearchThread(boolean isRecursiveSearched) {
244             super();
245             this.isRecursiveSearched = isRecursiveSearched;
246             this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
247 
248             resultFileList.clear();
249             totalFileSize.set(0);
250             totalPageCount.set(0);
251 
252             SwingUtilities.invokeLater(() -> {
253                 progressBar.setValue(0);
254                 progressBar.setString("Starting search...");
255                 resultTableModel.setRowCount(0);
256                 statInfoLabel.setText("");
257             });
258         }
259 
260         @Override
261         public void run() {
262             try {
263                 List<File> fileList = fileListPanel.getFileList();
264                 Set<File> fileSet = new TreeSet<>();
265                 String[] extensions = new String[]{"pdf", "PDF"};
266                 for (File file : fileList) {
267                     fileSet.addAll(FileUtils.listFiles(file, extensions, isRecursiveSearched));
268                 }
269 
270                 List<Future<?>> futures = new ArrayList<>();
271                 totalFiles = fileSet.size();
272                 updateProgress();
273 
274                 for (File file : fileSet) {
275                     if (currentThread().isInterrupted()) {
276                         return;
277                     }
278                     futures.add(executorService.submit(() -> {
279                         if (currentThread().isInterrupted()) {
280                             return null;
281                         }
282                         processFile(file);
283                         incrementProcessedFiles();
284                         return null;
285                     }));
286                 }
287 
288                 // Wait for all tasks to complete
289                 for (Future<?> future : futures) {
290                     try {
291                         future.get();
292                     } catch (InterruptedException e) {
293                         logger.error("Search interrupted", e);
294                         currentThread().interrupt(); // Restore interrupted status
295                         return;
296                     }
297                 }
298 
299                 showResult();
300             } catch (Exception e) {
301                 logger.error("Search failed", e);
302                 SwingUtilities.invokeLater(() -> progressBar.setString("Search failed"));
303             } finally {
304                 executorService.shutdown();
305                 SwingUtilities.invokeLater(() -> {
306                     statButton.setEnabled(true);
307                     cancelButton.setEnabled(false);
308                 });
309             }
310         }
311 
312         private void incrementProcessedFiles() {
313             processedFiles.incrementAndGet();
314             updateProgress();
315         }
316 
317         private void updateProgress() {
318             if (totalFiles > 0) {
319                 SwingUtilities.invokeLater(() -> {
320                     int processed = processedFiles.get();
321                     int percentage = (int) (processed * 100.0 / totalFiles);
322                     progressBar.setValue(percentage);
323                     progressBar.setString(String.format("Processing: %d/%d files (%d%%)", processed, totalFiles, percentage));
324                 });
325             }
326         }
327     }
328 }