/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.io.monitor;

import java.io.File;
import java.io.FileFilter;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.comparator.NameFileComparator;

FileAlterationObserver represents the state of files below a root directory, checking the filesystem and notifying listeners of create, change or delete events.

To use this implementation:

Basic Usage

Create a FileAlterationObserver for the directory and register the listeners:
     File directory = new File(new File("."), "src");
     FileAlterationObserver observer = new FileAlterationObserver(directory);
     observer.addListener(...);
     observer.addListener(...);
To manually observe a directory, initialize the observer and invoked the checkAndNotify() method as required:
     // initialize
     observer.init();
     ...
     // invoke as required
     observer.checkAndNotify();
     ...
     observer.checkAndNotify();
     ...
     // finished
     observer.finish();
Alternatively, register the observer(s) with a FileAlterationMonitor, which creates a new thread, invoking the observer at the specified interval:
     long interval = ...
     FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
     monitor.addObserver(observer);
     monitor.start();
     ...
     monitor.stop();

File Filters

This implementation can monitor portions of the file system by using FileFilters to observe only the files and/or directories that are of interest. This makes it more efficient and reduces the noise from unwanted file system events.

Commons IO has a good range of useful, ready made File Filter implementations for this purpose.

For example, to only observe 1) visible directories and 2) files with a ".java" suffix in a root directory called "src" you could set up a FileAlterationObserver in the following way:

     // Create a FileFilter
     IOFileFilter directories = FileFilterUtils.and(
                                     FileFilterUtils.directoryFileFilter(),
                                     HiddenFileFilter.VISIBLE);
     IOFileFilter files       = FileFilterUtils.and(
                                     FileFilterUtils.fileFileFilter(),
                                     FileFilterUtils.suffixFileFilter(".java"));
     IOFileFilter filter = FileFilterUtils.or(directories, files);
     // Create the File system observer and register File Listeners
     FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter);
     observer.addListener(...);
     observer.addListener(...);

FileEntry

FileEntry represents the state of a file or directory, capturing File attributes at a point in time. Custom implementations of FileEntry can be used to capture additional properties that the basic implementation does not support. The FileEntry.refresh(File) method is used to determine if a file or directory has changed since the last check and stores the current state of the File's properties.
See Also:
Version:$Id$
Since:2.0
/** * FileAlterationObserver represents the state of files below a root directory, * checking the filesystem and notifying listeners of create, change or * delete events. * <p> * To use this implementation: * <ul> * <li>Create {@link FileAlterationListener} implementation(s) that process * the file/directory create, change and delete events</li> * <li>Register the listener(s) with a {@link FileAlterationObserver} for * the appropriate directory.</li> * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or * run manually.</li> * </ul> * * <h2>Basic Usage</h2> * Create a {@link FileAlterationObserver} for the directory and register the listeners: * <pre> * File directory = new File(new File("."), "src"); * FileAlterationObserver observer = new FileAlterationObserver(directory); * observer.addListener(...); * observer.addListener(...); * </pre> * To manually observe a directory, initialize the observer and invoked the * {@link #checkAndNotify()} method as required: * <pre> * // initialize * observer.init(); * ... * // invoke as required * observer.checkAndNotify(); * ... * observer.checkAndNotify(); * ... * // finished * observer.finish(); * </pre> * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, * which creates a new thread, invoking the observer at the specified interval: * <pre> * long interval = ... * FileAlterationMonitor monitor = new FileAlterationMonitor(interval); * monitor.addObserver(observer); * monitor.start(); * ... * monitor.stop(); * </pre> * * <h2>File Filters</h2> * This implementation can monitor portions of the file system * by using {@link FileFilter}s to observe only the files and/or directories * that are of interest. This makes it more efficient and reduces the * noise from <i>unwanted</i> file system events. * <p> * <a href="http://commons.apache.org/io/">Commons IO</a> has a good range of * useful, ready made * <a href="../filefilter/package-summary.html">File Filter</a> * implementations for this purpose. * <p> * For example, to only observe 1) visible directories and 2) files with a ".java" suffix * in a root directory called "src" you could set up a {@link FileAlterationObserver} in the following * way: * <pre> * // Create a FileFilter * IOFileFilter directories = FileFilterUtils.and( * FileFilterUtils.directoryFileFilter(), * HiddenFileFilter.VISIBLE); * IOFileFilter files = FileFilterUtils.and( * FileFilterUtils.fileFileFilter(), * FileFilterUtils.suffixFileFilter(".java")); * IOFileFilter filter = FileFilterUtils.or(directories, files); * * // Create the File system observer and register File Listeners * FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter); * observer.addListener(...); * observer.addListener(...); * </pre> * * <h2>FileEntry</h2> * {@link FileEntry} represents the state of a file or directory, capturing * {@link File} attributes at a point in time. Custom implementations of * {@link FileEntry} can be used to capture additional properties that the * basic implementation does not support. The {@link FileEntry#refresh(File)} * method is used to determine if a file or directory has changed since the last * check and stores the current state of the {@link File}'s properties. * * @see FileAlterationListener * @see FileAlterationMonitor * @version $Id$ * @since 2.0 */
public class FileAlterationObserver implements Serializable { private static final long serialVersionUID = 1185122225658782848L; private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>(); private final FileEntry rootEntry; private final FileFilter fileFilter; private final Comparator<File> comparator;
Construct an observer for the specified directory.
Params:
  • directoryName – the name of the directory to observe
/** * Construct an observer for the specified directory. * * @param directoryName the name of the directory to observe */
public FileAlterationObserver(final String directoryName) { this(new File(directoryName)); }
Construct an observer for the specified directory and file filter.
Params:
  • directoryName – the name of the directory to observe
  • fileFilter – The file filter or null if none
/** * Construct an observer for the specified directory and file filter. * * @param directoryName the name of the directory to observe * @param fileFilter The file filter or null if none */
public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) { this(new File(directoryName), fileFilter); }
Construct an observer for the specified directory, file filter and file comparator.
Params:
  • directoryName – the name of the directory to observe
  • fileFilter – The file filter or null if none
  • caseSensitivity – what case sensitivity to use comparing file names, null means system sensitive
