From c072937fcb725b6e374d5fcb2f436c1af06c1ac3 Mon Sep 17 00:00:00 2001
From: tpylak <tpylak>
Date: Fri, 12 Oct 2007 15:12:49 +0000
Subject: [PATCH] file abstraction for incoming data store

SVN: 2144
---
 .../utilities/DirectoryScanningTimerTask.java | 188 +++++++++++++-----
 .../cisd/common/utilities/FileUtilities.java  |   6 +
 .../cisd/common/utilities/IStoreHandler.java  |  27 +++
 .../cisd/common/utilities/StoreItem.java      |  58 ++++++
 .../DirectoryScanningTimerTaskTest.java       |  73 +------
 .../common/utilities/FileUtilitiesTest.java   |  56 +++++-
 6 files changed, 284 insertions(+), 124 deletions(-)
 create mode 100644 common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java
 create mode 100644 common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java

diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java b/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java
index 9363d1fd3f7..bd27877a4fd 100644
--- a/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java
+++ b/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java
@@ -18,7 +18,6 @@ package ch.systemsx.cisd.common.utilities;
 
 import java.io.File;
 import java.io.FileFilter;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.TimerTask;
@@ -26,7 +25,6 @@ import java.util.TimerTask;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 
-import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
 import ch.systemsx.cisd.common.logging.ISimpleLogger;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
@@ -41,10 +39,10 @@ import ch.systemsx.cisd.common.logging.LogFactory;
  * 
  * @author Bernd Rinn
  */
