001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.io.monitor;
018
019 import java.io.File;
020 import java.io.FileFilter;
021 import java.io.Serializable;
022 import java.util.Arrays;
023 import java.util.Comparator;
024 import java.util.List;
025 import java.util.concurrent.CopyOnWriteArrayList;
026
027 import org.apache.commons.io.FileUtils;
028 import org.apache.commons.io.IOCase;
029 import org.apache.commons.io.comparator.NameFileComparator;
030
031 /**
032 * FileAlterationObserver represents the state of files below a root directory,
033 * checking the filesystem and notifying listeners of create, change or
034 * delete events.
035 * <p>
036 * To use this implementation:
037 * <ul>
038 * <li>Create {@link FileAlterationListener} implementation(s) that process
039 * the file/directory create, change and delete events</li>
040 * <li>Register the listener(s) with a {@link FileAlterationObserver} for
041 * the appropriate directory.</li>
042 * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or
043 * run manually.</li>
044 * </ul>
045 *
046 * <h2>Basic Usage</h2>
047 * Create a {@link FileAlterationObserver} for the directory and register the listeners:
048 * <pre>
049 * File directory = new File(new File("."), "src");
050 * FileAlterationObserver observer = new FileAlterationObserver(directory);
051 * observer.addListener(...);
052 * observer.addListener(...);
053 * </pre>
054 * To manually observe a directory, initialize the observer and invoked the
055 * {@link #checkAndNotify()} method as required:
056 * <pre>
057 * // intialize
058 * observer.init();
059 * ...
060 * // invoke as required
061 * observer.checkAndNotify();
062 * ...
063 * observer.checkAndNotify();
064 * ...
065 * // finished
066 * observer.finish();
067 * </pre>
068 * Alternatively, register the oberver(s) with a {@link FileAlterationMonitor},
069 * which creates a new thread, invoking the observer at the specified interval:
070 * <pre>
071 * long interval = ...
072 * FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
073 * monitor.addObserver(observer);
074 * monitor.start();
075 * ...
076 * monitor.stop();
077 * </pre>
078 *
079 * <h2>File Filters</h2>
080 * This implementation can monitor portions of the file system
081 * by using {@link FileFilter}s to observe only the files and/or directories
082 * that are of interest. This makes it more efficient and reduces the
083 * noise from <i>unwanted</i> file system events.
084 * <p>
085 * <a href="http://commons.apache.org/io/">Commons IO</a> has a good range of
086 * useful, ready made
087 * <a href="../filefilter/package-summary.html">File Filter</a>
088 * implementations for this purpose.
089 * <p>
090 * For example, to only observe 1) visible directories and 2) files with a ".java" suffix
091 * in a root directory called "src" you could set up a {@link FileAlterationObserver} in the following
092 * way:
093 * <pre>
094 * // Create a FileFilter
095 * IOFileFilter directories = FileFilterUtils.and(
096 * FileFilterUtils.directoryFileFilter(),
097 * HiddenFileFilter.VISIBLE);
098 * IOFileFilter files = FileFilterUtils.and(
099 * FileFilterUtils.fileFileFilter(),
100 * FileFilterUtils.suffixFileFilter(".java"));
101 * IOFileFilter filter = FileFilterUtils.or(directories, files);
102 *
103 * // Create the File system observer and register File Listeners
104 * FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter);
105 * observer.addListener(...);
106 * observer.addListener(...);
107 * </pre>
108 *
109 * <h2>FileEntry</h2>
110 * {@link FileEntry} represents the state of a file or directory, capturing
111 * {@link File} attributes at a point in time. Custom implementations of
112 * {@link FileEntry} can be used to capture additional properties that the
113 * basic implementation does not support. The {@link FileEntry#refresh(File)}
114 * method is used to determine if a file or directory has changed since the last
115 * check and stores the current state of the {@link File}'s properties.
116 *
117 * @see FileAlterationListener
118 * @see FileAlterationMonitor
119 * @version $Id: FileAlterationObserver.java 1022803 2010-10-15 01:09:39Z niallp $
120 * @since Commons IO 2.0
121 */
122 public class FileAlterationObserver implements Serializable {
123
124 private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<FileAlterationListener>();
125 private final FileEntry rootEntry;
126 private final FileFilter fileFilter;
127 private final Comparator<File> comparator;
128
129 /**
130 * Construct an observer for the specified directory.
131 *
132 * @param directoryName the name of the directory to observe
133 */
134 public FileAlterationObserver(String directoryName) {
135 this(new File(directoryName));
136 }
137
138 /**
139 * Construct an observer for the specified directory and file filter.
140 *
141 * @param directoryName the name of the directory to observe
142 * @param fileFilter The file filter or null if none
143 */
144 public FileAlterationObserver(String directoryName, FileFilter fileFilter) {
145 this(new File(directoryName), fileFilter);
146 }
147
148 /**
149 * Construct an observer for the specified directory, file filter and
150 * file comparator.
151 *
152 * @param directoryName the name of the directory to observe
153 * @param fileFilter The file filter or null if none
154 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive
155 */
156 public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
157 this(new File(directoryName), fileFilter, caseSensitivity);
158 }
159
160 /**
161 * Construct an observer for the specified directory.
162 *
163 * @param directory the directory to observe
164 */
165 public FileAlterationObserver(File directory) {
166 this(directory, (FileFilter)null);
167 }
168
169 /**
170 * Construct an observer for the specified directory and file filter.
171 *
172 * @param directory the directory to observe
173 * @param fileFilter The file filter or null if none
174 */
175 public FileAlterationObserver(File directory, FileFilter fileFilter) {
176 this(directory, fileFilter, (IOCase)null);
177 }
178
179 /**
180 * Construct an observer for the specified directory, file filter and
181 * file comparator.
182 *
183 * @param directory the directory to observe
184 * @param fileFilter The file filter or null if none
185 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive
186 */
187 public FileAlterationObserver(File directory, FileFilter fileFilter, IOCase caseSensitivity) {
188 this(new FileEntry(directory), fileFilter, caseSensitivity);
189 }
190
191 /**
192 * Construct an observer for the specified directory, file filter and
193 * file comparator.
194 *
195 * @param rootEntry the root directory to observe
196 * @param fileFilter The file filter or null if none
197 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive
198 */
199 protected FileAlterationObserver(FileEntry rootEntry, FileFilter fileFilter, IOCase caseSensitivity) {
200 if (rootEntry == null) {
201 throw new IllegalArgumentException("Root entry is missing");
202 }
203 if (rootEntry.getFile() == null) {
204 throw new IllegalArgumentException("Root directory is missing");
205 }
206 this.rootEntry = rootEntry;
207 this.fileFilter = fileFilter;
208 if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) {
209 this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR;
210 } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) {
211 this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
212 } else {
213 this.comparator = NameFileComparator.NAME_COMPARATOR;
214 }
215 }
216
217 /**
218 * Return the directory being observed.
219 *
220 * @return the directory being observed
221 */
222 public File getDirectory() {
223 return rootEntry.getFile();
224 }
225
226 /**
227 * Add a file system listener.
228 *
229 * @param listener The file system listener
230 */
231 public void addListener(final FileAlterationListener listener) {
232 if (listener != null) {
233 listeners.add(listener);
234 }
235 }
236
237 /**
238 * Remove a file system listener.
239 *
240 * @param listener The file system listener
241 */
242 public void removeListener(final FileAlterationListener listener) {
243 if (listener != null) {
244 while (listeners.remove(listener)) {
245 }
246 }
247 }
248
249 /**
250 * Returns the set of registered file system listeners.
251 *
252 * @return The file system listeners
253 */
254 public Iterable<FileAlterationListener> getListeners() {
255 return listeners;
256 }
257
258 /**
259 * Initialize the observer.
260 *
261 * @throws Exception if an error occurs
262 */
263 public void initialize() throws Exception {
264 rootEntry.refresh(rootEntry.getFile());
265 File[] files = listFiles(rootEntry.getFile());
266 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
267 for (int i = 0; i < files.length; i++) {
268 children[i] = createFileEntry(rootEntry, files[i]);
269 }
270 rootEntry.setChildren(children);
271 }
272
273 /**
274 * Final processing.
275 *
276 * @throws Exception if an error occurs
277 */
278 public void destroy() throws Exception {
279 }
280
281 /**
282 * Check whether the file and its chlidren have been created, modified or deleted.
283 */
284 public void checkAndNotify() {
285
286 /* fire onStart() */
287 for (FileAlterationListener listener : listeners) {
288 listener.onStart(this);
289 }
290
291 /* fire directory/file events */
292 File rootFile = rootEntry.getFile();
293 if (rootFile.exists()) {
294 checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
295 } else if (rootEntry.isExists()) {
296 checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
297 } else {
298 // Didn't exist and still doesn't
299 }
300
301 /* fire onStop() */
302 for (FileAlterationListener listener : listeners) {
303 listener.onStop(this);
304 }
305 }
306
307 /**
308 * Compare two file lists for files which have been created, modified or deleted.
309 *
310 * @param parent The parent entry
311 * @param previous The original list of files
312 * @param files The current list of files
313 */
314 private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) {
315 int c = 0;
316 FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
317 for (FileEntry entry : previous) {
318 while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
319 current[c] = createFileEntry(parent, files[c]);
320 doCreate(current[c]);
321 c++;
322 }
323 if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
324 doMatch(entry, files[c]);
325 checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
326 current[c] = entry;
327 c++;
328 } else {
329 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
330 doDelete(entry);
331 }
332 }
333 for (; c < files.length; c++) {
334 current[c] = createFileEntry(parent, files[c]);
335 doCreate(current[c]);
336 }
337 parent.setChildren(current);
338 }
339
340 /**
341 * Create a new file entry for the specified file.
342 *
343 * @param parent The parent file entry
344 * @param file The file to create an entry for
345 * @return A new file entry
346 */
347 private FileEntry createFileEntry(FileEntry parent, File file) {
348 FileEntry entry = parent.newChildInstance(file);
349 entry.refresh(file);
350 File[] files = listFiles(file);
351 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
352 for (int i = 0; i < files.length; i++) {
353 children[i] = createFileEntry(entry, files[i]);
354 }
355 entry.setChildren(children);
356 return entry;
357 }
358
359 /**
360 * Fire directory/file created events to the registered listeners.
361 *
362 * @param entry The file entry
363 */
364 private void doCreate(FileEntry entry) {
365 for (FileAlterationListener listener : listeners) {
366 if (entry.isDirectory()) {
367 listener.onDirectoryCreate(entry.getFile());
368 } else {
369 listener.onFileCreate(entry.getFile());
370 }
371 }
372 FileEntry[] children = entry.getChildren();
373 for (FileEntry aChildren : children) {
374 doCreate(aChildren);
375 }
376 }
377
378 /**
379 * Fire directory/file change events to the registered listeners.
380 *
381 * @param entry The previous file system entry
382 * @param file The current file
383 */
384 private void doMatch(FileEntry entry, File file) {
385 if (entry.refresh(file)) {
386 for (FileAlterationListener listener : listeners) {
387 if (entry.isDirectory()) {
388 listener.onDirectoryChange(file);
389 } else {
390 listener.onFileChange(file);
391 }
392 }
393 }
394 }
395
396 /**
397 * Fire directory/file delete events to the registered listeners.
398 *
399 * @param entry The file entry
400 */
401 private void doDelete(FileEntry entry) {
402 for (FileAlterationListener listener : listeners) {
403 if (entry.isDirectory()) {
404 listener.onDirectoryDelete(entry.getFile());
405 } else {
406 listener.onFileDelete(entry.getFile());
407 }
408 }
409 }
410
411 /**
412 * List the contents of a directory
413 *
414 * @param file The file to list the contents of
415 * @return the directory contents or a zero length array if
416 * the empty or the file is not a directory
417 */
418 private File[] listFiles(File file) {
419 File[] children = null;
420 if (file.isDirectory()) {
421 children = (fileFilter == null) ? file.listFiles() : file.listFiles(fileFilter);
422 }
423 if (children == null) {
424 children = FileUtils.EMPTY_FILE_ARRAY;
425 }
426 if (comparator != null && children.length > 1) {
427 Arrays.sort(children, comparator);
428 }
429 return children;
430 }
431
432 /**
433 * Provide a String representation of this observer.
434 *
435 * @return a String representation of this observer
436 */
437 @Override
438 public String toString() {
439 StringBuilder builder = new StringBuilder();
440 builder.append(getClass().getSimpleName());
441 builder.append("[file='");
442 builder.append(getDirectory().getPath());
443 builder.append('\'');
444 if (fileFilter != null) {
445 builder.append(", ");
446 builder.append(fileFilter.toString());
447 }
448 builder.append(", listeners=");
449 builder.append(listeners.size());
450 builder.append("]");
451 return builder.toString();
452 }
453
454 }