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