* 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.wamblee.io;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
- * Monitors a directory for changes.
- *
+ * Monitors a directory for changes.
+ *
* @author Erik Brakkee
*/
public class DirectoryMonitor {
-
- private static final Log LOG = LogFactory.getLog(DirectoryMonitor.class);
-
- public static interface Listener {
- void fileChanged(File aFile);
- void fileCreated(File aFile);
- void fileDeleted(File aFile);
- };
-
- private File _directory;
- private FileFilter _filter;
- private Listener _listener;
- private Map<File,Date> _contents;
-
- public DirectoryMonitor(File aDirectory, FileFilter aFilefilter, Listener aListener) {
- _directory = aDirectory;
- if ( !_directory.isDirectory()) {
- throw new IllegalArgumentException("Directory '" + _directory + "' does not exist");
- }
- _filter = aFilefilter;
- _listener = aListener;
- _contents = new HashMap<File,Date>();
- }
-
- public void poll() {
- LOG.debug("Polling " + _directory);
- Map<File,Date> newContents = new HashMap<File,Date>();
- File[] files = _directory.listFiles(_filter);
- for (File file: files) {
- if ( _contents.containsKey(file)) {
- Date oldDate = _contents.get(file);
- if (file.lastModified() != oldDate.getTime()) {
- _listener.fileChanged(file);
- } else {
- // No change.
- }
- _contents.remove(file);
- newContents.put(file, new Date(file.lastModified()));
- } else {
- _listener.fileCreated(file);
- newContents.put(file, new Date(file.lastModified()));
- }
- }
- for (File file: _contents.keySet()) {
- _listener.fileDeleted(file);
- }
- _contents = newContents;
- }
+
+ private static final Log LOG = LogFactory.getLog(DirectoryMonitor.class);
+
+ public static interface Listener {
+
+ void fileChanged(File aFile);
+
+ void fileCreated(File aFile);
+
+ void fileDeleted(File aFile);
+ };
+
+ private File _directory;
+ private FileFilter _filter;
+ private Listener _listener;
+ private Map<File, Date> _contents;
+
+ public DirectoryMonitor(File aDirectory, FileFilter aFilefilter,
+ Listener aListener) {
+ _directory = aDirectory;
+ if (!_directory.isDirectory()) {
+ throw new IllegalArgumentException("Directory '" + _directory
+ + "' does not exist");
+ }
+ _filter = aFilefilter;
+ _listener = aListener;
+ _contents = new HashMap<File, Date>();
+ }
+
+ /**
+ * Polls the directory for changes and notifies the listener of any changes.
+ * In case of any exceptions thrown by the listener while handling the changes,
+ * the next call to this method will invoked the listeners again for the same changes.
+ */
+ public void poll() {
+ LOG.debug("Polling " + _directory);
+ Map<File, Date> newContents = new HashMap<File, Date>();
+ File[] files = _directory.listFiles(_filter);
+
+ // Check deleted files.
+ Set<File> deletedFiles = new HashSet<File>(_contents.keySet());
+ for (File file : files) {
+ if (file.isFile()) {
+ if (_contents.containsKey(file)) {
+ deletedFiles.remove(file);
+ }
+ }
+ }
+ for (File file : deletedFiles) {
+ _listener.fileDeleted(file);
+ }
+
+ for (File file : files) {
+ if (file.isFile()) {
+ if (_contents.containsKey(file)) {
+ Date oldDate = _contents.get(file);
+ if (file.lastModified() != oldDate.getTime()) {
+ _listener.fileChanged(file);
+ } else {
+ // No change.
+ }
+ newContents.put(file, new Date(file.lastModified()));
+ } else {
+ _listener.fileCreated(file);
+ newContents.put(file, new Date(file.lastModified()));
+ }
+ }
+ }
+
+ _contents = newContents;
+ }
}
--- /dev/null
+package org.wamblee.io;
+
+import org.apache.oro.io.AwkFilenameFilter;
+import org.easymock.EasyMock;
+
+import junit.framework.TestCase;
+
+public class DirectoryMonitorTest extends TestCase {
+
+ private static final String REGEX = "^.*\\.txt$";
+ private static final String FILE1 = "file1.txt";
+
+ private TestData _data;
+ private DirectoryMonitor.Listener _listener;
+ private DirectoryMonitor _monitor;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ _data = new TestData(this);
+ _data.clean();
+ _listener = EasyMock.createStrictMock(DirectoryMonitor.Listener.class);
+ _monitor = new DirectoryMonitor(_data.getRoot(), new AwkFilenameFilter(
+ REGEX), _listener);
+ }
+
+ public void testEmptyDir() {
+ // Nothing is expected to be called.
+ for (int i = 0; i < 10; i++) {
+ EasyMock.replay(_listener);
+ _monitor.poll();
+ EasyMock.verify(_listener);
+ EasyMock.reset(_listener);
+ }
+ }
+
+ public void testFileCreated() {
+ _listener.fileCreated(_data.getFile(FILE1));
+ EasyMock.replay(_listener);
+ _data.createFile(FILE1, "hello");
+ _monitor.poll();
+ EasyMock.verify(_listener);
+ }
+
+ public void testFileDeleted() {
+ _data.createFile(FILE1, "hello");
+ _monitor.poll();
+
+ EasyMock.reset(_listener);
+
+ _data.deleteFile(FILE1);
+ _listener.fileDeleted(_data.getFile(FILE1));
+ EasyMock.replay(_listener);
+ _monitor.poll();
+ EasyMock.verify(_listener);
+ }
+
+ public void testFileChanged() throws InterruptedException {
+ _data.createFile(FILE1, "hello");
+ _monitor.poll();
+ EasyMock.reset(_listener);
+
+ Thread.sleep(2000);
+ _data.deleteFile(FILE1);
+ _data.createFile(FILE1, "bla");
+
+ _listener.fileChanged(_data.getFile(FILE1));
+ EasyMock.replay(_listener);
+ _monitor.poll();
+ EasyMock.verify(_listener);
+ }
+
+ public void testFileFilterIsUsed() {
+ _monitor.poll();
+
+ _data.createFile("file.xml", "hello");
+ EasyMock.replay(_listener);
+ _monitor.poll();
+ EasyMock.verify(_listener);
+ }
+
+ public void testDirectoryIsIgnored() {
+ _monitor.poll();
+ _data.createDir(FILE1);
+ EasyMock.replay(_listener);
+ _monitor.poll();
+ EasyMock.verify(_listener);
+ }
+
+ public void testExceptionsWIllLeadToRepeatedNotifications() {
+ _monitor.poll();
+ _data.createFile(FILE1, "hello");
+
+ _listener.fileCreated(_data.getFile(FILE1));
+ EasyMock.expectLastCall().andThrow(new RuntimeException());
+ EasyMock.replay(_listener);
+ try {
+ _monitor.poll();
+ } catch (RuntimeException e) {
+ EasyMock.verify(_listener);
+ EasyMock.reset(_listener);
+
+ // polling again should lead to the same filecreated call.
+ // this time no exception is thrown.
+
+ _listener.fileCreated(_data.getFile(FILE1));
+ EasyMock.replay(_listener);
+ _monitor.poll();
+ EasyMock.verify(_listener);
+ return;
+ }
+ fail(); // should not get here.
+
+
+ }
+}
/**
* File system utilities.
- *
+ *
* @author Erik Brakkee
*/
public final class FileSystemUtils {
- private static final Log LOG = LogFactory.getLog(FileSystemUtils.class);
-
- /**
- * Test output directory relative to the sub project.
- */
- private static final String TEST_OUTPUT_DIR = "../target/testoutput";
-
- /**
- * Test input directory relative to the sub project.
- */
- private static final String TEST_INPUT_DIR = "../src/test/resources";
-
- /*
- * Disabled.
- *
- */
- private FileSystemUtils() {
- // Empty
- }
-
- /**
- * Deletes a directory recursively. The test case will fail if the directory
- * does not exist or if deletion fails.
- *
- * @param aDir
- * Directory to delete.
- */
- public static void deleteDirRecursively(String aDir) {
- deleteDirRecursively(new File(aDir));
- }
-
- /**
- * Deletes a directory recursively. See {@link #deleteDirRecursively}.
- *
- * @param aDir
- * Directory.
- */
- public static void deleteDirRecursively(File aDir) {
- TestCase.assertTrue(aDir.isDirectory());
-
- for (File file : aDir.listFiles()) {
- if (file.isDirectory()) {
- deleteDirRecursively(file);
- } else {
- delete(file);
- }
- }
-
- delete(aDir);
- }
-
- /**
- * Deletes a file or directory. The test case will fail if the file or
- * directory does not exist or if deletion fails. Deletion of a non-empty
- * directory will always fail.
- *
- * @param aFile
- * File or directory to delete.
- */
- public static void delete(File aFile) {
- TestCase.assertTrue(aFile.delete());
- }
-
- /**
- * Gets a path relative to a sub project. This utility should be used to
- * easily access file paths within a subproject without requiring any
- * specific Eclipse configuration.
- *
- * @param aRelativePath
- * Relative path.
- * @param aTestClass
- * Test class.
- */
- public static File getPath(String aRelativePath, Class aTestClass) {
- CodeSource source = aTestClass.getProtectionDomain().getCodeSource();
- if (source == null) {
- LOG.warn("Could not obtain path for '" + aRelativePath
- + "' for class " + aTestClass
- + ", using relative path as is");
- return new File(aRelativePath);
- }
- URL location = source.getLocation();
- String protocol = location.getProtocol();
- if (!protocol.equals("file")) {
- LOG.warn("protocol is not 'file': " + location);
- return new File(aRelativePath);
- }
-
- String path = location.getPath();
- try {
- path = URLDecoder.decode(location.getPath(), "UTF-8");
- } catch (UnsupportedEncodingException e) {
- // ignore it.. just don't decode
- LOG.warn("Decoding path failed: '" + location.getPath() + "'", e );
- }
-
- return new File(new File(path).getParentFile(), aRelativePath);
- }
-
- /**
- * Ensures that a directory hierarchy exists (recursively if needed). If it
- * is not possible to create the directory, then the test case will fail.
- *
- * @param aDir
- * Directory to create.
- */
- public static void createDir(File aDir) {
- if (aDir.exists() && !aDir.isDirectory()) {
- TestCase.fail("'" + aDir
- + "' already exists and is not a directory");
- }
- if (aDir.exists()) {
- return;
- }
- createDir(aDir.getParentFile());
- TestCase.assertTrue("Could not create '" + aDir + "'", aDir.mkdir());
- }
-
- /**
- * Gets the test output directory for a specific test class.
- *
- * @param aTestClass
- * Test class.
- * @return Test output directory.
- */
- public static File getTestOutputDir(Class aTestClass) {
- File file = getPath(TEST_OUTPUT_DIR, aTestClass);
- String packageName = aTestClass.getPackage().getName();
- String packagePath = packageName.replaceAll("\\.", "/");
- return new File(file, packagePath);
- }
-
- /**
- * Gets the test input directory for a specific test class.
- *
- * @param aTestClass
- * Test class.
- * @return Test input directory.
- */
- public static File getTestInputDir(Class aTestClass) {
- File file = getPath(TEST_INPUT_DIR, aTestClass);
- String packageName = aTestClass.getPackage().getName();
- String packagePath = packageName.replaceAll("\\.", "/");
- return new File(file, packagePath);
- }
-
- /**
- * Creates a directory hierarchy for the output directory of a test class if
- * needed.
- *
- * @param aTestClass
- * Test class
- * @return Test directory.
- */
- public static File createTestOutputDir(Class aTestClass) {
- File file = getTestOutputDir(aTestClass);
- createDir(file);
- return file;
- }
-
- /**
- * Gets a test output file name. This returns a File object representing the
- * output file and ensures that the directory where the file will be created
- * already exists.
- *
- * @param aName
- * Name of the file.
- * @param aTestClass
- * Test class.
- * @return File.
- */
- public static File getTestOutputFile(String aName, Class aTestClass) {
- File file = new File(getTestOutputDir(aTestClass), aName);
- createDir(file.getParentFile());
- return file;
- }
-
- public static String read(InputStream aIs) throws IOException {
- try {
- StringBuffer buffer = new StringBuffer();
- int c;
- while ((c = aIs.read()) != -1) {
- buffer.append((char)c);
- }
- return buffer.toString();
- } finally {
- aIs.close();
- }
- }
-
- /**
- * Copies an input stream to an output stream.
- * @param aIs Input stream.
- * @param aOs Output stream.
+ private static final Log LOG = LogFactory.getLog(FileSystemUtils.class);
+
+ /**
+ * Test output directory relative to the sub project.
+ */
+ private static final String TEST_OUTPUT_DIR = "../target/testoutput";
+
+ /**
+ * Test input directory relative to the sub project.
+ */
+ private static final String TEST_INPUT_DIR = "../src/test/resources";
+
+ /*
+ * Disabled.
+ *
+ */
+ private FileSystemUtils() {
+ // Empty
+ }
+
+ /**
+ * Deletes a directory recursively. The test case will fail if the directory
+ * does not exist or if deletion fails.
+ *
+ * @param aDir
+ * Directory to delete.
+ */
+ public static void deleteDirRecursively(String aDir) {
+ deleteDirRecursively(new File(aDir));
+ }
+
+ /**
+ * Deletes a directory recursively. See {@link #deleteDirRecursively}.
+ *
+ * @param aDir
+ * Directory.
+ */
+ public static void deleteDirRecursively(File aDir) {
+ TestCase.assertTrue(aDir.isDirectory());
+
+ for (File file : aDir.listFiles()) {
+ if (file.isDirectory()) {
+ deleteDirRecursively(file);
+ } else {
+ delete(file);
+ }
+ }
+
+ delete(aDir);
+ }
+
+ /**
+ * Deletes a file or directory. The test case will fail if the file or
+ * directory does not exist or if deletion fails. Deletion of a non-empty
+ * directory will always fail.
+ *
+ * @param aFile
+ * File or directory to delete.
+ */
+ public static void delete(File aFile) {
+ TestCase.assertTrue(aFile.delete());
+ }
+
+ /**
+ * Gets a path relative to a sub project. This utility should be used to
+ * easily access file paths within a subproject without requiring any
+ * specific Eclipse configuration.
+ *
+ * @param aRelativePath
+ * Relative path.
+ * @param aTestClass
+ * Test class.
+ */
+ public static File getPath(String aRelativePath, Class aTestClass) {
+ CodeSource source = aTestClass.getProtectionDomain().getCodeSource();
+ if (source == null) {
+ LOG.warn("Could not obtain path for '" + aRelativePath
+ + "' for class " + aTestClass
+ + ", using relative path as is");
+ return new File(aRelativePath);
+ }
+ URL location = source.getLocation();
+ String protocol = location.getProtocol();
+ if (!protocol.equals("file")) {
+ LOG.warn("protocol is not 'file': " + location);
+ return new File(aRelativePath);
+ }
+
+ String path = location.getPath();
+ try {
+ path = URLDecoder.decode(location.getPath(), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // ignore it.. just don't decode
+ LOG.warn("Decoding path failed: '" + location.getPath() + "'", e);
+ }
+
+ return new File(new File(path).getParentFile(), aRelativePath);
+ }
+
+ /**
+ * Ensures that a directory hierarchy exists (recursively if needed). If it
+ * is not possible to create the directory, then the test case will fail.
+ *
+ * @param aDir
+ * Directory to create.
+ */
+ public static void createDir(File aDir) {
+ if (aDir.exists() && !aDir.isDirectory()) {
+ TestCase.fail("'" + aDir
+ + "' already exists and is not a directory");
+ }
+ if (aDir.exists()) {
+ return;
+ }
+ createDir(aDir.getParentFile());
+ TestCase.assertTrue("Could not create '" + aDir + "'", aDir.mkdir());
+ }
+
+ /**
+ * Creates a file in a directory.
+ * @param aDir Directory path. Will be created if it does not exist.
+ * @param aName Filename.
+ * @param aContents Contents.
+ */
+ public static void createFile(File aDir, String aName, InputStream aContents) {
+ createDir(aDir);
+ try {
+ OutputStream os = new FileOutputStream(new File(aDir, aName));
+ copyStream(aContents, os);
+ } catch (IOException e) {
+ e.printStackTrace();
+ TestCase.fail(e.getMessage());
+ }
+ }
+
+ /**
+ * Gets the test output directory for a specific test class.
+ *
+ * @param aTestClass
+ * Test class.
+ * @return Test output directory.
+ */
+ public static File getTestOutputDir(Class aTestClass) {
+ File file = getPath(TEST_OUTPUT_DIR, aTestClass);
+ String className = aTestClass.getName();
+ String classRelPath = className.replaceAll("\\.", "/");
+ return new File(file, classRelPath);
+ }
+
+ /**
+ * Gets the test input directory for a specific test class.
+ *
+ * @param aTestClass
+ * Test class.
+ * @return Test input directory.
+ */
+ public static File getTestInputDir(Class aTestClass) {
+ File file = getPath(TEST_INPUT_DIR, aTestClass);
+ String packageName = aTestClass.getPackage().getName();
+ String packagePath = packageName.replaceAll("\\.", "/");
+ return new File(file, packagePath);
+ }
+
+ /**
+ * Creates a directory hierarchy for the output directory of a test class if
+ * needed.
+ *
+ * @param aTestClass
+ * Test class
+ * @return Test directory.
+ */
+ public static File createTestOutputDir(Class aTestClass) {
+ File file = getTestOutputDir(aTestClass);
+ createDir(file);
+ return file;
+ }
+
+ /**
+ * Gets a test output file name. This returns a File object representing the
+ * output file and ensures that the directory where the file will be created
+ * already exists.
+ *
+ * @param aName
+ * Name of the file.
+ * @param aTestClass
+ * Test class.
+ * @return File.
+ */
+ public static File getTestOutputFile(String aName, Class aTestClass) {
+ File file = new File(getTestOutputDir(aTestClass), aName);
+ createDir(file.getParentFile());
+ return file;
+ }
+
+ public static String read(InputStream aIs) throws IOException {
+ try {
+ StringBuffer buffer = new StringBuffer();
+ int c;
+ while ((c = aIs.read()) != -1) {
+ buffer.append((char) c);
+ }
+ return buffer.toString();
+ } finally {
+ aIs.close();
+ }
+ }
+
+ /**
+ * Copies an input stream to an output stream.
+ *
+ * @param aIs
+ * Input stream.
+ * @param aOs
+ * Output stream.
*/
public static void copyStream(InputStream aIs, OutputStream aOs) {
try {
aOs.write(c);
}
aIs.close();
- aOs.close();
+ aOs.close();
} catch (IOException e) {
e.printStackTrace();
Assert.fail(e.getMessage());
+ aTarget.getPath() + " failed.", false);
}
}
-
+
/**
* Remove all files within a given directory including the directory itself.
* This only attempts to remove regular files and not directories within the
cleanDir(aDir);
delete(aDir);
}
-
+
/**
* Remove all regular files within a given directory.
*
*/
package org.wamblee.io;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import junit.framework.Assert;
/**
- * TestData provides a convenient interface for managing test output files.
+ * TestData provides a convenient interface for managing test output files.
*
* @author Erik Brakkee
*/
private File _root;
/**
- * Test data to be constructed in the setUp of a test.
- * {@link #clean()} must be called to make sure that this
- * directory is empty before executing a test.
+ * Test data to be constructed in the setUp of a test. {@link #clean()} must
+ * be called to make sure that this directory is empty before executing a
+ * test.
*/
public TestData(Object aTestcase) {
_testcase = aTestcase;
_root = getTestRootDir(aTestcase);
+ FileSystemUtils.createDir(_root);
}
/**
return FileSystemUtils.getTestOutputDir(aTestcase.getClass());
}
+ public void createFile(String aRelative, String aFile, InputStream aContents) {
+ FileSystemUtils
+ .createFile(new File(_root, aRelative), aFile, aContents);
+ }
+
+ public void createFile(String aFile, String aContents) {
+ createFile(".", aFile, aContents);
+ }
+
+ public void createFile(String aRelative, String aFile, String aContents) {
+ InputStream is = new ByteArrayInputStream(aContents.getBytes());
+ FileSystemUtils.createFile(new File(_root, aRelative), aFile, is);
+ }
+
+ public void deleteFile(String aFile) {
+ deleteFile(".", aFile);
+ }
+
+ public void deleteFile(String aRelative, String aFile) {
+ FileSystemUtils.delete(new File(_root, aFile));
+ }
+
/**
* Returns a temporary directory.
*
*/
public void clean() {
FileSystemUtils.deleteDirRecursively(_root);
+ FileSystemUtils.createDir(_root);
}
/**
public void copyDir(File aSrc) {
FileSystemUtils.copyDir(aSrc, _root);
}
-
+
/**
* Copies a classpath resource to a relative path in the test output
* directory.
/**
* Copies a resource to the root directory of the test output.
- * @param aResource Resource.
+ *
+ * @param aResource
+ * Resource.
*/
public void copyResource(String aResource) {
String basename = new File(aResource).getName();
copyResource(aResource, basename);
}
+ public void createDir(String aRelative) {
+ FileSystemUtils.createDir(new File(_root, aRelative));
+ }
+
/**
* Deletes a file or directory relative to the test output root.
*
}
/**
- * Deletes a directory including its contents.
- * @param aRelative Relative path.
+ * Deletes a directory including its contents.
+ *
+ * @param aRelative
+ * Relative path.
*/
- public void deleteDir(String aRelative) {
+ public void deleteDir(String aRelative) {
FileSystemUtils.deleteDir(new File(_root, aRelative));
}
/**
- * Deletes a directory recursively.
- * @param aRelative Relative path.
+ * Deletes a directory recursively.
+ *
+ * @param aRelative
+ * Relative path.
*/
- public void deleteDirRecursively(String aRelative) {
+ public void deleteDirRecursively(String aRelative) {
FileSystemUtils.deleteDir(new File(_root, aRelative));
}
-
+
+ /**
+ * Gets the root of the test output directory.
+ *
+ * @return Root of the test output.
+ */
+ public File getRoot() {
+ return _root;
+ }
+
/**
- * Gets a file object for a relative path.
+ * Gets a file object for a relative path.
*/
- public File getFile(String aRelative) {
- return new File(_root, aRelative);
+ public File getFile(String aRelative) {
+ return new File(_root, aRelative);
}
}