View Javadoc
1   package edu.jiangxin.apktoolbox.file.duplicate;
2   
3   import edu.jiangxin.apktoolbox.utils.DateUtils;
4   import edu.jiangxin.apktoolbox.swing.extend.FileListPanel;
5   import edu.jiangxin.apktoolbox.swing.extend.EasyPanel;
6   import edu.jiangxin.apktoolbox.utils.Constants;
7   import edu.jiangxin.apktoolbox.utils.FileUtils;
8   import org.apache.commons.codec.digest.DigestUtils;
9   import org.apache.commons.io.FilenameUtils;
10  import org.apache.commons.lang3.StringUtils;
11  
12  import javax.swing.*;
13  import javax.swing.table.DefaultTableModel;
14  import java.awt.*;
15  import java.awt.event.ActionEvent;
16  import java.awt.event.ActionListener;
17  import java.awt.event.MouseAdapter;
18  import java.awt.event.MouseEvent;
19  import java.io.*;
20  import java.util.List;
21  import java.util.*;
22  
23  public class DuplicateSearchPanel extends EasyPanel {
24  
25      private static final long serialVersionUID = 1L;
26  
27      private JTabbedPane tabbedPane;
28  
29      private JPanel optionPanel;
30  
31      private FileListPanel fileListPanel;
32  
33      private JCheckBox isSizeChecked;
34      private JCheckBox isFileNameChecked;
35      private JCheckBox isMD5Checked;
36      private JCheckBox isModifiedTimeChecked;
37  
38      private JCheckBox isHiddenFileSearched;
39      private JCheckBox isRecursiveSearched;
40      private JTextField suffixTextField;
41  
42      private JPanel resultPanel;
43  
44      private JTable resultTable;
45  
46      private DefaultTableModel resultTableModel;
47  
48      private JButton searchButton;
49      private JButton cancelButton;
50  
51      private JMenuItem openDirMenuItem;
52      private JMenuItem deleteFileMenuItem;
53      private JMenuItem deleteFilesInSameDirMenuItem;
54      private JMenuItem deleteFilesInSameDirRecursiveMenuItem;
55  
56      private Thread searchThread;
57  
58      final private Map<String, List<File>> duplicateFileGroupMap = new HashMap<>();
59  
60      @Override
61      public void initUI() {
62          tabbedPane = new JTabbedPane();
63          add(tabbedPane);
64  
65          createOptionPanel();
66          tabbedPane.addTab("Option", null, optionPanel, "Show Search Options");
67  
68          createResultPanel();
69          tabbedPane.addTab("Result", null, resultPanel, "Show Search Result");
70      }
71  
72      private void createOptionPanel() {
73          optionPanel = new JPanel();
74          optionPanel.setLayout(new BoxLayout(optionPanel, BoxLayout.Y_AXIS));
75  
76          fileListPanel = new FileListPanel();
77  
78          JPanel checkOptionPanel = new JPanel();
79          checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS));
80          checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options"));
81  
82          isSizeChecked = new JCheckBox("Size");
83          isSizeChecked.setSelected(true);
84          isFileNameChecked = new JCheckBox("Filename");
85          isMD5Checked = new JCheckBox("MD5");
86          isModifiedTimeChecked = new JCheckBox("Last Modified Time");
87          checkOptionPanel.add(isSizeChecked);
88          checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
89          checkOptionPanel.add(isFileNameChecked);
90          checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
91          checkOptionPanel.add(isMD5Checked);
92          checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
93          checkOptionPanel.add(isModifiedTimeChecked);
94          checkOptionPanel.add(Box.createHorizontalGlue());
95  
96          JPanel searchOptionPanel = new JPanel();
97          searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS));
98          searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Search Options"));
99  
100         isHiddenFileSearched = new JCheckBox("Hidden Files");
101         isRecursiveSearched = new JCheckBox("Recursive");
102         isRecursiveSearched.setSelected(true);
103         JLabel suffixLabel = new JLabel("Suffix: ");
104         suffixTextField = new JTextField();
105         suffixTextField.setToolTipText("an array of extensions, ex. {\"java\",\"xml\"}. If this parameter is empty, all files are returned.");
106         searchOptionPanel.add(isHiddenFileSearched);
107         searchOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
108         searchOptionPanel.add(isRecursiveSearched);
109         searchOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
110         searchOptionPanel.add(suffixLabel);
111         searchOptionPanel.add(suffixTextField);
112         searchOptionPanel.add(Box.createHorizontalGlue());
113 
114         JPanel operationPanel = new JPanel();
115         operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS));
116         operationPanel.setBorder(BorderFactory.createTitledBorder("Operations"));
117 
118         searchButton = new JButton("Search");
119         cancelButton = new JButton("Cancel");
120         searchButton.addActionListener(new OperationButtonActionListener());
121         cancelButton.addActionListener(new OperationButtonActionListener());
122         operationPanel.add(searchButton);
123         operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
124         operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
125         operationPanel.add(cancelButton);
126         operationPanel.add(Box.createHorizontalGlue());
127 
128         optionPanel.add(fileListPanel);
129         optionPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
130         optionPanel.add(checkOptionPanel);
131         optionPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
132         optionPanel.add(searchOptionPanel);
133         optionPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
134         optionPanel.add(operationPanel);
135     }
136 
137     private void createResultPanel() {
138         resultPanel = new JPanel();
139         resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS));
140 
141         resultTableModel = new DuplicateFilesTableModel(new Vector<>(), DuplicateFilesConstants.COLUMN_NAMES);
142         resultTable = new JTable(resultTableModel);
143 
144         resultTable.setDefaultRenderer(Vector.class, new DuplicateFilesTableCellRenderer());
145 
146         for (int i = 0; i < resultTable.getColumnCount(); i++) {
147             resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new DuplicateFilesTableCellRenderer());
148         }
149 
150         resultTable.addMouseListener(new MyMouseListener());
151 
152         resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
153 
154         JScrollPane scrollPane = new JScrollPane(resultTable);
155         resultPanel.add(scrollPane);
156     }
157 
158     public String getComparedKey(File file) {
159         StringBuilder sb = new StringBuilder();
160         if (isSizeChecked.isSelected()) {
161             sb.append("[Size][");
162             sb.append(DigestUtils.md5Hex(String.valueOf(file.length())));
163             sb.append("]");
164         }
165         if (isFileNameChecked.isSelected()) {
166             sb.append("[Filename][");
167             sb.append(DigestUtils.md5Hex(file.getName()));
168             sb.append("]");
169         }
170         if (isMD5Checked.isSelected()) {
171             sb.append("[MD5][");
172             try (InputStream is = new FileInputStream(file)) {
173                 sb.append(DigestUtils.md5Hex(is));
174             } catch (FileNotFoundException e) {
175                 logger.error("getComparedKey FileNotFoundException");
176             } catch (IOException e) {
177                 logger.error("getComparedKey IOException");
178             }
179             sb.append("]");
180         }
181         if (isModifiedTimeChecked.isSelected()) {
182             sb.append("[ModifiedTime][");
183             sb.append(DigestUtils.md5Hex(String.valueOf(file.lastModified())));
184             sb.append("]");
185         }
186         logger.info("path: " + file.getAbsolutePath() + ", key: " + sb);
187         return sb.toString();
188     }
189 
190     class MyMouseListener extends MouseAdapter {
191         @Override
192         public void mouseReleased(MouseEvent e) {
193             super.mouseReleased(e);
194             int r = resultTable.rowAtPoint(e.getPoint());
195             if (r >= 0 && r < resultTable.getRowCount()) {
196                 resultTable.setRowSelectionInterval(r, r);
197             } else {
198                 resultTable.clearSelection();
199             }
200             int rowIndex = resultTable.getSelectedRow();
201             if (rowIndex < 0) {
202                 return;
203             }
204             if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
205                 JPopupMenu popupmenu = new JPopupMenu();
206                 MyMenuActionListener menuActionListener = new MyMenuActionListener();
207 
208                 openDirMenuItem = new JMenuItem("Open parent folder of this file");
209                 openDirMenuItem.addActionListener(menuActionListener);
210                 popupmenu.add(openDirMenuItem);
211 
212                 deleteFileMenuItem = new JMenuItem("Delete this duplicate file");
213                 deleteFileMenuItem.addActionListener(menuActionListener);
214                 popupmenu.add(deleteFileMenuItem);
215 
216                 deleteFilesInSameDirMenuItem = new JMenuItem("Delete these duplicate files in the same directory");
217                 deleteFilesInSameDirMenuItem.addActionListener(menuActionListener);
218                 popupmenu.add(deleteFilesInSameDirMenuItem);
219 
220                 deleteFilesInSameDirRecursiveMenuItem = new JMenuItem("Delete these duplicate files in the same directory(Recursive)");
221                 deleteFilesInSameDirRecursiveMenuItem.addActionListener(menuActionListener);
222                 popupmenu.add(deleteFilesInSameDirRecursiveMenuItem);
223 
224                 popupmenu.show(e.getComponent(), e.getX(), e.getY());
225             }
226         }
227     }
228 
229     class MyMenuActionListener implements ActionListener {
230         @Override
231         public void actionPerformed(ActionEvent actionEvent) {
232             Object source = actionEvent.getSource();
233             if (source.equals(openDirMenuItem)) {
234                 onOpenDir();
235             } else if (source.equals(deleteFileMenuItem)) {
236                 onDeleteFile();
237             } else if (source.equals(deleteFilesInSameDirMenuItem)) {
238                 onDeleteFilesInSameDir();
239             } else if (source.equals(deleteFilesInSameDirRecursiveMenuItem)) {
240                 onDeleteFilesInSameDirRecursive();
241             } else {
242                 logger.error("invalid source");
243             }
244         }
245 
246         private void onOpenDir() {
247             int rowIndex = resultTable.getSelectedRow();
248             String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
249             File parent = new File(parentPath);
250             if (parent.isDirectory()) {
251                 try {
252                     Desktop.getDesktop().open(parent);
253                 } catch (IOException e) {
254                     logger.error("open parent failed: " + parent.getPath());
255                 }
256             }
257         }
258 
259         private void onDeleteFile() {
260             int rowIndex = resultTable.getSelectedRow();
261             String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
262             String name = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_NAME).getModelIndex()).toString();
263             File selectedFile = new File(parentPath, name);
264             String key = getComparedKey(selectedFile);
265             List<File> files = duplicateFileGroupMap.get(key);
266             for (File file : files) {
267                 if (!selectedFile.equals(file)) {
268                     continue;
269                 }
270                 files.remove(file);
271                 boolean isSuccessful = file.delete();
272                 logger.info("delete file: " + file.getAbsolutePath() + ", result: " + isSuccessful);
273                 break;
274             }
275             resultTableModel.setRowCount(0);
276             showResult();
277         }
278 
279         private void onDeleteFilesInSameDir() {
280             int rowIndex = resultTable.getSelectedRow();
281             String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
282             for (Map.Entry<String, List<File>> entry : duplicateFileGroupMap.entrySet()) {
283                 List<File> duplicateFileGroup = entry.getValue();
284                 for (File duplicateFile : duplicateFileGroup) {
285                     String parentPathTmp = duplicateFile.getParent();
286                     if (Objects.equals(parentPath, parentPathTmp)) {
287                         duplicateFileGroup.remove(duplicateFile);
288                         boolean isSuccessful = duplicateFile.delete();
289                         logger.info("delete file: " + duplicateFile.getAbsolutePath() + ", result: " + isSuccessful);
290                         break;
291                     }
292                 }
293             }
294             resultTableModel.setRowCount(0);
295             showResult();
296         }
297 
298         private void onDeleteFilesInSameDirRecursive() {
299             int rowIndex = resultTable.getSelectedRow();
300             String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
301             for (Map.Entry<String, List<File>> entry : duplicateFileGroupMap.entrySet()) {
302                 List<File> duplicateFileGroup = entry.getValue();
303                 for (File duplicateFile : duplicateFileGroup) {
304                     String parentPathTmp = duplicateFile.getParent();
305                     if (Objects.equals(parentPath, parentPathTmp) || FilenameUtils.directoryContains(parentPath, parentPathTmp)) {
306                         duplicateFileGroup.remove(duplicateFile);
307                         boolean isSuccessful = duplicateFile.delete();
308                         logger.info("delete file: " + duplicateFile.getAbsolutePath() + ", result: " + isSuccessful);
309                         break;
310                     }
311                 }
312             }
313             resultTableModel.setRowCount(0);
314             showResult();
315         }
316     }
317 
318     class OperationButtonActionListener implements ActionListener {
319         @Override
320         public void actionPerformed(ActionEvent e) {
321             Object source = e.getSource();
322             if (source.equals(searchButton)) {
323                 String[] extensions = null;
324                 if (StringUtils.isNotEmpty(suffixTextField.getText())) {
325                     extensions = suffixTextField.getText().split(",");
326                 }
327                 searchThread = new SearchThread(extensions, isRecursiveSearched.isSelected(), isHiddenFileSearched.isSelected(), duplicateFileGroupMap);
328                 searchThread.start();
329             } else if (source.equals(cancelButton)) {
330                 if (searchThread.isAlive()) {
331                     searchThread.interrupt();
332                 }
333             }
334 
335         }
336     }
337 
338     private void showResult() {
339         SwingUtilities.invokeLater(() -> {
340             int groupIndex = 0;
341             for (Map.Entry<String, List<File>> entry : duplicateFileGroupMap.entrySet()) {
342                 List<File> duplicateFileGroup = entry.getValue();
343                 if (duplicateFileGroup.size() < 2) {
344                     continue;
345                 }
346                 groupIndex++;
347                 for (File duplicateFile : duplicateFileGroup) {
348                     Vector<Object> rowData = getRowVector(groupIndex, duplicateFile);
349                     resultTableModel.addRow(rowData);
350                 }
351             }
352             tabbedPane.setSelectedIndex(1);
353         });
354     }
355 
356     private Vector<Object> getRowVector(int groupIndex, File file) {
357         Vector<Object> rowData = new Vector<>();
358         rowData.add(groupIndex);
359         rowData.add(file.getParent());
360         rowData.add(file.getName());
361         rowData.add(FilenameUtils.getExtension(file.getName()));
362         rowData.add(FileUtils.sizeOfInHumanFormat(file));
363         rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified()));
364         return rowData;
365     }
366 
367     class SearchThread extends Thread {
368         private String[] extensions;
369         private boolean isRecursiveSearched;
370         private boolean isHiddenFileSearched;
371         private Map<String, List<File>> duplicateFileGroupMap;
372 
373         public SearchThread(String[] extensions, boolean isRecursiveSearched, boolean isHiddenFileSearched, Map<String, List<File>> duplicateFileGroupMap) {
374             super();
375             this.extensions = extensions;
376             this.isRecursiveSearched = isRecursiveSearched;
377             this.isHiddenFileSearched = isHiddenFileSearched;
378             this.duplicateFileGroupMap = duplicateFileGroupMap;
379         }
380 
381         @Override
382         public void run() {
383             super.run();
384             duplicateFileGroupMap.clear();
385             List<File> fileList = fileListPanel.getFileList();
386 
387             Set<File> fileSet = new TreeSet<>(fileList);
388             for (File file : fileList) {
389                 fileSet.addAll(org.apache.commons.io.FileUtils.listFiles(file, extensions, isRecursiveSearched));
390             }
391 
392             for (File file : fileSet) {
393                 if (Thread.currentThread().isInterrupted()) {
394                     break;
395                 }
396                 if (file.isHidden() && !isHiddenFileSearched) {
397                     continue;
398                 }
399                 String hash = getComparedKey(file);
400                 List<File> list = duplicateFileGroupMap.computeIfAbsent(hash, k -> new LinkedList<>());
401                 list.add(file);
402             }
403             showResult();
404         }
405     }
406 }