-public final class DirectoryScanningTimerTask extends TimerTask implements ISelfTestable
+public final class DirectoryScanningTimerTask extends TimerTask
 {
 
-    public static final String FAULTY_PATH_FILENAME = ".faulty_paths";
+    static final String FAULTY_PATH_FILENAME = ".faulty_paths";
 
     private static final Logger operationLog =
             LogFactory.getLogger(LogCategory.OPERATION, DirectoryScanningTimerTask.class);
@@ -52,26 +50,33 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
     private static final Logger notificationLog =
             LogFactory.getLogger(LogCategory.NOTIFY, DirectoryScanningTimerTask.class);
 
-    private static final IFromStringConverter<File> FILE_CONVERTER = new IFromStringConverter<File>()
-        {
-            public File fromString(String value)
-            {
-                return new File(value);
-            }
-        };
+    public static interface IScannedStore
+    {
+        /**
+         * List items in the scanned store in order in which they should be handled.
+         */
+        StoreItem[] tryListSortedReadyToProcess(ISimpleLogger loggerOrNull);
 
-    private final IPathHandler handler;
+        boolean exists(StoreItem item);
 
-    private final File sourceDirectory;
+        /**
+         * returned description should give the user the idea about file location. You should not use the result for
+         * something else than printing it for user. It should not be especially assumed that the result is the path
+         * which could be used in java.io.File constructor.
+         */
+        String getLocationDescription(StoreItem item);
+    }
+
+    private final IStoreHandler handler;
+
+    private final IScannedStore sourceDirectory;
 
     /** The number of consecutive errors of reading a directory that need to occur before the event is logged. */
     private final int ignoredErrorCount;
 
     private int errorCountReadingDirectory;
 
-    private final FileFilter filter;
-
-    private final Set<File> faultyPaths;
+    private final Set<String> faultyPaths;
 
     private final File faultyPathsFile;
 
@@ -102,20 +107,101 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
     public DirectoryScanningTimerTask(File sourceDirectory, FileFilter filter, IPathHandler handler,
             int ignoredErrorCount)
     {
-        assert sourceDirectory != null;
-        assert filter != null;
+        this(asScannedStore(sourceDirectory, filter), sourceDirectory, asScanningHandler(sourceDirectory, handler),
+                ignoredErrorCount);
+    }
+
+    /**
+     * Creates a <var>DirectoryScanningTimerTask</var>.
+     * 
+     * @param scannedStore The store which is scan for entries.
+     * @param faultyPathDirectory The directory in which file with faulty paths is should be stored.
+     * @param handler The handler that is used for treating the matching paths.
+     * @param ignoredErrorCount The number of consecutive errors of reading the directory that need to occur before the
+     *            next error is logged (can be used to suppress error when the directory is on a remote share and the
+     *            server is flaky sometimes)
+     */
+    public DirectoryScanningTimerTask(IScannedStore scannedStore, File faultyPathDirectory, IStoreHandler handler,
+            int ignoredErrorCount)
+    {
+        assert scannedStore != null;
         assert handler != null;
         assert ignoredErrorCount >= 0;
 
         this.ignoredErrorCount = ignoredErrorCount;
-        this.sourceDirectory = sourceDirectory;
-        this.filter = filter;
+        this.sourceDirectory = scannedStore;
         this.handler = handler;
-        this.faultyPaths = new HashSet<File>();
-        this.faultyPathsFile = new File(sourceDirectory, FAULTY_PATH_FILENAME);
+        this.faultyPaths = new HashSet<String>();
+        this.faultyPathsFile = new File(faultyPathDirectory, FAULTY_PATH_FILENAME);
         faultyPathsFile.delete();
     }
 
+    private static IStoreHandler asScanningHandler(final File directory, final IPathHandler handler)
+    {
+        return new IStoreHandler()
+            {
+                public void handle(StoreItem item)
+                {
+                    File path = asFile(directory, item);
+                    handler.handle(path);
+                }
+            };
+    }
+
+    private static IScannedStore asScannedStore(final File directory, final FileFilter filter)
+    {
+        return new IScannedStore()
+            {
+                public String getLocationDescription(StoreItem item)
+                {
+                    return DirectoryScanningTimerTask.getLocationDescription(asFile(item));
+                }
+
+                public boolean exists(StoreItem item)
+                {
+                    return asFile(item).exists();
+                }
+
+                public StoreItem[] tryListSortedReadyToProcess(ISimpleLogger loggerOrNull)
+                {
+                    File[] files = FileUtilities.tryListFiles(directory, filter, loggerOrNull);
+                    if (files != null)
+                    {
+                        FileUtilities.sortByLastModified(files);
+                        return asItems(files);
+                    } else
+                    {
+                        return null;
+                    }
+                }
+
+                private StoreItem[] asItems(File[] files)
+                {
+                    StoreItem[] items = new StoreItem[files.length];
+                    for (int i = 0; i < items.length; i++)
+                    {
+                        items[i] = new StoreItem(files[i].getName());
+                    }
+                    return items;
+                }
+
+                private File asFile(StoreItem item)
+                {
+                    return DirectoryScanningTimerTask.asFile(directory, item);
+                }
+            };
+    }
+
+    private static String getLocationDescription(File file)
+    {
+        return file.getPath();
+    }
+
+    private static File asFile(File parentDirectory, StoreItem item)
+    {
+        return new File(parentDirectory, item.getName());
+    }
+
     /**
      * Handles all entries in the source directory that are picked by the filter.
      */
@@ -129,13 +215,10 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
                 operationLog.trace("Start scanning directory " + sourceDirectory + ".");
             }
             checkForFaultyPathsFileChanged();
-            final File[] paths = listFiles();
-            // Sort in order of "oldest first" in order to move older items before newer items. This becomes important
-            // when doing online quality control of measurements.
-            Arrays.sort(paths, FileComparator.BY_LAST_MODIFIED);
-            for (File path : paths)
+            final StoreItem[] paths = listFiles();
+            for (StoreItem path : paths)
             {
-                if (faultyPathsFile.equals(path)) // Never touch the faultyPathsFile.
+                if (isFaultyPathsFile(path)) // Never touch the faultyPathsFile.
                 {
                     continue;
                 }
@@ -151,6 +234,13 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
         }
     }
 
