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 }