Improving handling of last access time

Updating the adaptor to call the Win Api SetFileTime with the value of -1 for
the last access time before reading the content of a file. This will stop
Windows from updating the last access time as the file content is read by the
adaptor. As a fail over, the adaptor also queries the last access time of a
document before reading the content and then check if the time has changed. If
it has, then the adaptor will attempt to reset the last access time of the
document back to the original.
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 faa36f5..609588b 100644
--- a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
+++ b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
@@ -490,7 +490,7 @@
     }
 
     final boolean docIsDirectory = attrs.isDirectory();
-    final FileTime lastAccessTime = attrs.lastAccessTime();
+    final FileTime lastAccessTime = delegate.getLastAccessTime(doc);
 
     if (!docIsDirectory) {
       if (lastAccessTimeFilter.excluded(lastAccessTime)) {
@@ -614,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);
+            }
           }
         }
       }
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/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/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);
   }