d86a6ba69e06f3d610eae7a706344a707fba97cf
[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         /** The name of the last file scanned. */
67         private String lastFilename;
68
69         /**
70          * Creates a new file scanner for the given project.
71          *
72          * @param project
73          *            The project whose files to scan
74          */
75         public FileScanner(Project project) {
76                 this.project = project;
77         }
78
79         /**
80          * Adds the given listener to the list of listeners.
81          *
82          * @param fileScannerListener
83          *            The listener to add
84          */
85         public void addFileScannerListener(FileScannerListener fileScannerListener) {
86                 fileScannerListeners.add(fileScannerListener);
87         }
88
89         /**
90          * Removes the given listener from the list of listeners.
91          *
92          * @param fileScannerListener
93          *            The listener to remove
94          */
95         public void removeFileScannerListener(FileScannerListener fileScannerListener) {
96                 fileScannerListeners.remove(fileScannerListener);
97         }
98
99         /**
100          * Notifies all listeners that the file scan finished.
101          */
102         protected void fireFileScannerFinished() {
103                 for (FileScannerListener fileScannerListener : new ArrayList<FileScannerListener>(fileScannerListeners)) {
104                         fileScannerListener.fileScannerFinished(this);
105                 }
106         }
107
108         /**
109          * Returns the name of the last file scanned.
110          *
111          * @return The name of the last file scanned, or {@code null} if there was
112          *         no file scanned yet
113          */
114         public String getLastFilename() {
115                 return lastFilename;
116         }
117
118         /**
119          * {@inheritDoc}
120          * <p>
121          * Scans all available files in the project’s local path and emits an event
122          * when finished.
123          *
124          * @see FileScannerListener#fileScannerFinished(FileScanner)
125          */
126         @Override
127         public void run() {
128                 files = new ArrayList<ScannedFile>();
129                 error = false;
130                 lastFilename = null;
131                 try {
132                         scanFiles(new File(project.getLocalPath()), files);
133                         Collections.sort(files);
134                 } catch (IOException ioe1) {
135                         error = true;
136                 }
137                 fireFileScannerFinished();
138         }
139
140         /**
141          * Returns whether there was an error scanning for files.
142          *
143          * @return <code>true</code> if there was an error, <code>false</code>
144          *         otherwise
145          */
146         public boolean isError() {
147                 return error;
148         }
149
150         /**
151          * Returns the list of found files.
152          *
153          * @return The list of found files
154          */
155         public List<ScannedFile> getFiles() {
156                 return files;
157         }
158
159         /**
160          * Recursively scans a directory and adds all found files to the given list.
161          *
162          * @param rootDir
163          *            The directory to scan
164          * @param fileList
165          *            The list to which to add the found files
166          * @throws IOException
167          *             if an I/O error occurs
168          */
169         private void scanFiles(File rootDir, List<ScannedFile> fileList) throws IOException {
170                 File[] files = rootDir.listFiles(new FileFilter() {
171
172                         @Override
173                         @SuppressWarnings("synthetic-access")
174                         public boolean accept(File file) {
175                                 return !project.isIgnoreHiddenFiles() || !file.isHidden();
176                         }
177                 });
178                 if (files == null) {
179                         throw new IOException(I18n.getMessage("jsite.file-scanner.can-not-read-directory"));
180                 }
181                 for (File file : files) {
182                         if (file.isDirectory()) {
183                                 scanFiles(file, fileList);
184                                 continue;
185                         }
186                         String filename = project.shortenFilename(file).replace('\\', '/');
187                         String hash = hashFile(project.getLocalPath(), filename);
188                         fileList.add(new ScannedFile(filename, hash));
189                         lastFilename = filename;
190                 }
191         }
192
193         /**
194          * Hashes the given file.
195          *
196          * @param path
197          *            The path of the project
198          * @param filename
199          *            The name of the file, relative to the project path
200          * @return The hash of the file
201          */
202         private static String hashFile(String path, String filename) {
203                 InputStream fileInputStream = null;
204                 DigestOutputStream digestOutputStream = null;
205                 File file = new File(path, filename);
206                 try {
207                         fileInputStream = new FileInputStream(file);
208                         digestOutputStream = new DigestOutputStream(new NullOutputStream(), MessageDigest.getInstance("SHA-256"));
209                         StreamCopier.copy(fileInputStream, digestOutputStream, file.length());
210                         return toHex(digestOutputStream.getMessageDigest().digest());
211                 } catch (NoSuchAlgorithmException nsae1) {
212                         logger.log(Level.WARNING, "Could not get SHA-256 digest!", nsae1);
213                 } catch (IOException ioe1) {
214                         logger.log(Level.WARNING, "Could not read file!", ioe1);
215                 } finally {
216                         Closer.close(digestOutputStream);
217                         Closer.close(fileInputStream);
218                 }
219                 return toHex(new byte[32]);
220         }
221
222         /**
223          * Converts the given byte array into a hexadecimal string.
224          *
225          * @param array
226          *            The array to convert
227          * @return The hexadecimal string
228          */
229         private static String toHex(byte[] array) {
230                 StringBuilder hexString = new StringBuilder(array.length * 2);
231                 for (byte b : array) {
232                         hexString.append("0123456789abcdef".charAt((b >>> 4) & 0x0f)).append("0123456789abcdef".charAt(b & 0xf));
233                 }
234                 return hexString.toString();
235         }
236
237         /**
238          * Container for a scanned file, consisting of the name of the file and its
239          * hash.
240          *
241          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
242          */
243         public static class ScannedFile implements Comparable<ScannedFile> {
244
245                 /** The name of the file. */
246                 private final String filename;
247
248                 /** The hash of the file. */
249                 private final String hash;
250
251                 /**
252                  * Creates a new scanned file.
253                  *
254                  * @param filename
255                  *            The name of the file
256                  * @param hash
257                  *            The hash of the file
258                  */
259                 public ScannedFile(String filename, String hash) {
260                         this.filename = filename;
261                         this.hash = hash;
262                 }
263
264                 //
265                 // ACCESSORS
266                 //
267
268                 /**
269                  * Returns the name of the file.
270                  *
271                  * @return The name of the file
272                  */
273                 public String getFilename() {
274                         return filename;
275                 }
276
277                 /**
278                  * Returns the hash of the file.
279                  *
280                  * @return The hash of the file
281                  */
282                 public String getHash() {
283                         return hash;
284                 }
285
286                 //
287                 // OBJECT METHODS
288                 //
289
290                 /**
291                  * {@inheritDoc}
292                  */
293                 @Override
294                 public int hashCode() {
295                         return filename.hashCode();
296                 }
297
298                 /**
299                  * {@inheritDoc}
300                  */
301                 @Override
302                 public boolean equals(Object obj) {
303                         return filename.equals(obj);
304                 }
305
306                 /**
307                  * {@inheritDoc}
308                  */
309                 @Override
310                 public String toString() {
311                         return filename;
312                 }
313
314                 //
315                 // COMPARABLE METHODS
316                 //
317
318                 /**
319                  * {@inheritDoc}
320                  */
321                 @Override
322                 public int compareTo(ScannedFile scannedFile) {
323                         return filename.compareTo(scannedFile.filename);
324                 }
325
326         }
327
328 }