/** * Construct an observer for the specified directory, file filter and * file comparator. * * @param directoryName the name of the directory to observe * @param fileFilter The file filter or null if none * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive */
public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase caseSensitivity) { this(new File(directoryName), fileFilter, caseSensitivity); }
Construct an observer for the specified directory.
Params:
  • directory – the directory to observe
/** * Construct an observer for the specified directory. * * @param directory the directory to observe */
public FileAlterationObserver(final File directory) { this(directory, null); }
Construct an observer for the specified directory and file filter.
Params:
  • directory – the directory to observe
  • fileFilter – The file filter or null if none
/** * Construct an observer for the specified directory and file filter. * * @param directory the directory to observe * @param fileFilter The file filter or null if none */
public FileAlterationObserver(final File directory, final FileFilter fileFilter) { this(directory, fileFilter, null); }
Construct an observer for the specified directory, file filter and file comparator.
Params:
  • directory – the directory to observe
  • fileFilter – The file filter or null if none
  • caseSensitivity – what case sensitivity to use comparing file names, null means system sensitive
/** * Construct an observer for the specified directory, file filter and * file comparator. * * @param directory the directory to observe * @param fileFilter The file filter or null if none * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive */
public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase caseSensitivity) { this(new FileEntry(directory), fileFilter, caseSensitivity); }
Construct an observer for the specified directory, file filter and file comparator.
Params:
  • rootEntry – the root directory to observe
  • fileFilter – The file filter or null if none
  • caseSensitivity – what case sensitivity to use comparing file names, null means system sensitive
/** * Construct an observer for the specified directory, file filter and * file comparator. * * @param rootEntry the root directory to observe * @param fileFilter The file filter or null if none * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive */
protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase caseSensitivity) { if (rootEntry == null) { throw new IllegalArgumentException("Root entry is missing"); } if (rootEntry.getFile() == null) { throw new IllegalArgumentException("Root directory is missing"); } this.rootEntry = rootEntry; this.fileFilter = fileFilter; if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) { this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR; } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) { this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR; } else { this.comparator = NameFileComparator.NAME_COMPARATOR; } }
Return the directory being observed.
Returns:the directory being observed
/** * Return the directory being observed. * * @return the directory being observed */
public File getDirectory() { return rootEntry.getFile(); }
Return the fileFilter.
Returns:the fileFilter
Since:2.1
/** * Return the fileFilter. * * @return the fileFilter * @since 2.1 */
public FileFilter getFileFilter() { return fileFilter; }
Add a file system listener.
Params:
  • listener – The file system listener
/** * Add a file system listener. * * @param listener The file system listener */
public void addListener(final FileAlterationListener listener) { if (listener != null) { listeners.add(listener); } }
Remove a file system listener.
Params:
  • listener – The file system listener
/** * Remove a file system listener. * * @param listener The file system listener */
public void removeListener(final FileAlterationListener listener) { if (listener != null) { while (listeners.remove(listener)) { } } }
Returns the set of registered file system listeners.
Returns:The file system listeners
/** * Returns the set of registered file system listeners. * * @return The file system listeners */
public Iterable<FileAlterationListener> getListeners() { return listeners; }
Initialize the observer.
Throws:
  • Exception – if an error occurs
/** * Initialize the observer. * * @throws Exception if an error occurs */
public void initialize() throws Exception { rootEntry.refresh(rootEntry.getFile()); final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry); rootEntry.setChildren(children); }
Final processing.
Throws:
  • Exception – if an error occurs
