Merge branch 'master' of https://code.google.com/p/plexi.fs
diff --git a/lib/plexi b/lib/plexi
index ef7a357..7bb4377 160000
--- a/lib/plexi
+++ b/lib/plexi
@@ -1 +1 @@
-Subproject commit ef7a357a5c3171f0a9e4fac117e6446b4820cf2a
+Subproject commit 7bb4377649cc5426db2ce26e883c695f2935a040
diff --git a/src/com/google/enterprise/adaptor/fs/FileDelegate.java b/src/com/google/enterprise/adaptor/fs/FileDelegate.java
index 1bdd715..a480cb0 100644
--- a/src/com/google/enterprise/adaptor/fs/FileDelegate.java
+++ b/src/com/google/enterprise/adaptor/fs/FileDelegate.java
@@ -61,6 +61,13 @@
   BasicFileAttributes readBasicAttributes(Path doc) throws IOException;
 
   /**
+   * Gets the lastAccess time for the file or directory.
+   *
+   * @param doc the file/folder to set the last accessed time on
+   */
+  FileTime getLastAccessTime(Path doc) throws IOException;
+
+  /**
    * Sets the lastAccess time for the file or directory.
    *
    * @param doc the file/folder to set the last accessed time on
diff --git a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
index efd8fae..609588b 100644
--- a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
+++ b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
@@ -34,12 +34,14 @@
 import com.google.enterprise.adaptor.Response;
 import com.google.enterprise.adaptor.StartupException;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.nio.charset.Charset;
 import java.nio.file.AccessDeniedException;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.attribute.AclFileAttributeView;
 import java.nio.file.attribute.BasicFileAttributes;
@@ -206,7 +208,7 @@
 
   @Override
   public void initConfig(Config config) {
-    config.addKey(CONFIG_SRC, "");
+    config.addKey(CONFIG_SRC, null);
     config.addKey(CONFIG_SUPPORTED_ACCOUNTS,
         "BUILTIN\\Administrators,Everyone,BUILTIN\\Users,BUILTIN\\Guest,"
         + "NT AUTHORITY\\INTERACTIVE,NT AUTHORITY\\Authenticated Users");
@@ -454,14 +456,6 @@
     DocId id = req.getDocId();
     Path doc = delegate.getPath(id.getUniqueId());
 
-    if (!isSupportedPath(doc)) {
-      log.log(Level.WARNING, "The path {0} is not a supported file type.", doc);
-      resp.respondNotFound();
-      return;
-    }
-
-    final boolean docIsDirectory = delegate.isDirectory(doc);
-
     if (!id.equals(delegate.newDocId(doc))) {
       log.log(Level.WARNING,
           "The {0} is not a valid id generated by the adaptor.", id);
@@ -475,18 +469,38 @@
     }
 
     // Populate the document metadata.
-    BasicFileAttributes attrs = delegate.readBasicAttributes(doc);
-    final FileTime lastAccessTime = attrs.lastAccessTime();
+    BasicFileAttributes attrs;
+    try {
+      attrs = delegate.readBasicAttributes(doc);
+    } catch (FileNotFoundException e) {
+      log.log(Level.INFO, "Not found: {0}", doc);
+      resp.respondNotFound();
+      return;
+    } catch (NoSuchFileException e) {
+      log.log(Level.INFO, "Not found: {0}", doc);
+      resp.respondNotFound();
+      return;
+    }      
+
+    if (!isFileOrFolder(doc)) {
+      log.log(Level.INFO, "The path {0} is not a regular file or directory.",
+              doc);
+      resp.respondNotFound();
+      return;
+    }
+
+    final boolean docIsDirectory = attrs.isDirectory();
+    final FileTime lastAccessTime = delegate.getLastAccessTime(doc);
 
     if (!docIsDirectory) {
       if (lastAccessTimeFilter.excluded(lastAccessTime)) {
-        log.log(Level.WARNING, "Skipping {0} because it was last accessed {1}.",
+        log.log(Level.FINE, "Skipping {0} because it was last accessed {1}.",
             new Object[] {doc, lastAccessTime.toString().substring(0, 10)});
         resp.respondNotFound();
         return;
       }
       if (lastModifiedTimeFilter.excluded(attrs.lastModifiedTime())) {
-        log.log(Level.WARNING, "Skipping {0} because it was last modified {1}.",
+        log.log(Level.FINE, "Skipping {0} because it was last modified {1}.",
             new Object[] {doc, 
                 attrs.lastModifiedTime().toString().substring(0, 10)});
         resp.respondNotFound();
@@ -587,7 +601,7 @@
       HtmlResponseWriter writer = createHtmlResponseWriter(resp);
       writer.start(id, getFileName(doc));
       for (Path file : delegate.newDirectoryStream(doc)) {
-        if (isSupportedPath(file)) {
+        if (isFileOrFolder(file)) {
           writer.addLink(delegate.newDocId(file), getFileName(file));
         }
       }
@@ -600,13 +614,18 @@
         try {
           input.close();
         } finally {
-          try {
-            delegate.setLastAccessTime(doc, lastAccessTime);
-          } catch (IOException e) {
-            // This failure can be expected. We can have full permissions
-            // to read but not write/update permissions.
-            log.log(Level.CONFIG,
-                "Unable to restore last access time for {0}.", doc);
+          // Do a follow up check to see if the last access time has changed.
+          // If the last access time has changed, attempt to reset it.
+          if (!lastAccessTime.equals(delegate.getLastAccessTime(doc))) {
+            log.log(Level.FINE, "Restoring last access time for {0}.", doc);
+            try {
+              delegate.setLastAccessTime(doc, lastAccessTime);
+            } catch (IOException e) {
+              // This failure can be expected. We can have full permissions
+              // to read but not write/update permissions.
+              log.log(Level.FINE,
+                  "Unable to restore last access time for {0}.", doc);
+            }
           }
         }
       }
@@ -631,8 +650,12 @@
     return name.isEmpty() ? file.getRoot().toString() : name;
   }
 
+  /**
+   * Returns true if the path is a regular file or a folder;
+   * false if the path is a link, a special file, or doesn't exist.
+   */
   @VisibleForTesting
