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 1304052 2012-03-22 20:55:29Z ggregory $
120 * @since 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 * Return the fileFilter.
228 *
229 * @return the fileFilter
230 * @since 2.1
231 */
232 public FileFilter getFileFilter() {
233 return fileFilter;
234 }
235
236 /**
237 * Add a file system listener.
238 *
239 * @param listener The file system listener
240 */
241 public void addListener(final FileAlterationListener listener) {
242 if (listener != null) {
243 listeners.add(listener);
244 }
245 }
246
247 /**
248 * Remove a file system listener.
249 *
250 * @param listener The file system listener
251 */
252 public void removeListener(final FileAlterationListener listener) {
253 if (listener != null) {
254 while (listeners.remove(listener)) {
255 }
256 }
257 }
258
259 /**
260 * Returns the set of registered file system listeners.
261 *
262 * @return The file system listeners
263 */
264 public Iterable<FileAlterationListener> getListeners() {
265 return listeners;
266 }
267
268 /**
269 * Initialize the observer.
270 *
271 * @throws Exception if an error occurs
272 */
273 public void initialize() throws Exception {
274 rootEntry.refresh(rootEntry.getFile());
275 File[] files = listFiles(rootEntry.getFile());
276 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
277 for (int i = 0; i < files.length; i++) {
278 children[i] = createFileEntry(rootEntry, files[i]);
279 }
280 rootEntry.setChildren(children);
281 }
282
283 /**
284 * Final processing.
285 *
286 * @throws Exception if an error occurs
287 */
288 public void destroy() throws Exception {
289 }
290
291 /**
292 * Check whether the file and its chlidren have been created, modified or deleted.
293 */
294 public void checkAndNotify() {
295
296 /* fire onStart() */
297 for (FileAlterationListener listener : listeners) {
298 listener.onStart(this);
299 }
300
301 /* fire directory/file events */
302 File rootFile = rootEntry.getFile();
303 if (rootFile.exists()) {
304 checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
305 } else if (rootEntry.isExists()) {
306 checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
307 } else {
308 // Didn't exist and still doesn't
309 }
310
311 /* fire onStop() */
312 for (FileAlterationListener listener : listeners) {
313 listener.onStop(this);
314 }
315 }
316
317 /**
318 * Compare two file lists for files which have been created, modified or deleted.
319 *
320 * @param parent The parent entry
321 * @param previous The original list of files
322 * @param files The current list of files
323 */
324 private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) {
325 int c = 0;
326 FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
327 for (FileEntry entry : previous) {
328 while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
329 current[c] = createFileEntry(parent, files[c]);
330 doCreate(current[c]);
331 c++;
332 }
333 if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
334 doMatch(entry, files[c]);
335 checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
336 current[c] = entry;
337 c++;
338 } else {
339 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
340 doDelete(entry);
341 }
342 }
343 for (; c < files.length; c++) {
344 current[c] = createFileEntry(parent, files[c]);
345 doCreate(current[c]);
346 }
347 parent.setChildren(current);
348 }
349
350 /**
351 * Create a new file entry for the specified file.
352 *
353 * @param parent The parent file entry
354 * @param file The file to create an entry for
355 * @return A new file entry
356 */
357 private FileEntry createFileEntry(FileEntry parent, File file) {
358 FileEntry entry = parent.newChildInstance(file);
359 entry.refresh(file);
360 File[] files = listFiles(file);
361 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
362 for (int i = 0; i < files.length; i++) {
363 children[i] = createFileEntry(entry, files[i]);
364 }
365 entry.setChildren(children);
366 return entry;
367 }
368
369 /**
370 * Fire directory/file created events to the registered listeners.
371 *
372 * @param entry The file entry
373 */
374 private void doCreate(FileEntry entry) {
375 for (FileAlterationListener listener : listeners) {
376 if (entry.isDirectory()) {
377 listener.onDirectoryCreate(entry.getFile());
378 } else {
379 listener.onFileCreate(entry.getFile());
380 }
381 }
382 FileEntry[] children = entry.getChildren();
383 for (FileEntry aChildren : children) {
384 doCreate(aChildren);
385 }
386 }
387
388 /**
389 * Fire directory/file change events to the registered listeners.
390 *
391 * @param entry The previous file system entry
392 * @param file The current file
393 */
394 private void doMatch(FileEntry entry, File file) {
395 if (entry.refresh(file)) {
396 for (FileAlterationListener listener : listeners) {
397 if (entry.isDirectory()) {
398 listener.onDirectoryChange(file);
399 } else {
400 listener.onFileChange(file);
401 }
402 }
403 }
404 }
405
406 /**
407 * Fire directory/file delete events to the registered listeners.
408 *
409 * @param entry The file entry
410 */
411 private void doDelete(FileEntry entry) {
412 for (FileAlterationListener listener : listeners) {
413 if (entry.isDirectory()) {
414 listener.onDirectoryDelete(entry.getFile());
415 } else {
416 listener.onFileDelete(entry.getFile());
417 }
418 }
419 }
420
421 /**
422 * List the contents of a directory
423 *
424 * @param file The file to list the contents of
425 * @return the directory contents or a zero length array if
426 * the empty or the file is not a directory
427 */
428 private File[] listFiles(File file) {
429 File[] children = null;
430 if (file.isDirectory()) {
431 children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
432 }
433 if (children == null) {
434 children = FileUtils.EMPTY_FILE_ARRAY;
435 }
436 if (comparator != null && children.length > 1) {
437 Arrays.sort(children, comparator);
438 }
439 return children;
440 }
441
442 /**
443 * Provide a String representation of this observer.
444 *
445 * @return a String representation of this observer
446 */
447 @Override
448 public String toString() {
449 StringBuilder builder = new StringBuilder();
450 builder.append(getClass().getSimpleName());
451 builder.append("[file='");
452 builder.append(getDirectory().getPath());
453 builder.append('\'');
454 if (fileFilter != null) {
455 builder.append(", ");
456 builder.append(fileFilter.toString());
457 }
458 builder.append(", listeners=");
459 builder.append(listeners.size());
460 builder.append("]");
461 return builder.toString();
462 }
463
464 }