001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    package com.mucommander.ui.dialog.file;
020    
021    import com.mucommander.file.AbstractFile;
022    import com.mucommander.file.FileFactory;
023    import com.mucommander.file.util.FileSet;
024    import com.mucommander.file.util.PathUtils;
025    import com.mucommander.io.security.MuProvider;
026    import com.mucommander.job.CalculateChecksumJob;
027    import com.mucommander.text.Translator;
028    import com.mucommander.ui.action.CalculateChecksumAction;
029    import com.mucommander.ui.action.MuAction;
030    import com.mucommander.ui.dialog.DialogToolkit;
031    import com.mucommander.ui.layout.YBoxPanel;
032    import com.mucommander.ui.main.MainFrame;
033    import com.mucommander.ui.text.FilePathField;
034    import com.mucommander.util.StringUtils;
035    
036    import javax.swing.*;
037    import java.awt.*;
038    import java.awt.event.ActionEvent;
039    import java.awt.event.ActionListener;
040    import java.awt.event.ItemEvent;
041    import java.awt.event.ItemListener;
042    import java.io.IOException;
043    import java.security.MessageDigest;
044    import java.security.NoSuchAlgorithmException;
045    import java.security.Security;
046    import java.util.Comparator;
047    import java.util.Iterator;
048    import java.util.SortedSet;
049    import java.util.TreeSet;
050    
051    /**
052     * This dialog prepares a {@link com.mucommander.job.CalculateChecksumJob} and lets the user choose a checksum
053     * algorithm, and a destination for the checksum file.
054     *
055     * @author Maxence Bernard
056     */
057    public class CalculateChecksumDialog extends JobDialog implements ActionListener, ItemListener {
058    
059        private JComboBox algorithmComboBox;
060        private JRadioButton specificLocationRadioButton;
061        private JTextField specificLocationTextField;
062        private JButton okButton;
063    
064        /** An instance of all MessageDigest implementations */
065        private MessageDigest[] messageDigests;
066    
067        /** Default checksum algorithm (most commonly used) */
068        private final static String DEFAULT_ALGORITHM = "MD5";
069    
070        /** Last algorithm used, saved after validation of this dialog */
071        private static String lastUsedAlgorithm = DEFAULT_ALGORITHM;
072    
073        /** Dialog size constraints */
074        private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320,0);
075    
076    
077        static {
078            // Register additional MessageDigest implementations provided by the muCommander API
079            MuProvider.registerProvider();
080        }
081    
082    
083        public CalculateChecksumDialog(MainFrame mainFrame, FileSet files) {
084            super(mainFrame, MuAction.getStandardLabel(CalculateChecksumAction.class), files);
085    
086            YBoxPanel mainPanel = new YBoxPanel();
087    
088            // Retrieve all MessageDigest instances and sort them by alphabetical order of their algorithm
089    
090            // Create a TreeSet with a custom Comparator
091            SortedSet algorithmSortedSet = new TreeSet(new Comparator() {
092                        public int compare(Object o1, Object o2) {
093                            return ((MessageDigest)o1).getAlgorithm().compareTo(((MessageDigest)o2).getAlgorithm());
094                        }
095                    });
096    
097            // Add all MessageDigest to the TreeSet
098            Iterator algoIter = Security.getAlgorithms("MessageDigest").iterator();
099            while(algoIter.hasNext()) {
100                try {
101                    algorithmSortedSet.add(MessageDigest.getInstance((String)algoIter.next()));
102                }
103                catch(NoSuchAlgorithmException e) {
104                    // Should never happen and if it ever does, the digest will simply be discarded
105                }
106            }
107    
108            // Convert the sorted set into an array
109            messageDigests = new MessageDigest[algorithmSortedSet.size()];
110            algorithmSortedSet.toArray(messageDigests);
111    
112            // Add the sorted list of algorithms to a combo box to let the user choose one
113            algorithmComboBox = new JComboBox();
114            for(int i=0; i<messageDigests.length; i++)
115                algorithmComboBox.addItem(messageDigests[i].getAlgorithm());
116    
117            // Select the last used algorithm (if any), or the default algorithm
118            algorithmComboBox.setSelectedItem(lastUsedAlgorithm);
119            algorithmComboBox.addItemListener(this);
120    
121            FlowLayout flowLayout = new FlowLayout(FlowLayout.LEADING, 0, 0);
122            JPanel tempPanel = new JPanel(flowLayout);
123            tempPanel.add(new JLabel(Translator.get("calculate_checksum_dialog.checksum_algorithm")+" : "));
124            tempPanel.add(algorithmComboBox);
125    
126            mainPanel.add(tempPanel);
127            mainPanel.addSpace(10);
128    
129            // Create the components that allow to choose where the checksum file should be created
130    
131            mainPanel.add(new JLabel(Translator.get("destination")+" :"));
132            mainPanel.addSpace(5);
133    
134            JRadioButton tempLocationRadioButton = new JRadioButton(Translator.get("calculate_checksum_dialog.temporary_file"), true);
135            mainPanel.add(tempLocationRadioButton);
136    
137            specificLocationRadioButton = new JRadioButton("", false);
138            tempPanel = new JPanel(new BorderLayout());
139            tempPanel.add(specificLocationRadioButton, BorderLayout.WEST);
140            specificLocationRadioButton.addItemListener(this);
141            
142            // Create a path field with auto-completion capabilities
143            specificLocationTextField = new FilePathField(getChecksumFilename(lastUsedAlgorithm));
144            specificLocationTextField.setEnabled(false);
145            tempPanel.add(specificLocationTextField, BorderLayout.CENTER);
146    
147            JPanel tempPanel2 = new JPanel(new BorderLayout());
148            tempPanel2.add(tempPanel, BorderLayout.NORTH);
149            mainPanel.add(tempPanel2);
150    
151            ButtonGroup buttonGroup = new ButtonGroup();
152            buttonGroup.add(tempLocationRadioButton);
153            buttonGroup.add(specificLocationRadioButton);
154    
155            // Create file details button and OK/cancel buttons and lay them out a single row
156    
157            JPanel fileDetailsPanel = createFileDetailsPanel();
158    
159            okButton = new JButton(Translator.get("ok"));
160            JButton cancelButton = new JButton(Translator.get("cancel"));
161    
162            mainPanel.add(createButtonsPanel(createFileDetailsButton(fileDetailsPanel),
163                    DialogToolkit.createOKCancelPanel(okButton, cancelButton, getRootPane(), this)));
164    
165            mainPanel.add(fileDetailsPanel);
166    
167    //        mainPanel.add(new HelpButtonPanel(new HelpButton(mainFrame, "CalculateChecksum")));
168            
169            getContentPane().add(mainPanel);
170    
171            // Give initial keyboard focus to the 'Delete' button
172            setInitialFocusComponent(algorithmComboBox);
173    
174            // Call dispose() when dialog is closed
175            setDefaultCloseOperation(DISPOSE_ON_CLOSE);
176    
177            // Size dialog and show it to the screen
178            setMinimumSize(MINIMUM_DIALOG_DIMENSION);
179            setResizable(true);
180            showDialog();
181        }
182    
183        /**
184         * Returns the MessageDigest instance corresponding to the currently selected algorithm.
185         *
186         * @return the MessageDigest instance corresponding to the currently selected algorithm.
187         */
188        private MessageDigest getSelectedMessageDigest() {
189            return messageDigests[algorithmComboBox.getSelectedIndex()];
190        }
191    
192        /**
193         * Returns a de-facto standard filename for the specified checksum algorithm, e.g. <code>MD5SUMS</code> for
194         * <code>md5</code>.
195         *
196         * @param algorithm a checksum algorithm
197         * @return a standard filename for the specified checksum algorithm
198         */
199        private String getChecksumFilename(String algorithm) {
200            // Adler32 -> ADLER32SUMS
201            // CRC32   -> <filename>.sfv    (needs special treatment)
202            // MD2     -> MD2SUMS
203            // MD4     -> MD4SUMS
204            // MD5     -> MD5SUMS
205            // SHA     -> SHA1SUMS          (needs special treatment)
206            // SHA-256 -> SHA256SUMS
207            // SHA-384 -> SHA384SUMS
208            // SHA-512 -> SHA512SUMS
209    
210            algorithm = algorithm.toUpperCase();
211    
212            if(algorithm.equals("SHA"))
213                return "SHA1SUMS";
214    
215            if(algorithm.equals("CRC32"))
216                return (files.size()==1?files.fileAt(0):files.getBaseFolder()).getName()+".sfv";
217    
218            return StringUtils.replaceCompat(algorithm, "-", "")+"SUMS";
219        }
220    
221    
222        ///////////////////////////////////
223        // ActionListener implementation //
224        ///////////////////////////////////
225    
226        public void actionPerformed(ActionEvent e) {
227            // Start by disposing this dialog
228            dispose();
229    
230            if(e.getSource()==okButton) {
231                try {
232                    MessageDigest digest = getSelectedMessageDigest();
233                    String algorithm = digest.getAlgorithm();
234                    AbstractFile checksumFile;
235    
236                    // Resolve the destination checksum file
237    
238                    if(specificLocationRadioButton.isSelected()) {
239                        // User-defined checksum file
240                        String enteredPath = specificLocationTextField.getText();
241    
242                        PathUtils.ResolvedDestination resolvedDest = PathUtils.resolveDestination(enteredPath, mainFrame.getActiveTable().getCurrentFolder());
243                        // The path entered doesn't correspond to any existing folder
244                        if (resolvedDest==null) {
245                            showErrorDialog(Translator.get("invalid_path", enteredPath));
246                            return;
247                        }
248    
249                        if(resolvedDest.getDestinationType()==PathUtils.ResolvedDestination.EXISTING_FOLDER)
250                            checksumFile = resolvedDest.getDestinationFile().getDirectChild(getChecksumFilename(algorithm));
251                        else
252                            checksumFile = resolvedDest.getDestinationFile();
253                    }
254                    else {
255                        // Temporary file
256                        checksumFile = FileFactory.getTemporaryFile(getChecksumFilename(algorithm), true);
257                    }
258    
259                    // Save the algorithm that was used for the next time this dialog is invoked
260                    lastUsedAlgorithm = algorithm; 
261    
262                    // Start processing files
263                    ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("properties_dialog.calculating"));
264                    CalculateChecksumJob job = new CalculateChecksumJob(progressDialog, mainFrame, files, checksumFile, digest);
265                    progressDialog.start(job);
266                }
267                catch(IOException ex) {
268                    // Note: FileFactory.getTemporaryFile() should never throw an IOException
269    
270                    showErrorDialog(Translator.get("invalid_path", specificLocationTextField.getText()));
271                    return;
272                }
273            }
274        }
275    
276    
277        /////////////////////////////////
278        // ItemListener implementation //
279        /////////////////////////////////
280    
281        public void itemStateChanged(ItemEvent e) {
282            Object source = e.getSource();
283    
284            if(source==specificLocationRadioButton) {
285                // Enables/disables the text field when the corresponding radio button's selected state has changed.
286                specificLocationTextField.setEnabled(specificLocationRadioButton.isSelected());
287                specificLocationTextField.requestFocus();
288            }
289            else if(source==algorithmComboBox) {
290                specificLocationTextField.setText(getChecksumFilename(getSelectedMessageDigest().getAlgorithm()));
291            }
292        }
293    }