-  boolean isSupportedPath(Path p) throws IOException {
+  boolean isFileOrFolder(Path p) throws IOException {
     return delegate.isRegularFile(p) || delegate.isDirectory(p);
   }
 
diff --git a/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java b/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java
index 84ab250..ebb737e 100644
--- a/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java
+++ b/src/com/google/enterprise/adaptor/fs/NioFileDelegate.java
@@ -64,6 +64,12 @@
   }
 
   @Override
+  public FileTime getLastAccessTime(Path doc) throws IOException {
+    return (FileTime)Files.getAttribute(doc, "lastAccessTime",
+        LinkOption.NOFOLLOW_LINKS);
+  }
+
+  @Override
   public void setLastAccessTime(Path doc, FileTime time) throws IOException {
     Files.setAttribute(doc, "lastAccessTime", time, LinkOption.NOFOLLOW_LINKS);
   }
diff --git a/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java b/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java
index f71296c..90c3b06 100644
--- a/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java
+++ b/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java
@@ -288,10 +288,9 @@
 
     Netapi32Ex.SHARE_INFO_502 info =
         new Netapi32Ex.SHARE_INFO_502(buf.getValue());
-    netapi32.NetApiBufferFree(buf.getValue());
-
     WinNT.SECURITY_DESCRIPTOR_RELATIVE sdr =
         new WinNT.SECURITY_DESCRIPTOR_RELATIVE(info.shi502_security_descriptor);
+    netapi32.NetApiBufferFree(buf.getValue());
     WinNT.ACL dacl = sdr.getDiscretionaryACL();
 
     ImmutableList.Builder<AclEntry> builder = ImmutableList.builder();
diff --git a/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java b/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java
index ff23659..19bedfa 100644
--- a/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java
+++ b/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java
@@ -43,14 +43,18 @@
 import com.sun.jna.ptr.PointerByReference;
 import com.sun.jna.win32.W32APIOptions;
 
+import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.attribute.AclEntry;
 import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.FileTime;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -82,6 +86,29 @@
   }
 
   @Override
+  public InputStream newInputStream(Path doc) throws IOException {
+    return new BufferedInputStream(new WinFileInputStream(doc));
+  }
+
+  @Override
+  public FileTime getLastAccessTime(Path doc) throws IOException {
+    HANDLE handle = kernel32.CreateFile(doc.toString(), WinNT.GENERIC_READ,
+        WinNT.FILE_SHARE_READ | WinNT.FILE_SHARE_WRITE,
+        new WinBase.SECURITY_ATTRIBUTES(), WinNT.OPEN_EXISTING,
+        WinNT.FILE_ATTRIBUTE_NORMAL, null);
+      if (Kernel32.INVALID_HANDLE_VALUE.equals(handle)) {
+        throw new IOException("Unable to open " + doc
+            + ". GetLastError: " + kernel32.GetLastError());
+      }
+
+    WinBase.FILETIME.ByReference accessTime =
+        new WinBase.FILETIME.ByReference();
+    kernel32.GetFileTime(handle, null, accessTime, null);
+    kernel32.CloseHandle(handle);
+    return FileTime.fromMillis(accessTime.toDate().getTime());
+  }
+
+  @Override
   public AclFileAttributeViews getAclViews(Path doc) throws IOException {
     return aclViews.getAclViews(doc);
   }
@@ -474,4 +501,55 @@
   public void destroy() {
     stopMonitorPath();
   }
