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