+    private boolean isFaultyPathsFile(StoreItem item)
+    {
+        String itemLocation = sourceDirectory.getLocationDescription(item);
+        String faultyPathsLocation = getLocationDescription(faultyPathsFile);
+        return itemLocation.equals(faultyPathsLocation);
+    }
+
     private void checkForFaultyPathsFileChanged()
     {
         if (faultyPathsFile.exists())
@@ -158,12 +248,12 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
             if (faultyPathsFile.lastModified() > faultyPathsLastChanged) // Handles manual manipulation.
             {
                 faultyPaths.clear();
-                CollectionIO.readCollection(faultyPathsFile, faultyPaths, FILE_CONVERTER);
+                CollectionIO.readCollection(faultyPathsFile, faultyPaths);
                 faultyPathsLastChanged = faultyPathsFile.lastModified();
                 if (operationLog.isInfoEnabled())
                 {
                     operationLog.info(String.format("Reread faulty paths file (%s), new set contains %d entries",
-                            faultyPathsFile.getPath(), faultyPaths.size()));
+                            getLocationDescription(faultyPathsFile), faultyPaths.size()));
                 }
             }
         } else
@@ -173,7 +263,7 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
         }
     }
 
-    private File[] listFiles()
+    private StoreItem[] listFiles()
     {
         final boolean logNotifyError = (errorCountReadingDirectory == ignoredErrorCount); // Avoid mailbox flooding.
         final boolean logOperationError = (errorCountReadingDirectory < ignoredErrorCount);
@@ -181,7 +271,7 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
                 logNotifyError ? createSimpleErrorLogger(LogCategory.NOTIFY)
                         : (logOperationError ? createSimpleErrorLogger(LogCategory.OPERATION) : null);
 
-        final File[] paths = FileUtilities.tryListFiles(sourceDirectory, filter, errorLogger);
+        final StoreItem[] paths = sourceDirectory.tryListSortedReadyToProcess(errorLogger);
         if (errorCountReadingDirectory > ignoredErrorCount && paths != null)
         {
             if (notificationLog.isInfoEnabled())
@@ -196,7 +286,7 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
         {
             errorCountReadingDirectory = 0;
         }
-        return (paths == null) ? new File[0] : paths;
+        return (paths == null) ? new StoreItem[0] : paths;
     }
 
     private ISimpleLogger createSimpleErrorLogger(final LogCategory category)
@@ -221,41 +311,35 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf
             };
     }
 
-    private void handle(File path)
+    private void handle(StoreItem item)
     {
-        if (faultyPaths.contains(path))
+        if (isFaultyPath(item))
         { // Guard: skip faulty paths.
             return;
         }
         try
         {
-            handler.handle(path);
+            handler.handle(item);
         } finally
         {
-            if (path.exists())
+            if (sourceDirectory.exists(item))
             {
-                addToFaultyPaths(path);
+                addToFaultyPaths(item);
             }
         }
     }
 
-    private void addToFaultyPaths(File path)
+    private boolean isFaultyPath(StoreItem item)
     {
-        faultyPaths.add(path);
-        CollectionIO.writeIterable(faultyPathsFile, faultyPaths);
-        faultyPathsLastChanged = faultyPathsFile.lastModified();
+        String path = sourceDirectory.getLocationDescription(item);
+        return faultyPaths.contains(path);
     }
 
-    public void check() throws ConfigurationFailureException
+    private void addToFaultyPaths(StoreItem item)
     {
-        if (operationLog.isDebugEnabled())
-        {
-            operationLog.debug("Checking source directory '" + sourceDirectory.getAbsolutePath() + "'.");
-        }
-        final String errorMessage = FileUtilities.checkDirectoryFullyAccessible(sourceDirectory, "source");
-        if (errorMessage != null)
-        {
-            throw new ConfigurationFailureException(errorMessage);
-        }
+        String path = sourceDirectory.getLocationDescription(item);
+        faultyPaths.add(path);
+        CollectionIO.writeIterable(faultyPathsFile, faultyPaths);
+        faultyPathsLastChanged = faultyPathsFile.lastModified();
     }
 }
diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java b/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java
index ce557ab13cd..8432fc111eb 100644
--- a/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java
+++ b/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java
@@ -30,6 +30,7 @@ import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -633,6 +634,11 @@ public final class FileUtilities
         return paths;
     }
 
+    public static void sortByLastModified(File[] files)
+    {
+        Arrays.sort(files, FileComparator.BY_LAST_MODIFIED);
+    }
+
     private static void logFailureInDirectoryListing(RuntimeException exOrNull, File directory, ISimpleLogger logger)
     {
         if (exOrNull == null)
diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java b/common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java
new file mode 100644
index 00000000000..7918fa7537e
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.common.utilities;
+
+/**
+ * Handles items in the file store
+ * 
+ * @author Tomasz Pylak on Oct 9, 2007
+ */
+public interface IStoreHandler
+{
+    void handle(StoreItem item);
+}
diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java b/common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java
new file mode 100644
index 00000000000..b2d95b86bbe
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.common.utilities;
+
+/**
+ * Represents one entry (file or directory) in a file store
+ * 
+ * @author Tomasz Pylak on Oct 8, 2007
+ */
+public class StoreItem
+{
+    public static final StoreItem[] EMPTY_ARRAY = new StoreItem[0];
+
+    private final String name;
+
+    public StoreItem(String name)
+    {
+        this.name = name;
+    }
+
+    /** Should not be used for logging. Use toString() instead. */
+    public String getName()
+    {
+        return name;
+    }
+
+    @Override
+    public String toString()
+    {
+        return name;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        return obj != null && obj instanceof StoreItem && name.equals(((StoreItem) obj).name);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return name.hashCode();
+    }
+}
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java
index 969ec93914a..13494e863b3 100644
--- a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java
@@ -16,8 +16,8 @@
 
 package ch.systemsx.cisd.common.utilities;
 
-import static org.testng.AssertJUnit.assertEquals;
 import static ch.systemsx.cisd.common.utilities.FileUtilities.ACCEPT_ALL_FILTER;
+import static org.testng.AssertJUnit.assertEquals;
 
 import java.io.File;
 import java.io.FileFilter;
@@ -31,14 +31,9 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogInitializer;
 import ch.systemsx.cisd.common.logging.LogMonitoringAppender;
-import ch.systemsx.cisd.common.utilities.CollectionIO;
-import ch.systemsx.cisd.common.utilities.DirectoryScanningTimerTask;
-import ch.systemsx.cisd.common.utilities.FileUtilities;
-import ch.systemsx.cisd.common.utilities.StoringUncaughtExceptionHandler;
 
 /**
  * Test cases for the {@link DirectoryScanningTimerTask}.
@@ -119,60 +114,6 @@ public class DirectoryScanningTimerTaskTest
         exceptionHandler.checkAndRethrowException();
     }
 
-    @Test(expectedExceptions =
-        { ConfigurationFailureException.class })
-    public void testFailedConstructionNonExistent()
-    {
-        final File nonExistentFile = new File(unitTestRootDirectory, "non-existent");
-        nonExistentFile.delete();
-        final DirectoryScanningTimerTask task =
-                new DirectoryScanningTimerTask(nonExistentFile, ACCEPT_ALL_FILTER, mockPathHandler);
-        task.check();
-    }
-
-    @Test(expectedExceptions =
-        { ConfigurationFailureException.class })
-    public void testFailedConstructionFileInsteadOfDirectory() throws IOException
-    {
-        final File file = new File(unitTestRootDirectory, "existent_file");
-        file.delete();
-        file.deleteOnExit();
-        file.createNewFile();
-        final DirectoryScanningTimerTask task =
-                new DirectoryScanningTimerTask(file, ACCEPT_ALL_FILTER, mockPathHandler);
-        task.check();
-    }
-
-    @Test(groups =
-        { "requires_unix" }, expectedExceptions =
-        { ConfigurationFailureException.class })
-    public void testFailedConstructionReadOnly() throws IOException, InterruptedException
-    {
-        final File readOnlyDirectory = new File(unitTestRootDirectory, "read_only_directory");
-        readOnlyDirectory.delete();
-        readOnlyDirectory.mkdir();
-        readOnlyDirectory.deleteOnExit();
-        assert readOnlyDirectory.setReadOnly();
-
-        try
-        {
-            // Here we should get an AssertationError
-            final DirectoryScanningTimerTask task =
-                    new DirectoryScanningTimerTask(readOnlyDirectory, ACCEPT_ALL_FILTER, mockPathHandler);
-            task.check();
-        } finally
-        {
-            // Unfortunately, with JDK 5 there is no portable way to set a file or directory read/write, once
-            // it has been set read-only, thus this test 'requires_unix' for the time being.
-            Runtime.getRuntime().exec(String.format("/bin/chmod u+w %s", readOnlyDirectory.getPath())).waitFor();
-            if (readOnlyDirectory.canWrite() == false)
-            {
-                // Can't use assert here since we expect an AssertationError
-                throw new IllegalStateException();
-            }
-        }
-    }
-
     @Test
     public void testFaultyPathsDeletion()
     {
@@ -219,7 +160,8 @@ public class DirectoryScanningTimerTaskTest
         assert someFile.exists();
         final DirectoryScanningTimerTask scanner =
                 new DirectoryScanningTimerTask(workingDirectory, ACCEPT_ALL_FILTER, mockPathHandler);
-        CollectionIO.writeIterable(faultyPaths, Collections.singleton(someFile));
+        String fileLocation = someFile.getPath();
+        CollectionIO.writeIterable(faultyPaths, Collections.singleton(fileLocation));
         scanner.run();
         assertEquals(0, mockPathHandler.handledPaths.size());
     }
@@ -340,8 +282,7 @@ public class DirectoryScanningTimerTaskTest
             scanner.run();
             appender.verifyLogHasHappened();
             dir.delete();
-        }
-        finally
+        } finally
         {
             LogMonitoringAppender.removeAppender(appender);
         }
@@ -385,9 +326,9 @@ public class DirectoryScanningTimerTaskTest
         final LogMonitoringAppender appenderNotifyError =
                 LogMonitoringAppender.addAppender(LogCategory.NOTIFY, "Failed to get listing of directory");
         final LogMonitoringAppender appenderOperationError =
-            LogMonitoringAppender.addAppender(LogCategory.OPERATION, "Failed to get listing of directory");
+                LogMonitoringAppender.addAppender(LogCategory.OPERATION, "Failed to get listing of directory");
         final LogMonitoringAppender appenderOK =
-            LogMonitoringAppender.addAppender(LogCategory.NOTIFY, "' is available again");
+                LogMonitoringAppender.addAppender(LogCategory.NOTIFY, "' is available again");
         try
         {
             final int numberOfErrorsToIgnore = 2;
@@ -435,7 +376,7 @@ public class DirectoryScanningTimerTaskTest
             final int numberOfErrorsToIgnore = 2;
             // The directory needs to exist when the scanner is created, otherwise the self-test will fail.
             final DirectoryScanningTimerTask scanner =
-                new DirectoryScanningTimerTask(dir, ACCEPT_ALL_FILTER, mockPathHandler, numberOfErrorsToIgnore);
+                    new DirectoryScanningTimerTask(dir, ACCEPT_ALL_FILTER, mockPathHandler, numberOfErrorsToIgnore);
             dir.delete();
             assert dir.exists() == false;
             // First error -> ignored
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java
index 5b32c76aaf5..b976ee2d8c5 100644
--- a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java
@@ -64,7 +64,51 @@ public class FileUtilitiesTest
     {
         FileUtils.cleanDirectory(workingDirectory);
     }
-    
+
+    @Test
+    public void testFailedConstructionNonExistent()
+    {
+        final File nonExistentFile = new File(workingDirectory, "non-existent");
+        nonExistentFile.delete();
+        String errorMsg = FileUtilities.checkDirectoryFullyAccessible(nonExistentFile, "test");
+        assertNotNull(errorMsg);
+    }
+
+    @Test
+    public void testFailedConstructionFileInsteadOfDirectory() throws IOException
+    {
+        final File file = new File(workingDirectory, "existent_file");
+        file.delete();
+        file.deleteOnExit();
+        file.createNewFile();
+        String errorMsg = FileUtilities.checkDirectoryFullyAccessible(file, "test");
+        assertNotNull(errorMsg);
+    }
+
+    @Test(groups =
+        { "requires_unix" })
+    public void testFailedConstructionReadOnly() throws IOException, InterruptedException
+    {
+        final File readOnlyDirectory = new File(workingDirectory, "read_only_directory");
+        readOnlyDirectory.delete();
+        readOnlyDirectory.mkdir();
+        readOnlyDirectory.deleteOnExit();
+        assert readOnlyDirectory.setReadOnly();
+
+        String errorMsg = FileUtilities.checkDirectoryFullyAccessible(readOnlyDirectory, "test");
+
+        // --- clean before checking results
+        // Unfortunately, with JDK 5 there is no portable way to set a file or directory read/write, once
+        // it has been set read-only, thus this test 'requires_unix' for the time being.
+        Runtime.getRuntime().exec(String.format("/bin/chmod u+w %s", readOnlyDirectory.getPath())).waitFor();
+        if (readOnlyDirectory.canWrite() == false)
+        {
+            // Can't use assert here since we expect an AssertationError
+            throw new IllegalStateException();
+        }
+        assertNotNull(errorMsg);
+    }
+
     @Test
     public void testCopyFile() throws Exception
     {
@@ -77,7 +121,7 @@ public class FileUtilitiesTest
         assertEquals(FileUtilities.loadToString(sourceFile), FileUtilities.loadToString(destinationFile));
         assertEquals(47110000, destinationFile.lastModified());
     }
-    
+
     @Test
     public void testWriteToFile() throws Exception
     {
@@ -94,7 +138,7 @@ public class FileUtilitiesTest
         FileUtilities.writeToFile(file, "Hello world");
         assertEquals("Hello world\n", FileUtilities.loadToString(file));
     }
-    
+
     @Test
     public void testWriteToExistingDirectory() throws Exception
     {
@@ -112,7 +156,7 @@ public class FileUtilitiesTest
             assertTrue("Exception message not as expected: " + message, cause.getMessage().startsWith(dir.toString()));
         }
     }
-    
+
     @Test
     public void testWriteToExistingReadOnlyFile() throws Exception
     {
@@ -134,7 +178,7 @@ public class FileUtilitiesTest
             assert file.delete();
         }
     }
-    
+
     @Test
     public void testLoadToStringFile() throws Exception
     {
@@ -271,7 +315,7 @@ public class FileUtilitiesTest
         newFile = FileUtilities.createNextNumberedFile(file, pattern, "12abc_[1]");
         assertEquals(new File(workingDirectory, "12abc_[13]"), newFile);
         newFile = FileUtilities.createNextNumberedFile(file, Pattern.compile("xxx(\\d+)xxx"), "12abc_[1]");
-        assertEquals(new File(workingDirectory, "12abc_[1]"), newFile);        
+        assertEquals(new File(workingDirectory, "12abc_[1]"), newFile);
     }
 
     @Test
-- 
GitLab