+
+  private class WinFileInputStream extends InputStream {
+    private final HANDLE handle;
+
+    public WinFileInputStream(Path path) throws IOException {
+      handle = kernel32.CreateFile(path.toString(),
+          WinNT.GENERIC_READ | WinNT.GENERIC_WRITE,
+          WinNT.FILE_SHARE_READ | WinNT.FILE_SHARE_WRITE,
+          new WinBase.SECURITY_ATTRIBUTES(),
+          WinNT.OPEN_EXISTING, WinNT.FILE_ATTRIBUTE_NORMAL, null);
+      if (Kernel32.INVALID_HANDLE_VALUE.equals(handle)) {
+        throw new IOException("Unable to open " + path
+            + ". GetLastError: " + kernel32.GetLastError());
+      }
+
+      // Call SetFileTime with a value of 0xFFFFFFFF for lpLastAccessTime
+      // to keep Windows from updating the last access time
+      // when reading the file content.
+      WinBase.FILETIME ft = new WinBase.FILETIME();
+      ft.dwHighDateTime = 0xFFFFFFFF;
+      ft.dwLowDateTime = 0xFFFFFFFF;
+      kernel32.SetFileTime(handle, null, ft, null);
+    }
+
+    @Override
+    public int read() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int read(byte[] inBuf, int start, int count) throws IOException {
+      IntByReference lpNumberOfBytesRead = new IntByReference(0);
+      boolean result = kernel32.ReadFile(handle,
+          ByteBuffer.wrap(inBuf, start, count), count,
+          lpNumberOfBytesRead, null);
+      if (!result) {
+        throw new IOException("Unable to read file data. "
+            + "GetLastError: " + kernel32.GetLastError());
+      }
+      if (lpNumberOfBytesRead.getValue() != 0) {
+        return lpNumberOfBytesRead.getValue();
+      } else {
+        return -1;
+      }
+    }
+
+    @Override
+    public void close() throws IOException {
+      kernel32.CloseHandle(handle);
+    }
+  }
 }
diff --git a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
index c756af8..6a5b25f 100644
--- a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
@@ -202,13 +202,13 @@
   }
 
   @Test
-  public void testIsSupportedPath() throws Exception {
+  public void testIsFileOrFolder() throws Exception {
     root.addChildren(new MockFile("foo"), new MockFile("bar", true),
                      new MockFile("link").setIsRegularFile(false));
-    assertTrue(adaptor.isSupportedPath(rootPath));
-    assertTrue(adaptor.isSupportedPath(getPath("foo")));
-    assertTrue(adaptor.isSupportedPath(getPath("bar")));
-    assertFalse(adaptor.isSupportedPath(getPath("link")));
+    assertTrue(adaptor.isFileOrFolder(rootPath));
+    assertTrue(adaptor.isFileOrFolder(getPath("foo")));
+    assertTrue(adaptor.isFileOrFolder(getPath("bar")));
+    assertFalse(adaptor.isFileOrFolder(getPath("link")));
   }
 
   @Test
@@ -321,13 +321,6 @@
     assertTrue(response.notFound);
   }
 
-  /*
-   * This test was invalidated when the check for isSupportedPath()
-   * was moved above the check for bad DocIds in the beginning of
-   * FsAdaptor.getDocumentContent().  This test still passes, but
-   * the notFound response is coming from the earlier check for
-   * isSupportedPath(), not from the bad DocId.
-   */
   @Test
   public void testGetDocContentBadDocId() throws Exception {
     root.addChildren(new MockFile("badfile"));
@@ -339,6 +332,15 @@
   }
 
   @Test
+  public void testGetDocContentFileNotFound() throws Exception {
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    // The requested DocId is missing the root component of the path.
+    adaptor.getDocContent(new MockRequest(getDocId("non-existent")), response);
+    assertTrue(response.notFound);
+  }
+
+  @Test
   public void testGetDocContentHiddenFile() throws Exception {
     root.addChildren(new MockFile("hidden.txt").setIsHidden(true));
     adaptor.init(context);
diff --git a/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java b/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
index 4764a8a..c8fd6a1 100644
--- a/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
+++ b/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
@@ -105,6 +105,11 @@
   }
 
   @Override
+  public FileTime getLastAccessTime(Path doc) throws IOException {
+    return readBasicAttributes(doc).lastAccessTime();
+  }
+
+  @Override
   public void setLastAccessTime(Path doc, FileTime time) throws IOException {
     getFile(doc).setLastAccessTime(time);
   }
diff --git a/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java b/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java
index 761fdd9..ee79e96 100644
--- a/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java
+++ b/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java
@@ -30,6 +30,7 @@
 import java.io.InputStreamReader;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.attribute.AclFileAttributeView;
@@ -43,6 +44,9 @@
   private FileDelegate delegate = new TestNioFileDelegate();
 
   @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Rule
   public TemporaryFolder temp = new TemporaryFolder();
 
   private Path newTempDir(String name) throws IOException {
@@ -131,6 +135,13 @@
   }
 
   @Test
+  public void testReadBasicAttributesFileNotFound() throws Exception {
+    Path file = Paths.get(temp.getRoot().toString(), "notFound");
+    thrown.expect(NoSuchFileException.class);
+    BasicFileAttributes attrs = delegate.readBasicAttributes(file);
+  }
+
+  @Test
   public void testProbeContentType() throws Exception {
     TestHelper.assumeOsIsNotMac();
     String content = "<html><title>Foo</title><body>Bar</body></html>";