/** * Final processing. * * @throws Exception if an error occurs */
public void destroy() throws Exception { }
Check whether the file and its children have been created, modified or deleted.
/** * Check whether the file and its children have been created, modified or deleted. */
public void checkAndNotify() { /* fire onStart() */ for (final FileAlterationListener listener : listeners) { listener.onStart(this); } /* fire directory/file events */ final File rootFile = rootEntry.getFile(); if (rootFile.exists()) { checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile)); } else if (rootEntry.isExists()) { checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); } else { // Didn't exist and still doesn't } /* fire onStop() */ for (final FileAlterationListener listener : listeners) { listener.onStop(this); } }
Compare two file lists for files which have been created, modified or deleted.
Params:
  • parent – The parent entry
  • previous – The original list of files
  • files – The current list of files
/** * Compare two file lists for files which have been created, modified or deleted. * * @param parent The parent entry * @param previous The original list of files * @param files The current list of files */
private void checkAndNotify(final FileEntry parent, final FileEntry[] previous, final File[] files) { int c = 0; final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; for (final FileEntry entry : previous) { while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) { current[c] = createFileEntry(parent, files[c]); doCreate(current[c]); c++; } if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) { doMatch(entry, files[c]); checkAndNotify(entry, entry.getChildren(), listFiles(files[c])); current[c] = entry; c++; } else { checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); doDelete(entry); } } for (; c < files.length; c++) { current[c] = createFileEntry(parent, files[c]); doCreate(current[c]); } parent.setChildren(current); }
Create a new file entry for the specified file.
Params:
  • parent – The parent file entry
  • file – The file to create an entry for
Returns:A new file entry
/** * Create a new file entry for the specified file. * * @param parent The parent file entry * @param file The file to create an entry for * @return A new file entry */
private FileEntry createFileEntry(final FileEntry parent, final File file) { final FileEntry entry = parent.newChildInstance(file); entry.refresh(file); final FileEntry[] children = doListFiles(file, entry); entry.setChildren(children); return entry; }
List the files
Params:
  • file – The file to list files for
  • entry – the parent entry
Returns:The child files
/** * List the files * @param file The file to list files for * @param entry the parent entry * @return The child files */
private FileEntry[] doListFiles(final File file, final FileEntry entry) { final File[] files = listFiles(file); final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; for (int i = 0; i < files.length; i++) { children[i] = createFileEntry(entry, files[i]); } return children; }
Fire directory/file created events to the registered listeners.
Params:
  • entry – The file entry
/** * Fire directory/file created events to the registered listeners. * * @param entry The file entry */
private void doCreate(final FileEntry entry) { for (final FileAlterationListener listener : listeners) { if (entry.isDirectory()) { listener.onDirectoryCreate(entry.getFile()); } else { listener.onFileCreate(entry.getFile()); } } final FileEntry[] children = entry.getChildren(); for (final FileEntry aChildren : children) { doCreate(aChildren); } }
Fire directory/file change events to the registered listeners.
Params:
  • entry – The previous file system entry
  • file – The current file
/** * Fire directory/file change events to the registered listeners. * * @param entry The previous file system entry * @param file The current file */
private void doMatch(final FileEntry entry, final File file) { if (entry.refresh(file)) { for (final FileAlterationListener listener : listeners) { if (entry.isDirectory()) { listener.onDirectoryChange(file); } else { listener.onFileChange(file); } } } }
Fire directory/file delete events to the registered listeners.
Params:
  • entry – The file entry
/** * Fire directory/file delete events to the registered listeners. * * @param entry The file entry */
private void doDelete(final FileEntry entry) { for (final FileAlterationListener listener : listeners) { if (entry.isDirectory()) { listener.onDirectoryDelete(entry.getFile()); } else { listener.onFileDelete(entry.getFile()); } } }
List the contents of a directory
Params:
  • file – The file to list the contents of
Returns:the directory contents or a zero length array if the empty or the file is not a directory
/** * List the contents of a directory * * @param file The file to list the contents of * @return the directory contents or a zero length array if * the empty or the file is not a directory */
private File[] listFiles(final File file) { File[] children = null; if (file.isDirectory()) { children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter); } if (children == null) { children = FileUtils.EMPTY_FILE_ARRAY; } if (comparator != null && children.length > 1) { Arrays.sort(children, comparator); } return children; }
Provide a String representation of this observer.
Returns:a String representation of this observer
/** * Provide a String representation of this observer. * * @return a String representation of this observer */
@Override public String toString() { final StringBuilder builder = new StringBuilder(); builder.append(getClass().getSimpleName()); builder.append("[file='"); builder.append(getDirectory().getPath()); builder.append('\''); if (fileFilter != null) { builder.append(", "); builder.append(fileFilter.toString()); } builder.append(", listeners="); builder.append(listeners.size()); builder.append("]"); return builder.toString(); } }