bd98ed078d3aaf11a684d3f895192f9af14a8d1e
[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.io.OutputStream;
27 import java.security.DigestOutputStream;
28 import java.security.MessageDigest;
29 import java.security.NoSuchAlgorithmException;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35
36 import net.pterodactylus.util.io.Closer;
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         @SuppressWarnings("synthetic-access")
188         private static String hashFile(String path, String filename) {
189                 InputStream fileInputStream = null;
190                 DigestOutputStream digestOutputStream = null;
191                 File file = new File(path, filename);
192                 try {
193                         fileInputStream = new FileInputStream(file);
194                         digestOutputStream = new DigestOutputStream(new NullOutputStream(), MessageDigest.getInstance("SHA-256"));
195                         StreamCopier.copy(fileInputStream, digestOutputStream, file.length());
196                         return toHex(digestOutputStream.getMessageDigest().digest());
197                 } catch (NoSuchAlgorithmException nsae1) {
198                         logger.log(Level.WARNING, "Could not get SHA-256 digest!", nsae1);
199                 } catch (IOException ioe1) {
200                         logger.log(Level.WARNING, "Could not read file!", ioe1);
201                 } finally {
202                         Closer.close(digestOutputStream);
203                         Closer.close(fileInputStream);
204                 }
205                 return toHex(new byte[32]);
206         }
207
208         /**
209          * Converts the given byte array into a hexadecimal string.
210          *
211          * @param array
212          *            The array to convert
213          * @return The hexadecimal string
214          */
215         private static String toHex(byte[] array) {
216                 StringBuilder hexString = new StringBuilder(array.length * 2);
217                 for (byte b : array) {
218                         hexString.append("0123456789abcdef".charAt((b >>> 4) & 0x0f)).append("0123456789abcdef".charAt(b & 0xf));
219                 }
220                 return hexString.toString();
221         }
222
223         /**
224          * {@link OutputStream} that discards all written bytes.
225          *
226          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
227          */
228         private static class NullOutputStream extends OutputStream {
229
230                 /**
231                  * {@inheritDoc}
232                  */
233                 @Override
234                 public void write(int b) {
235                         /* do nothing. */
236                 }
237
238                 /**
239                  * {@inheritDoc}
240                  */
241                 @Override
242                 public void write(byte[] b) {
243                         /* do nothing. */
244                 }
245
246                 /**
247                  * {@inheritDoc}
248                  */
249                 @Override
250                 public void write(byte[] b, int off, int len) {
251                         /* do nothing. */
252                 }
253
254         }
255
256         /**
257          * Container for a scanned file, consisting of the name of the file and its
258          * hash.
259          *
260          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
261          */
262         public static class ScannedFile implements Comparable<ScannedFile> {
263
264                 /** The name of the file. */
265                 private final String filename;
266
267                 /** The hash of the file. */
268                 private final String hash;
269
270                 /**
271                  * Creates a new scanned file.
272                  *
273                  * @param filename
274                  *            The name of the file
275                  * @param hash
276                  *            The hash of the file
277                  */
278                 public ScannedFile(String filename, String hash) {
279                         this.filename = filename;
280                         this.hash = hash;
281                 }
282
283                 //
284                 // ACCESSORS
285                 //
286
287                 /**
288                  * Returns the name of the file.
289                  *
290                  * @return The name of the file
291                  */
292                 public String getFilename() {
293                         return filename;
294                 }
295
296                 /**
297                  * Returns the hash of the file.
298                  *
299                  * @return The hash of the file
300                  */
301                 public String getHash() {
302                         return hash;
303                 }
304
305                 //
306                 // OBJECT METHODS
307                 //
308
309                 /**
310                  * {@inheritDoc}
311                  */
312                 @Override
313                 public int hashCode() {
314                         return filename.hashCode();
315                 }
316
317                 /**
318                  * {@inheritDoc}
319                  */
320                 @Override
321                 public boolean equals(Object obj) {
322                         return filename.equals(obj);
323                 }
324
325                 /**
326                  * {@inheritDoc}
327                  */
328                 @Override
329                 public String toString() {
330                         return filename;
331                 }
332
333                 //
334                 // COMPARABLE METHODS
335                 //
336
337                 /**
338                  * {@inheritDoc}
339                  */
340                 @Override
341                 public int compareTo(ScannedFile scannedFile) {
342                         return filename.compareTo(scannedFile.filename);
343                 }
344
345         }
346
347 }