Use NullOutputStream from utils.
[jSite.git] / src / main / java / de / todesbaum / jsite / gui / FileScanner.java
1 /*
2  * jSite - FileScanner.java - Copyright © 2006–2012 David Roden
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17  */
18
19 package de.todesbaum.jsite.gui;
20
21 import java.io.File;
22 import java.io.FileFilter;
23 import java.io.FileInputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.security.DigestOutputStream;
27 import java.security.MessageDigest;
28 import java.security.NoSuchAlgorithmException;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.List;
32 import java.util.logging.Level;
33 import java.util.logging.Logger;
34
35 import net.pterodactylus.util.io.Closer;
36 import net.pterodactylus.util.io.NullOutputStream;
37 import net.pterodactylus.util.io.StreamCopier;
38 import de.todesbaum.jsite.application.Project;
39 import de.todesbaum.jsite.i18n.I18n;
40
41 /**
42  * Scans the local path of a project anychronously and returns the list of found
43  * files as an event.
44  *
45  * @see Project#getLocalPath()
46  * @see FileScannerListener#fileScannerFinished(FileScanner)
47  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
48  */
49 public class FileScanner implements Runnable {
50
51         /** The logger. */
52         private final static Logger logger = Logger.getLogger(FileScanner.class.getName());
53
54         /** The list of listeners. */
55         private final List<FileScannerListener> fileScannerListeners = new ArrayList<FileScannerListener>();
56
57         /** The project to scan. */
58         private final Project project;
59
60         /** The list of found files. */
61         private List<ScannedFile> files;
62
63         /** Wether there was an error. */
64         private boolean error = false;
65
66         /**
67          * Creates a new file scanner for the given project.
68          *
69          * @param project
70          *            The project whose files to scan
71          */
72         public FileScanner(Project project) {
73                 this.project = project;
74         }
75
76         /**
77          * Adds the given listener to the list of listeners.
78          *
79          * @param fileScannerListener
80          *            The listener to add
81          */
82         public void addFileScannerListener(FileScannerListener fileScannerListener) {
83                 fileScannerListeners.add(fileScannerListener);
84         }
85
86         /**
87          * Removes the given listener from the list of listeners.
88          *
89          * @param fileScannerListener
90          *            The listener to remove
91          */
92         public void removeFileScannerListener(FileScannerListener fileScannerListener) {
93                 fileScannerListeners.remove(fileScannerListener);
94         }
95
96         /**
97          * Notifies all listeners that the file scan finished.
98          */
99         protected void fireFileScannerFinished() {
100                 for (FileScannerListener fileScannerListener : new ArrayList<FileScannerListener>(fileScannerListeners)) {
101                         fileScannerListener.fileScannerFinished(this);
102                 }
103         }
104
105         /**
106          * {@inheritDoc}
107          * <p>
108          * Scans all available files in the project’s local path and emits an event
109          * when finished.
110          *
111          * @see FileScannerListener#fileScannerFinished(FileScanner)
112          */
113         @Override
114         public void run() {
115                 files = new ArrayList<ScannedFile>();
116                 error = false;
117                 try {
118                         scanFiles(new File(project.getLocalPath()), files);
119                         Collections.sort(files);
120                 } catch (IOException ioe1) {
121                         error = true;
122                 }
123                 fireFileScannerFinished();
124         }
125
126         /**
127          * Returns whether there was an error scanning for files.
128          *
129          * @return <code>true</code> if there was an error, <code>false</code>
130          *         otherwise
131          */
132         public boolean isError() {
133                 return error;
134         }
135
136         /**
137          * Returns the list of found files.
138          *
139          * @return The list of found files
140          */
141         public List<ScannedFile> getFiles() {
142                 return files;
143         }
144
145         /**
146          * Recursively scans a directory and adds all found files to the given list.
147          *
148          * @param rootDir
149          *            The directory to scan
150          * @param fileList
151          *            The list to which to add the found files
152          * @throws IOException
153          *             if an I/O error occurs
154          */
155         private void scanFiles(File rootDir, List<ScannedFile> fileList) throws IOException {
156                 File[] files = rootDir.listFiles(new FileFilter() {
157
158                         @Override
159                         @SuppressWarnings("synthetic-access")
160                         public boolean accept(File file) {
161                                 return !project.isIgnoreHiddenFiles() || !file.isHidden();
162                         }
163                 });
164                 if (files == null) {
165                         throw new IOException(I18n.getMessage("jsite.file-scanner.can-not-read-directory"));
166                 }
167                 for (File file : files) {
168                         if (file.isDirectory()) {
169                                 scanFiles(file, fileList);
170                                 continue;
171                         }
172                         String filename = project.shortenFilename(file).replace('\\', '/');
173                         String hash = hashFile(project.getLocalPath(), filename);
174                         fileList.add(new ScannedFile(filename, hash));
175                 }
176         }
177
178         /**
179          * Hashes the given file.
180          *
181          * @param path
182          *            The path of the project
183          * @param filename
184          *            The name of the file, relative to the project path
185          * @return The hash of the file
186          */
187         private static String hashFile(String path, String filename) {
188                 InputStream fileInputStream = null;
189                 DigestOutputStream digestOutputStream = null;
190                 File file = new File(path, filename);
191                 try {
192                         fileInputStream = new FileInputStream(file);
193                         digestOutputStream = new DigestOutputStream(new NullOutputStream(), MessageDigest.getInstance("SHA-256"));
194                         StreamCopier.copy(fileInputStream, digestOutputStream, file.length());
195                         return toHex(digestOutputStream.getMessageDigest().digest());
196                 } catch (NoSuchAlgorithmException nsae1) {
197                         logger.log(Level.WARNING, "Could not get SHA-256 digest!", nsae1);
198                 } catch (IOException ioe1) {
199                         logger.log(Level.WARNING, "Could not read file!", ioe1);
200                 } finally {
201                         Closer.close(digestOutputStream);
202                         Closer.close(fileInputStream);
203                 }
204                 return toHex(new byte[32]);
205         }
206
207         /**
208          * Converts the given byte array into a hexadecimal string.
209          *
210          * @param array
211          *            The array to convert
212          * @return The hexadecimal string
213          */
214         private static String toHex(byte[] array) {
215                 StringBuilder hexString = new StringBuilder(array.length * 2);
216                 for (byte b : array) {
217                         hexString.append("0123456789abcdef".charAt((b >>> 4) & 0x0f)).append("0123456789abcdef".charAt(b & 0xf));
218                 }
219                 return hexString.toString();
220         }
221
222         /**
223          * Container for a scanned file, consisting of the name of the file and its
224          * hash.
225          *
226          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
227          */
228         public static class ScannedFile implements Comparable<ScannedFile> {
229
230                 /** The name of the file. */
231                 private final String filename;
232
233                 /** The hash of the file. */
234                 private final String hash;
235
236                 /**
237                  * Creates a new scanned file.
238                  *
239                  * @param filename
240                  *            The name of the file
241                  * @param hash
242                  *            The hash of the file
243                  */
244                 public ScannedFile(String filename, String hash) {
245                         this.filename = filename;
246                         this.hash = hash;
247                 }
248
249                 //
250                 // ACCESSORS
251                 //
252
253                 /**
254                  * Returns the name of the file.
255                  *
256                  * @return The name of the file
257                  */
258                 public String getFilename() {
259                         return filename;
260                 }
261
262                 /**
263                  * Returns the hash of the file.
264                  *
265                  * @return The hash of the file
266                  */
267                 public String getHash() {
268                         return hash;
269                 }
270
271                 //
272                 // OBJECT METHODS
273                 //
274
275                 /**
276                  * {@inheritDoc}
277                  */
278                 @Override
279                 public int hashCode() {
280                         return filename.hashCode();
281                 }
282
283                 /**
284                  * {@inheritDoc}
285                  */
286                 @Override
287                 public boolean equals(Object obj) {
288                         return filename.equals(obj);
289                 }
290
291                 /**
292                  * {@inheritDoc}
293                  */
294                 @Override
295                 public String toString() {
296                         return filename;
297                 }
298
299                 //
300                 // COMPARABLE METHODS
301                 //
302
303                 /**
304                  * {@inheritDoc}
305                  */
306                 @Override
307                 public int compareTo(ScannedFile scannedFile) {
308                         return filename.compareTo(scannedFile.filename);
309                 }
310
311         }
312
313 }