Don't crawl hidden files or directories.

This change avoids crawling hidden files or directories.

When crawling a directory, hidden files and subdirectories
are still included in the returned content listing, however
subsequent attempts to crawl them will return not found.

This change also verifies that the requested file/folder
does not reside within a hidden ancester directory.

Code Review: http://codereview.appspot.com/39390046
diff --git a/src/com/google/enterprise/adaptor/fs/FileDelegate.java b/src/com/google/enterprise/adaptor/fs/FileDelegate.java
index 6c087f5..6e68acf 100644
--- a/src/com/google/enterprise/adaptor/fs/FileDelegate.java
+++ b/src/com/google/enterprise/adaptor/fs/FileDelegate.java
@@ -48,6 +48,12 @@
   boolean isRegularFile(Path doc) throws IOException;
 
   /**
+   * Returns {@code true} if the specified path represents
+   * a hidden file or directory, {@code false} otherwise.
+   */
+  boolean isHidden(Path doc) throws IOException;
+
+  /**
    * Returns the {@link BasicFileAttributes} for the file or directory.
    *
    * @param doc the file/folder to get the {@link BasicFileAttributes} for
diff --git a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
index 7fd8cd4..fec5bed 100644
--- a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
+++ b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
@@ -283,17 +283,13 @@
       return;
     }
 
-    if (!isDescendantOfRoot(doc)) {
-      log.log(Level.WARNING,
-          "Skipping {0} since it is not a descendant of {1}.",
-          new Object[] { doc, rootPath });
+    if (!isVisibleDescendantOfRoot(doc)) {
       resp.respondNotFound();
       return;
     }
 
     if (!isSupportedPath(doc)) {
-      log.log(Level.WARNING, "The path {0} is not a supported file type.",
-          doc);
+      log.log(Level.WARNING, "The path {0} is not a supported file type.", doc);
       resp.respondNotFound();
       return;
     }
@@ -426,13 +422,29 @@
     return delegate.isRegularFile(p) || delegate.isDirectory(p);
   }
 
-  private boolean isDescendantOfRoot(Path file) {
-    while (file != null) {
+  /**
+   * Verifies that the file is a descendant of the root directory,
+   * and that it, nor none of its ancestors, is hidden.
+   */
+  private boolean isVisibleDescendantOfRoot(Path doc) throws IOException {
+    for (Path file = doc; file != null; file = file.getParent()) {
+      if (delegate.isHidden(file)) {
+        if (doc.equals(file)) {
+          log.log(Level.WARNING, "Skipping {0} because it is hidden.", doc);
+        } else {
+          log.log(Level.WARNING,
+              "Skipping {0} because it is hidden under {1}.",
+              new Object[] { doc, file });
+        }
+        return false;
+      }
       if (file.equals(rootPath)) {
         return true;
       }
-      file = file.getParent();
     }
+    log.log(Level.WARNING,
+        "Skipping {0} because it is not a descendant of {1}.",
+        new Object[] { doc, rootPath });
     return false;
   }
 
diff --git a/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java b/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java
index ab796b0..674f960 100644
--- a/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java
+++ b/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java
@@ -51,6 +51,11 @@
   }
 
   @Override
+  public boolean isHidden(Path doc) throws IOException {
+    return Files.isHidden(doc);
+  }
+
+  @Override
   public BasicFileAttributes readBasicAttributes(Path doc) throws IOException {
     return Files.readAttributes(doc, BasicFileAttributes.class);
   }