Add FsAdaptorTest and supporting test infrastructure

This adds unit tests for the FsAdaptor class, including a bunch
of supporting test classes.  The changes include:

FsAdaptorTest: 49 Unit tests for FsAdaptor class
FsAdaptor: Added a test constructor that takes a FileDelegate arg, made some
    methods and inner classes VisibleForTesting
HtmlResponseWriter: Fixed array bounds exception for unix paths
MockFile: A lightweight, in-memory file system
MockFileTest: Sanity checks for MockFile
MockFileDelgate: A FileDelegate backed by MockFiles
MockFileDelegateTest: Sanity checks for MockFileDelegate
MockAdaptorContext: A AdaptorContext wrapping AccumlatingDocIdPusher
MockRequest: A trivial Request implementation
MockResponse: A Response that supports only the features used by the adaptor
AccumulatingDocIdPusher: Imported from SP adaptor and heavily modified
UnsupportedDocIdPusher: Parent class of AccumulatingDocId pusher
MockDocIdCodec: Imported from adaptor framework tests

These unit tests provide 91% code coverage for the FsAdaptor class.

Code Review: https://codereview.appspot.com/58560043/
diff --git a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
index f957bde..2928a61 100644
--- a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
+++ b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
@@ -132,12 +132,14 @@
   /** The namespace applied to ACL Principals. */
   private String namespace;
 
+  /** The filesystem change monitor. */
+  private FsMonitor monitor;
+
   private AdaptorContext context;
   private Path rootPath;
   private boolean isDfsUnc;
   private DocId rootPathDocId;
   private FileDelegate delegate;
-  private FsMonitor monitor;
 
   public FsAdaptor() {
     // At the moment, we only support Windows.
@@ -154,6 +156,26 @@
     this.delegate = delegate;
   }
 
+  @VisibleForTesting
+  Set<String> getSupportedWindowsAccounts() {
+    return supportedWindowsAccounts;
+  }
+
+  @VisibleForTesting
+  String getBuiltinPrefix() {
+    return builtinPrefix;
+  }
+
+  @VisibleForTesting
+  String getNamespace() {
+    return namespace;
+  }
+
+  @VisibleForTesting
+  BlockingQueue<Path> getFsMonitorQueue() {
+    return monitor.getQueue();
+  }
+
   @Override
   public void initConfig(Config config) {
     config.addKey(CONFIG_SRC, null);
@@ -238,8 +260,13 @@
   @Override
   public void destroy() {
     delegate.destroy();
-    monitor.destroy();
-    monitor = null;
+    // TODO (bmj): The check for null monitor is strictly for the tests,
+    // some of which may not have fully initialized the adaptor.  Maybe
+    // look into handling this less obtrusively in the future.
+    if (monitor != null) {
+      monitor.destroy();
+      monitor = null;
+    }
   }
 
   @Override
@@ -414,10 +441,10 @@
     // Populate the document content.
     if (docIsDirectory) {
       HtmlResponseWriter writer = createHtmlResponseWriter(resp);
-      writer.start(id, getPathName(doc));
+      writer.start(id, getFileName(doc));
       for (Path file : delegate.newDirectoryStream(doc)) {
         if (isSupportedPath(file)) {
-          writer.addLink(delegate.newDocId(file), getPathName(file));
+          writer.addLink(delegate.newDocId(file), getFileName(file));
         }
       }
       writer.finish();
@@ -450,14 +477,14 @@
     response.setContentType("text/html; charset=" + CHARSET.name());
     // TODO(ejona): Get locale from request.
     return new HtmlResponseWriter(writer, context.getDocIdEncoder(),
-
         Locale.ENGLISH);
   }
 
   @VisibleForTesting
-  String getPathName(Path file) {
+  String getFileName(Path file) {
     // NOTE: file.getFileName() fails for UNC paths. Use file.toFile() instead.
-    return file.toFile().getName();
+    String name = file.toFile().getName();
+    return name.isEmpty() ? file.getRoot().toString() : name;
   }
 
   @VisibleForTesting
@@ -505,8 +532,8 @@
       Preconditions.checkNotNull(pusher, "the DocId pusher may not be null");
       Preconditions.checkArgument(maxFeedSize > 0,
           "the maxFeedSize must be greater than zero");
-      Preconditions.checkArgument(maxLatencyMillis > 0,
-          "the maxLatencyMillis must be greater than zero");
+      Preconditions.checkArgument(maxLatencyMillis >= 0,
+          "the maxLatencyMillis must be greater than or equal to zero");
       this.pusher = pusher;
       this.maxFeedSize = maxFeedSize;
       this.maxLatencyMillis = maxLatencyMillis;
diff --git a/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java b/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java
index 3cf0f47..e0d50f3 100644
--- a/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java
+++ b/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java
@@ -15,6 +15,7 @@
 package com.google.enterprise.adaptor.fs;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.enterprise.adaptor.DocId;
 import com.google.enterprise.adaptor.DocIdEncoder;
 
@@ -213,10 +214,10 @@
   }
 
   private String computeLabel(String label, DocId doc) {
-    if (label == null || "".equals(label)) {
+    if (Strings.isNullOrEmpty(label)) {
       // Use the last part of the URL if an item doesn't have a title. The last
       // part of the URL will generally be a filename in this case.
-      String[] parts = doc.getUniqueId().split("/");
+      String[] parts = doc.getUniqueId().split("/", 0);
       label = parts[parts.length - 1];
     }
     return label;
diff --git a/test/com/google/enterprise/adaptor/fs/AccumulatingDocIdPusher.java b/test/com/google/enterprise/adaptor/fs/AccumulatingDocIdPusher.java
new file mode 100644
index 0000000..5c38e25
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/AccumulatingDocIdPusher.java
@@ -0,0 +1,96 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.ExceptionHandler;
+import com.google.enterprise.adaptor.GroupPrincipal;
+import com.google.enterprise.adaptor.Principal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+class AccumulatingDocIdPusher extends UnsupportedDocIdPusher {
+  private List<Record> records = new ArrayList<Record>();
+  private List<Map<DocId, Acl>> namedResouces
+      = new ArrayList<Map<DocId, Acl>>();
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds)
+      throws InterruptedException {
+    return pushDocIds(docIds, null);
+  }
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds,
+                          ExceptionHandler handler)
+      throws InterruptedException {
+    List<Record> records = new ArrayList<Record>();
+    for (DocId docId : docIds) {
+      records.add(new Record.Builder(docId).build());
+    }
+    Record record = pushRecords(records, handler);
+    return record == null ? null : record.getDocId();
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records)
+      throws InterruptedException {
+    return pushRecords(records, null);
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records,
+                            ExceptionHandler handler)
+      throws InterruptedException {
+    for (Record record : records) {
+      this.records.add(record);
+    }
+    return null;
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources)
+      throws InterruptedException {
+    return pushNamedResources(resources, null);
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources,
+                                  ExceptionHandler handler)
+      throws InterruptedException {
+    namedResouces.add(Collections.unmodifiableMap(
+        new TreeMap<DocId, Acl>(resources)));
+    return null;
+  }
+
+  public List<Record> getRecords() {
+    return Collections.unmodifiableList(records);
+  }
+
+  public List<Map<DocId, Acl>> getNamedResources() {
+    return Collections.unmodifiableList(namedResouces);
+  }
+
+  public void reset() {
+    records.clear();
+    namedResouces.clear();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
index 1430991..913c704 100644
--- a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
@@ -1,4 +1,4 @@
-// Copyright 2013 Google Inc. All Rights Reserved.
+// Copyright 2014 Google Inc. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,86 +14,915 @@
 
 package com.google.enterprise.adaptor.fs;
 
+import static com.google.enterprise.adaptor.fs.AclView.user;
+import static com.google.enterprise.adaptor.fs.AclView.group;
+import static com.google.enterprise.adaptor.fs.AclView.GenericPermission.*;
+
 import static org.junit.Assert.*;
 
-import com.google.enterprise.adaptor.fs.MockDirectoryBuilder.ConfigureFile;
+import static java.nio.file.attribute.AclEntryFlag.*;
+import static java.nio.file.attribute.AclEntryPermission.*;
+import static java.nio.file.attribute.AclEntryType.*;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.Acl.InheritanceType;
+import com.google.enterprise.adaptor.AdaptorContext;
+import com.google.enterprise.adaptor.Config;
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdPusher.Record;
+import com.google.enterprise.adaptor.GroupPrincipal;
+import com.google.enterprise.adaptor.UserPrincipal;
 
 import org.junit.*;
 import org.junit.rules.ExpectedException;
 
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
 
 /** Test cases for {@link FsAdaptor}. */
 public class FsAdaptorTest {
-  private MockDirectoryBuilder builder;
-  private MockFileDelegate delegate;
-  private FsAdaptor adaptor;
-  private Config config;
 
-  @Override
-  protected void setUp() {
-    builder = new MockDirectoryBuilder();
-    delegate = new MockFileDelegate(builder);
-    adaptor = new FsAdaptor(delegate);
-    config = new Config();
+  static final String ROOT = "/";
+  static final Path rootPath = Paths.get(ROOT);
+  static final DocId shareAclDocId = new DocId("shareAcl");
+
+  private AdaptorContext context = new MockAdaptorContext();
+  private AccumulatingDocIdPusher pusher =
+      (AccumulatingDocIdPusher) context.getDocIdPusher();
+  private Config config = context.getConfig();
+  private MockFile root = new MockFile(ROOT, true);
+  private MockFileDelegate delegate = new MockFileDelegate(root);
+  private FsAdaptor adaptor = new FsAdaptor(delegate);
+  private DocId rootDocId;
+
+  @Before
+  public void setUp() throws Exception {
+    rootDocId = delegate.newDocId(rootPath);
     adaptor.initConfig(config);
+    config.overrideKey("filesystemadaptor.src", root.getPath());
+    config.overrideKey("adaptor.incrementalPollPeriodSecs", "0");
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    adaptor.destroy();
   }
 
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  private Path getPath(String path) {
+    return rootPath.resolve(path);
+  }
+
+  private DocId getDocId(String path) throws IOException {
+    return delegate.newDocId(getPath(path));
+  }
+
   @Test
-  public void testGetPathName() throws Exception {
-    assertEquals("share", adaptor.getPathName(Paths.get("\\\\host/share/")));
-    assertEquals("folder2", 
-        adaptor.getPathName(Paths.get("C:/folder1/folder2/")));
-    assertEquals("foo.text", 
-        adaptor.getPathName(Paths.get("C:/folder1/foo.txt")));
-    assertEquals("bug-test", adaptor.getPathName(Paths.get("\\\\\brwsr-xp.gdc-psl.net/bug-test/")));
+  public void testAdaptorStartupShutdown() throws Exception {
+    // Construction of Adaptor happened in setUp(), and
+    // destruction will happen in tearDown(), so the only
+    // thing left is to init the context.
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitNoSourcePath() throws Exception {
+    config.overrideKey("filesystemadaptor.src", "");
+    thrown.expect(IOException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitNonRootSourcePath() throws Exception {
+    root.addChildren(new MockFile("subdir", true));
+    config.overrideKey("filesystemadaptor.src", getPath("subdir").toString());
+    thrown.expect(IllegalStateException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitDfsUncActiveStorageUnc() throws Exception {
+    root.setDfsUncActiveStorageUnc(Paths.get("\\\\dfshost\\share"));
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitNonLinkDfsUncActiveStorageUnc() throws Exception {
+    root.setDfsUncActiveStorageUnc(rootPath);
+    thrown.expect(IOException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitSupportedWindowsAccounts() throws Exception {
+    String accounts = "Everyone, BUILTIN\\Users, NT AUTH\\New Users";
+    Set<String> expected =
+        ImmutableSet.of("Everyone", "BUILTIN\\Users", "NT AUTH\\New Users");
+    config.overrideKey("filesystemadaptor.supportedAccounts", accounts);
+    adaptor.init(context);
+    assertEquals(expected, adaptor.getSupportedWindowsAccounts());
+  }
+
+  @Test
+  public void testAdaptorInitBuiltinGroupPrefix() throws Exception {
+    String expected = "TestPrefix";
+    config.overrideKey("filesystemadaptor.builtinGroupPrefix", expected);
+    adaptor.init(context);
+    assertEquals(expected, adaptor.getBuiltinPrefix());
+  }
+
+  @Test
+  public void testAdaptorInitNamespace() throws Exception {
+    String expected = "TestNamespace";
+    config.overrideKey("adaptor.namespace", expected);
+    adaptor.init(context);
+    assertEquals(expected, adaptor.getNamespace());
+  }
+
+  @Test
+  public void testGetFolderName() throws Exception {
+    assertEquals("share", adaptor.getFileName(Paths.get("\\\\host/share/")));
+    assertEquals("folder2",
+        adaptor.getFileName(Paths.get("C:/folder1/folder2/")));
+    assertEquals("folder2",
+        adaptor.getFileName(Paths.get("/folder1/folder2/")));
+    assertEquals("share", adaptor.getFileName(Paths.get("\\\\host/share")));
+    assertEquals("folder1",
+        adaptor.getFileName(Paths.get("/folder1")));
+    assertEquals(File.separator,  // Windows flips the '/' to '\'.
+        adaptor.getFileName(Paths.get("/")));
+    assertEquals("C:\\",
+        adaptor.getFileName(Paths.get("C:\\")));
   }
 
   @Test
   public void testIsSupportedPath() throws Exception {
-    ConfigureFile configureFile = new ConfigureFile() {
-        public boolean configure(MockFile file) {
-          if ("link".equals(file.getName())) {
-            file.setIsDirectory(false);
-            file.setIsRegularFile(false);
-            return false;
-          }
-          return true;
-        }
-      };
-    builder.addDir(configureFile, null, "/root", "foo", "link", "bar");
-
-    assertTrue(adaptor.isSupportedPath(delegate.getPath("/root")));
-    assertTrue(adaptor.isSupportedPath(delegate.getPath("/root/foo")));
-    assertTrue(adaptor.isSupportedPath(delegate.getPath("/root/bar")));
-    assertFalse(adaptor.isSupportedPath(delegate.getPath("/root/link")));
+    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")));
   }
-  /*
+
   @Test
   public void testIsVisibleDescendantOfRoot() throws Exception {
-    ConfigureFile configureFile = new ConfigureFile() {
-        public boolean configure(MockFile file) {
-          if ("hidden".equals(file.getName())) {
-            file.setIsHidden(true);
-            return false;
-          }
-          return true;
-        }
-      };
-    MockFile root = builder.addDir(null, "/", "foo");
-    builder.addDir(configureFile, root, "dir1", "bar", "hidden");
-    MockFile dir2 = builder.addDir(configureFile, root, "dir2", "baz");
-    builder.addDir(configureFile, dir2, "hidden", "foobar");
-
-    assertTrue(adaptor.isSupportedPath(delegate.getPath("/root")));
-    assertTrue(adaptor.isSupportedPath(delegate.getPath("/root/foo")));
-    assertTrue(adaptor.isSupportedPath(delegate.getPath("/root/bar")));
-    assertFalse(adaptor.isSupportedPath(delegate.getPath("/root/link")));
+    adaptor.init(context);
+    root.addChildren(new MockFile("foo"),
+        new MockFile("hidden.txt").setIsHidden(true),
+        new MockFile("dir1", true).addChildren(new MockFile("bar"),
+            new MockFile("hidden.pdf").setIsHidden(true)),
+        new MockFile("hidden.dir", true).setIsHidden(true).addChildren(
+            new MockFile("baz")));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(rootPath));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(getPath("foo")));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(getPath("dir1")));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(getPath("dir1/bar")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("hidden.txt")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("dir1/hidden.pdf")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("hidden.dir")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("hidden.dir/baz")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(null));
   }
-  
-  */
 
+  @Test
+  public void testGetDocIds() throws Exception {
+    adaptor.init(context);
+    adaptor.getDocIds(pusher);
+
+    // We should just push the root docid.
+    List<Record> records = pusher.getRecords();
+    assertEquals(1, records.size());
+    assertEquals(delegate.newDocId(rootPath), records.get(0).getDocId());
+
+    // We should have pushed an acl for the share.
+    List<Map<DocId, Acl>> namedResources = pusher.getNamedResources();
+    Acl expected = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritanceType(InheritanceType.AND_BOTH_PERMIT).build();
+    assertEquals(1, namedResources.size());
+    assertEquals(1, namedResources.get(0).size());
+    assertEquals(expected, namedResources.get(0).get(shareAclDocId));
+  }
+
+  @Test
+  public void testGetDocIdsDfs() throws Exception {
+    Path uncPath = Paths.get("\\\\dfshost\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    adaptor.init(context);
+    adaptor.getDocIds(pusher);
+
+    // We should just push the root docid.
+    List<Record> records = pusher.getRecords();
+    assertEquals(1, records.size());
+    assertEquals(delegate.newDocId(rootPath), records.get(0).getDocId());
+
+    // We should have pushed acls for the share and the dfsShare.
+    List<Map<DocId, Acl>> namedResources = pusher.getNamedResources();
+    DocId dfsShareAcl = new DocId("dfsShareAcl");
+    Acl expectedDfsShareAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritanceType(InheritanceType.AND_BOTH_PERMIT).build();
+    Acl expectedShareAcl = new Acl.Builder(expectedDfsShareAcl)
+        .setInheritFrom(dfsShareAcl).build();
+    assertEquals(1, namedResources.size());
+    Map<DocId, Acl> acls = namedResources.get(0);
+    assertEquals(2, acls.size());
+    assertEquals(expectedDfsShareAcl, acls.get(dfsShareAcl));
+    assertEquals(expectedShareAcl, acls.get(shareAclDocId));
+  }
+
+  @Test
+  public void testGetDocIdsBrokenDfs() throws Exception {
+    Path uncPath = Paths.get("\\\\dfshost\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    adaptor.init(context);
+
+    // Now make the active storage disappear.
+    root.setDfsUncActiveStorageUnc(null);
+    thrown.expect(IOException.class);
+    adaptor.getDocIds(pusher);
+  }
+
+  @Test
+  public void testGetDocContentUnsupportedPath() throws Exception {
+    root.addChildren(new MockFile("unsupported").setIsRegularFile(false));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId("unsupported")), response);
+    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"));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    // The requested DocId is missing the root component of the path.
+    adaptor.getDocContent(new MockRequest(new DocId("badfile")), response);
+    assertTrue(response.notFound);
+  }
+
+  @Test
+  public void testGetDocContentHiddenFile() throws Exception {
+    root.addChildren(new MockFile("hidden.txt").setIsHidden(true));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId("hidden.txt")), response);
+    assertTrue(response.notFound);
+  }
+
+  @Test
+  public void testGetDocContentHiddenDirectory() throws Exception {
+    root.addChildren(new MockFile("hidden.dir", true).setIsHidden(true));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId("hidden.dir")), response);
+    assertTrue(response.notFound);
+  }
+
+  @Test
+  public void testGetDocContentRegularFile() throws Exception {
+    String fname = "test.html";
+    Date modifyDate = new Date(30000);
+    FileTime modifyTime = FileTime.fromMillis(modifyDate.getTime());
+    String content = "<html><title>Hello World</title></html>";
+    root.addChildren(new MockFile(fname).setLastModifiedTime(modifyTime)
+        .setFileContents(content).setContentType("text/html"));
+    adaptor.init(context);
+
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId(fname)), response);
+    assertFalse(response.notFound);
+    assertEquals(modifyDate, response.lastModified);
+    assertEquals(getPath(fname).toUri(), response.displayUrl);
+    assertEquals("text/html", response.contentType);
+    assertEquals(content, response.content.toString("UTF-8"));
+    // TODO: check metadata.
+    assertNotNull(response.metadata.get("Creation Time"));
+    // ACL checked in other tests.
+  }
+
+  @Test
+  public void testGetDocContentRootAcl() throws Exception {
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritFrom(shareAclDocId)
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES).build();
+    adaptor.init(context);
+    MockRequest request = new MockRequest(delegate.newDocId(rootPath));
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(request, response);
+    assertEquals(expectedAcl, response.acl);
+  }
+
+  @Test
+  public void testGetDocContentEmptyAcl() throws Exception {
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setInheritFrom(rootDocId, "childFilesAcl")
+        .setInheritanceType(InheritanceType.LEAF_NODE).build();
+    testFileAcl(MockFile.EMPTY_ACLVIEW, null, expectedAcl);
+  }
+
+  @Test
+  public void testGetDocContentDirectAcl() throws Exception {
+    AclFileAttributeView aclView = new AclView((user("joe")
+        .type(ALLOW).perms(GENERIC_READ).build()));
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(new UserPrincipal("joe")))
+        .setInheritFrom(rootDocId, "childFilesAcl")
+        .setInheritanceType(InheritanceType.LEAF_NODE).build();
+    testFileAcl(aclView, null, expectedAcl);
+  }
+
+  @Test
+  public void testGetDocContentNoInheritAcl() throws Exception {
+    AclFileAttributeView aclView = new AclView((user("joe")
+        .type(ALLOW).perms(GENERIC_READ).build()));
+    // Should inherit from the share, not the parent.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(new UserPrincipal("joe")))
+        .setInheritFrom(shareAclDocId)
+        .setInheritanceType(InheritanceType.LEAF_NODE).build();
+    testFileAcl(aclView, MockFile.EMPTY_ACLVIEW, expectedAcl);
+  }
+
+  private void testFileAcl(AclFileAttributeView aclView,
+      AclFileAttributeView inheritAclView, Acl expectedAcl) throws Exception {
+    String fname = "acltest";
+    root.addChildren(new MockFile(fname).setAclView(aclView)
+                     .setInheritedAclView(inheritAclView));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId(fname)), response);
+    assertEquals(expectedAcl, response.acl);
+  }
+
+  /** Test that LastAccessTime is restored after reading the file. */
+  @Test
+  public void testPreserveFileLastAccessTime() throws Exception {
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return super.newInputStream();
+        }
+      });
+  }
+
+  /** Test LastAccessTime is restored even if exception opening the file. */
+  @Test
+  public void testPreserveFileLastAccessTimeException1() throws Exception {
+    thrown.expect(IOException.class);
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          throw new IOException("newInputStream");
+        }
+      });
+  }
+
+  /** Test LastAccessTime is restored even if exception reading the file. */
+  @Test
+  public void testPreserveFileLastAccessTimeException2() throws Exception {
+    thrown.expect(IOException.class);
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return new FilterInputStream(super.newInputStream()) {
+              @Override
+              public int read(byte[] b, int off, int len) throws IOException {
+                throw new IOException("read");
+              }
+          };
+        }
+      });
+  }
+
+  /** Test LastAccessTime is restored even if exception closing the file. */
+  @Test
+  public void testPreserveFileLastAccessTimeException3() throws Exception {
+    thrown.expect(IOException.class);
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return new FilterInputStream(super.newInputStream()) {
+              @Override
+              public void close() throws IOException {
+                throw new IOException("close");
+              }
+          };
+        }
+      });
+  }
+
+  /** Test that failure to restore LastAccessTime is not fatal. */
+  @Test
+  public void testPreserveFileLastAccessTimeException4() throws Exception {
+    testNoPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return super.newInputStream();
+        }
+        @Override
+        MockFile setLastAccessTime(FileTime accessTime) throws IOException {
+          if (MockFile.DEFAULT_FILETIME.equals(getLastAccessTime())) {
+            // Let the above setting from newInputStream go through.
+            return super.setLastAccessTime(accessTime);
+          } else {
+            // But fail the attempt to restore from FsAdaptor.
+            throw new IOException("Restore LastAccessTime");
+          }
+        }
+      });
+  }
+
+  private void testPreserveFileLastAccessTime(MockFile file) throws Exception {
+    testFileLastAccessTime(file, true);
+  }
+
+  private void testNoPreserveFileLastAccessTime(MockFile file)
+      throws Exception {
+    testFileLastAccessTime(file, false);
+  }
+
+  private void testFileLastAccessTime(MockFile file, boolean isPreserved)
+      throws Exception {
+    String contents = "Test contents";
+    file.setFileContents(contents);
+    root.addChildren(file);
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    FileTime lastAccess = file.getLastAccessTime();
+    adaptor.getDocContent(new MockRequest(getDocId(file.getName())), response);
+    // Verify we indeed accessed the file
+    assertEquals(contents, response.content.toString("UTF-8"));
+    if (isPreserved) {
+      assertEquals(lastAccess, file.getLastAccessTime());
+    } else {
+      assertFalse(lastAccess.equals(file.getLastAccessTime()));
+    }
+  }
+
+  @Test
+  public void testGetDocContentRoot() throws Exception {
+    testGetDocContentDirectory(rootPath, rootPath.toString());
+    // ACLs checked in other tests.
+  }
+
+  @Test
+  public void testGetDocContentDirectory() throws Exception {
+    String fname = "test.dir";
+    root.addChildren(new MockFile(fname, true));
+    testGetDocContentDirectory(getPath(fname), fname);
+    // ACLs checked in other tests.
+  }
+
+  private void testGetDocContentDirectory(Path path, String label)
+      throws Exception {
+    MockFile dir = delegate.getFile(path);
+    FileTime modifyTime = dir.getLastModifiedTime();
+    Date modifyDate = new Date(modifyTime.toMillis());
+    dir.addChildren(new MockFile("test.txt"), new MockFile("subdir", true));
+    adaptor.init(context);
+    MockRequest request = new MockRequest(delegate.newDocId(path));
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(request, response);
+    assertFalse(response.notFound);
+    assertEquals(modifyDate, response.lastModified);
+    assertEquals(path.toUri(), response.displayUrl);
+    assertEquals("text/html; charset=UTF-8", response.contentType);
+    String expectedContent = "<!DOCTYPE html>\n<html><head><title>Folder "
+        + label + "</title></head><body><h1>Folder " + label + "</h1>"
+        + "<li><a href=\"subdir/\">subdir</a></li>"
+        + "<li><a href=\"test.txt\">test.txt</a></li></body></html>";
+    assertEquals(expectedContent, response.content.toString("UTF-8"));
+    assertNotNull(response.metadata.get("Creation Time"));
+  }
+
+  @Test
+  public void testGetDocContentDefaultRootAcls() throws Exception {
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedAcl,
+        "allFilesAcl", expectedAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentInheritOnlyRootAcls() throws Exception {
+    AclFileAttributeView inheritOnlyAclView = new AclView(
+        user("Longfellow Deeds").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, INHERIT_ONLY),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(inheritOnlyAclView);
+
+    // The root ACL should only include Administrators, not Mr. Deeds.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // But the childrens' inherited ACLs should include Mr. Deeds
+    Acl expectedInheritableAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Longfellow Deeds")))
+        .build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedInheritableAcl,
+        "allFilesAcl", expectedInheritableAcl,
+        "childFoldersAcl", expectedInheritableAcl,
+        "childFilesAcl", expectedInheritableAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentNoPropagateRootAcls() throws Exception {
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("Barren von Dink").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, NO_PROPAGATE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(noPropagateAclView);
+
+    // The root ACL should include both Administrators and the Barren.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Barren von Dink")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // The direct childrens' inherited ACLs should include both the
+    // Administrators and the Barren, but grandchildren should not
+    // inherit the Barren's NO_PROPAGATE permission.
+    Acl expectedNonChildAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedNonChildAcl,
+        "allFilesAcl", expectedNonChildAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFilesOnlyRootAcls() throws Exception {
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("For Your Files Only").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(noPropagateAclView);
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("For Your Files Only")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // Folders shouldn't include the file-only permissions.
+    Acl expectedFolderAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedFolderAcl,
+        "allFilesAcl", expectedAcl,
+        "childFoldersAcl", expectedFolderAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFoldersOnlyRootAcls() throws Exception {
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("Fluff 'n Folder").type(ALLOW).perms(GENERIC_READ)
+            .flags(DIRECTORY_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(noPropagateAclView);
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Fluff 'n Folder")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // Files shouldn't include the folder-only permissions.
+    Acl expectedFilesAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedAcl,
+        "allFilesAcl", expectedFilesAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedFilesAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentDefaultDirectoryAcls() throws Exception {
+    String name = "subdir";
+    root.addChildren(new MockFile(name, true));
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    Acl expectedFoldersAcl = new Acl.Builder(expectedAcl)
+        .setInheritFrom(rootDocId, "allFoldersAcl").build();
+    Acl expectedFilesAcl = new Acl.Builder(expectedAcl)
+        .setInheritFrom(rootDocId, "allFilesAcl").build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedFoldersAcl,
+        "allFilesAcl", expectedFilesAcl,
+        "childFoldersAcl", expectedFoldersAcl,
+        "childFilesAcl", expectedFilesAcl);
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentNoInheritDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView orphanAclView = new AclView(user("Annie").type(ALLOW)
+        .perms(GENERIC_READ).flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(orphanAclView)
+        .setInheritedAclView(MockFile.EMPTY_ACLVIEW));
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(new UserPrincipal("Annie")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedAcl,
+        "allFilesAcl", expectedAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentInheritOnlyDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView inheritOnlyAclView = new AclView(
+        user("Longfellow Deeds").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, INHERIT_ONLY),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(inheritOnlyAclView));
+
+    // The root ACL should only include Administrators, not Mr. Deeds.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // But the childrens' inherited ACLs should include Mr. Deeds
+    Acl expectedFoldersAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Longfellow Deeds")))
+        .setInheritFrom(rootDocId, "allFoldersAcl").build();
+    Acl expectedFilesAcl = new Acl.Builder(expectedFoldersAcl)
+        .setInheritFrom(rootDocId, "allFilesAcl").build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedFoldersAcl,
+        "allFilesAcl", expectedFilesAcl,
+        "childFoldersAcl", expectedFoldersAcl,
+        "childFilesAcl", expectedFilesAcl);
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentNoPropagateDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("Barren von Dink").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, NO_PROPAGATE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(noPropagateAclView));
+
+    // The root ACL should include both Administrators and the Barren.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Barren von Dink")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // The direct childrens' inherited ACLs should include both the
+    // Administrators and the Barren, but grandchildren should not
+    // inherit the Barren's NO_PROPAGATE permission.
+    Acl expectedNonChildAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", new Acl.Builder(expectedNonChildAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "allFilesAcl", new Acl.Builder(expectedNonChildAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build(),
+        "childFoldersAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "childFilesAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build());
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFilesOnlyDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView filesOnlyAclView = new AclView(
+        user("For Your Files Only").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(filesOnlyAclView));
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("For Your Files Only")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // Folders shouldn't include the file-only permissions.
+    Acl expectedFolderAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", new Acl.Builder(expectedFolderAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "allFilesAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build(),
+        "childFoldersAcl", new Acl.Builder(expectedFolderAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "childFilesAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build());
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFoldersOnlyDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView foldersOnlyAclView = new AclView(
+        user("Fluff 'n Folder").type(ALLOW).perms(GENERIC_READ)
+            .flags(DIRECTORY_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(foldersOnlyAclView));
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Fluff 'n Folder")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // Files shouldn't include the folder-only permissions.
+    Acl expectedFilesAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "allFilesAcl", new Acl.Builder(expectedFilesAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build(),
+        "childFoldersAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "childFilesAcl", new Acl.Builder(expectedFilesAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build());
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  private void testGetDocContentAcls(Path path, Acl expectedAcl,
+      Map<String, Acl> expectedAclResources) throws Exception {
+    adaptor.init(context);
+    MockRequest request = new MockRequest(delegate.newDocId(path));
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(request, response);
+    assertEquals(expectedAcl, response.acl);
+    assertEquals(expectedAclResources, response.namedResources);
+  }
+
+  @Test
+  public void testMonitorZeroFeedSize() throws Exception {
+    config.overrideKey("feed.maxUrls", "0");
+    thrown.expect(IllegalArgumentException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testMonitorZeroLatency() throws Exception {
+    config.overrideKey("adaptor.incrementalPollPeriodSecs", "0");
+    adaptor.init(context);
+  }
+
+
+  @Test
+  public void testMonitorSubZeroLatency() throws Exception {
+    config.overrideKey("adaptor.incrementalPollPeriodSecs", "-1");
+    thrown.expect(IllegalArgumentException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testMonitorUnsupportedPath() throws Exception {
+    root.addChildren(new MockFile("unsuppored").setIsRegularFile(false));
+    adaptor.init(context);
+    BlockingQueue<Path> queue = adaptor.getFsMonitorQueue();
+    queue.add(getPath("unsupported"));
+    Thread.sleep(100L);  // Allow FsMonitor to drain the queue.
+    // Verify it has been processed, but not been fed.
+    assertEquals(0, queue.size());
+    assertEquals(0, pusher.getRecords().size());
+    assertEquals(0, pusher.getNamedResources().size());
+  }
+
+  @Test
+  public void testMonitorOneFile() throws Exception {
+    String name = "test";
+    root.addChildren(new MockFile(name));
+    adaptor.init(context);
+    testMonitor(name);
+  }
+
+  @Test
+  public void testMonitorOneDirectory() throws Exception {
+    String name = "test.dir";
+    root.addChildren(new MockFile(name, true));
+    adaptor.init(context);
+    testMonitor(name);
+  }
+
+  @Test
+  public void testMonitorMultipleItems() throws Exception {
+    String dirname = "subdir";
+    String fname = "test.txt";
+    root.addChildren(new MockFile(dirname, true), new MockFile(fname));
+    adaptor.init(context);
+    testMonitor(dirname, fname);
+  }
+
+  @Test
+  public void testMonitorMultipleBatches() throws Exception {
+    String dirname = "subdir";
+    String fname = "test.txt";
+    root.addChildren(new MockFile(dirname, true), new MockFile(fname));
+    adaptor.init(context);
+    testMonitor(dirname);
+    // Make sure the previous batch does not get fed again.
+    pusher.reset();
+    testMonitor(fname);
+  }
+
+  private void testMonitor(String... names) throws Exception {
+    BlockingQueue<Path> queue = adaptor.getFsMonitorQueue();
+    for (String name : names) {
+      queue.add(getPath(name));
+    }
+    Thread.sleep(100L); // Allow FsMonitor to drain the queue.
+    assertEquals(0, queue.size());
+    assertEquals(0, pusher.getNamedResources().size());
+    Set<Record> expected = Sets.newHashSet();
+    for (String name : names) {
+      expected.add(new Record.Builder(getDocId(name))
+                   .setCrawlImmediately(true).build());
+    }
+    assertEquals(expected, Sets.newHashSet(pusher.getRecords()));
+  }
 }
diff --git a/test/com/google/enterprise/adaptor/fs/MockAdaptorContext.java b/test/com/google/enterprise/adaptor/fs/MockAdaptorContext.java
new file mode 100644
index 0000000..adba37b
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockAdaptorContext.java
@@ -0,0 +1,111 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.AdaptorContext;
+import com.google.enterprise.adaptor.AuthnAuthority;
+import com.google.enterprise.adaptor.AuthzAuthority;
+import com.google.enterprise.adaptor.Config;
+import com.google.enterprise.adaptor.DocIdEncoder;
+import com.google.enterprise.adaptor.DocIdPusher;
+import com.google.enterprise.adaptor.ExceptionHandler;
+import com.google.enterprise.adaptor.PollingIncrementalLister;
+import com.google.enterprise.adaptor.SensitiveValueDecoder;
+import com.google.enterprise.adaptor.Session;
+import com.google.enterprise.adaptor.StatusSource;
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+/**
+ * Mock of {@link AdaptorContext}.
+ */
+class MockAdaptorContext implements AdaptorContext {
+  private final Config config = new Config();
+  private final DocIdPusher docIdPusher = new AccumulatingDocIdPusher();
+  private final DocIdEncoder docIdEncoder = new MockDocIdCodec();
+
+  @Override
+  public Config getConfig() {
+    return config;
+  }
+
+  @Override
+  public DocIdPusher getDocIdPusher() {
+    return docIdPusher;
+  }
+
+  @Override
+  public DocIdEncoder getDocIdEncoder() {
+    return docIdEncoder;
+  }
+
+  @Override
+  public void addStatusSource(StatusSource source) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setGetDocIdsFullErrorHandler(ExceptionHandler handler) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ExceptionHandler getGetDocIdsFullErrorHandler() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setGetDocIdsIncrementalErrorHandler(
+      ExceptionHandler handler) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ExceptionHandler getGetDocIdsIncrementalErrorHandler() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public SensitiveValueDecoder getSensitiveValueDecoder() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public HttpContext createHttpContext(String path, HttpHandler handler) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Session getUserSession(HttpExchange ex, boolean create) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setPollingIncrementalLister(PollingIncrementalLister lister) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setAuthnAuthority(AuthnAuthority authnAuthority) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setAuthzAuthority(AuthzAuthority authzAuthority) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockDocIdCodec.java b/test/com/google/enterprise/adaptor/fs/MockDocIdCodec.java
new file mode 100644
index 0000000..e669083
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockDocIdCodec.java
@@ -0,0 +1,37 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdEncoder;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Mock of {@link DocIdCodec}.
+ */
+class MockDocIdCodec implements DocIdEncoder {
+  @Override
+  public URI encodeDocId(DocId docId) {
+    try {
+      String id = docId.getUniqueId();
+      return new URI("http", "localhost",
+          id.startsWith("/") ? id : "/" + id, null);
+    } catch (URISyntaxException ex) {
+      throw new AssertionError();
+    }
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockFile.java b/test/com/google/enterprise/adaptor/fs/MockFile.java
index 5cb2461..eb97926 100644
--- a/test/com/google/enterprise/adaptor/fs/MockFile.java
+++ b/test/com/google/enterprise/adaptor/fs/MockFile.java
@@ -42,16 +42,15 @@
 
 class MockFile {
   static final String SEPARATOR = "/";
-  static final FileTime defaultFileTime = FileTime.fromMillis(10000);
-  static final AclFileAttributeView fullAccessAclView = 
+  static final FileTime DEFAULT_FILETIME = FileTime.fromMillis(10000);
+  static final AclFileAttributeView FULL_ACCESS_ACLVIEW =
       new AclView(group("Everyone").type(ALLOW)
           .perms(READ_DATA, READ_ATTRIBUTES, READ_NAMED_ATTRS, READ_ACL)
           .flags(FILE_INHERIT, DIRECTORY_INHERIT));
-  static final AclFileAttributeView emptyAclView = new AclView();
+  static final AclFileAttributeView EMPTY_ACLVIEW = new AclView();
 
   private MockFile parent;
   private String name;
-  private boolean exists = true;
   private boolean isHidden = false;
   private boolean isRegularFile;
   private boolean isDirectory;
@@ -61,9 +60,9 @@
   private AclFileAttributeView shareAclView;
   private AclFileAttributeView aclView;
   private AclFileAttributeView inheritedAclView;
-  private FileTime creationTime = defaultFileTime;
-  private FileTime lastAccessTime = defaultFileTime;
-  private FileTime lastModifiedTime = defaultFileTime;
+  private FileTime creationTime = DEFAULT_FILETIME;
+  private FileTime lastModifiedTime = DEFAULT_FILETIME;
+  private FileTime lastAccessTime = DEFAULT_FILETIME;
   private String contentType;
   private byte[] fileContents;
 
@@ -100,6 +99,7 @@
    * MockFile, and registers this file as the parent of all the children.
    */
   MockFile addChildren(MockFile... children) {
+    Preconditions.checkState(isDirectory, "not a directory %s", getPath());
     for (MockFile child : children) {
       child.parent = this;
       directoryContents.add(child);
@@ -112,6 +112,7 @@
    */
   MockFile getChild(String name) throws FileNotFoundException {
     Preconditions.checkNotNull(name, "name cannot be null");
+    Preconditions.checkState(isDirectory, "not a directory %s", getPath());
     Iterator<MockFile> it = directoryContents.iterator();
     while (it.hasNext()) {
       MockFile f = it.next();
@@ -124,37 +125,15 @@
   }
 
   /**
-   * Remove this file.
-   */
-  void delete() {
-    if (parent != null) {
-      // Unlink from parent.
-      Iterator<MockFile> it = parent.directoryContents.iterator();
-      while (it.hasNext()) {
-        if (it.next() == this) {
-          it.remove();
-          break;
-        }
-      }
-    }
-    if (isDirectory) {
-      // TODO: If this is insufficient, delete recursively.
-      // But after this, getChild() will fail, so we should be OK.
-      directoryContents.clear();
-    }
-    exists = isDirectory = isRegularFile = isHidden = false;
-    parent = null;
-    name = null;
-  }
-
-  /**
    * Return the path to this file or directory.
    */
   String getPath() {
     if (parent == null) {
       return name;
     } else {
-      return parent.getPath() + SEPARATOR + name;
+      String parentPath = parent.getPath();
+      return (parentPath.endsWith(SEPARATOR))
+             ? parentPath + name : parentPath + SEPARATOR + name;
     }
   }
 
@@ -193,15 +172,6 @@
     return isRegularFile;
   }
 
-  MockFile setExists(boolean exists) {
-    this.exists = exists;
-    return this;
-  }
-
-  boolean exists() throws IOException {
-    return this.exists;
-  }
-
   MockFile setIsHidden(boolean isHidden) {
     this.isHidden = isHidden;
     return this;
@@ -217,7 +187,7 @@
     return this;
   }
 
-  FileTime getCreateTime() throws IOException {
+  FileTime getCreationTime() throws IOException {
     return creationTime;
   }
 
@@ -259,7 +229,7 @@
     this.dfsShareAclView = aclView;
     return this;
   }
-  
+
   AclFileAttributeView getDfsShareAclView() throws IOException {
     return dfsShareAclView;
   }
@@ -270,7 +240,7 @@
   }
 
   AclFileAttributeView getShareAclView() throws IOException {
-    return (shareAclView == null) ? fullAccessAclView : shareAclView;
+    return (shareAclView == null) ? FULL_ACCESS_ACLVIEW : shareAclView;
   }
 
   MockFile setAclView(AclFileAttributeView aclView) {
@@ -280,7 +250,7 @@
 
   AclFileAttributeView getAclView() throws IOException {
     if (aclView == null) {
-      return (parent == null) ? fullAccessAclView : emptyAclView;
+      return (parent == null) ? FULL_ACCESS_ACLVIEW : EMPTY_ACLVIEW;
     } else {
       return aclView;
     }
@@ -295,10 +265,10 @@
     if (inheritedAclView == null) {
       if (parent == null) {
         // root has no inherited ACL
-        return emptyAclView;
+        return EMPTY_ACLVIEW;
       } else if (parent.parent == null) {
         // root's children inherit its ACL
-        return parent.getAclView();	
+        return parent.getAclView();
       } else {
         // all other children inherit from their parent
         return parent.getInheritedAclView();
@@ -314,7 +284,7 @@
   }
 
   String getContentType() throws IOException {
-    return contentType;
+    return isRegularFile ? contentType : null;
   }
 
   MockFile setFileContents(String fileContents) {
@@ -339,7 +309,9 @@
   }
 
   DirectoryStream<Path> newDirectoryStream() throws IOException {
-    Preconditions.checkState(isDirectory, "not a directory %s", getPath());
+    if (!isDirectory) {
+      throw new NotDirectoryException("not a directory " + getPath());
+    }
     return new MockDirectoryStream(directoryContents);
   }
 
diff --git a/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java b/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
index 2c85a6b..9a4c6f8 100644
--- a/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
+++ b/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
@@ -44,14 +44,27 @@
    */
   MockFile getFile(Path doc) throws FileNotFoundException {
     Preconditions.checkNotNull(doc, "doc cannot be null");
-    Iterator<Path> iter = doc.iterator();
     MockFile file = root;
+    Iterator<Path> iter = doc.iterator();
+    if (doc.getRoot() != null) {
+      // Using startsWith because Path adds a trailing backslash to
+      // UNC roots.  The second check accounts for Windows Path
+      // implementation flipping slashes on Unix paths.
+      if (!(doc.getRoot().toString().startsWith(root.getPath()) ||
+          root.getPath().equals(doc.getRoot().toString().replace('\\', '/')))) {
+        throw new FileNotFoundException("not found: " + doc.toString());
+      }
+    } else if (iter.hasNext()) {
+      if (!(root.getPath().equals(iter.next().toString()))) {
+        throw new FileNotFoundException("not found: " + doc.toString());
+      }
+    }
     while (iter.hasNext()) {
       file = file.getChild(iter.next().toString());
     }
     return file;
   }
-    
+
   @Override
   public Path getPath(String pathname) throws IOException {
     Preconditions.checkNotNull(pathname, "pathname cannot be null");
@@ -60,17 +73,29 @@
 
   @Override
   public boolean isDirectory(Path doc) throws IOException {
-    return getFile(doc).isDirectory();
+    try {
+      return getFile(doc).isDirectory();
+    } catch (FileNotFoundException e) {
+      return false;
+    }
   }
 
   @Override
   public boolean isRegularFile(Path doc) throws IOException {
-    return getFile(doc).isRegularFile();
+    try {
+      return getFile(doc).isRegularFile();
+    } catch (FileNotFoundException e) {
+      return false;
+    }
   }
 
   @Override
   public boolean isHidden(Path doc) throws IOException {
-    return getFile(doc).isHidden();
+    try {
+      return getFile(doc).isHidden();
+    } catch (FileNotFoundException e) {
+      return false;
+    }
   }
 
   @Override
@@ -100,10 +125,16 @@
 
   @Override
   public DocId newDocId(Path doc) throws IOException {
-    String id = doc.toString();
+    String id = doc.toString().replace('\\', '/');
     if (isDirectory(doc) && !id.endsWith("/")) {
       id += "/";
     }
+    if (id.startsWith("//")) {
+      // String.replaceFirst uses regular expression string and replacement
+      // so they need to be escaped appropriately. The above String.replace
+      // does NOT use expressions so regex escaping is not needed.
+      id = id.replaceFirst("//", "\\\\\\\\");
+    }
     return new DocId(id);
   }
 
diff --git a/test/com/google/enterprise/adaptor/fs/MockFileDelegateTest.java b/test/com/google/enterprise/adaptor/fs/MockFileDelegateTest.java
new file mode 100644
index 0000000..91f85f8
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockFileDelegateTest.java
@@ -0,0 +1,222 @@
+// Copyright 2014 Google Inc.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import static org.junit.Assert.*;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+import com.google.enterprise.adaptor.DocId;
+
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+
+/**
+ * Test cases for {@link MockFileDelegate}.
+ */
+public class MockFileDelegateTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstructorNullRoot() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFileDelegate(null);
+  }
+
+  @Test
+  public void testGetPathNullPathname() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFileDelegate(new MockFile("root", true)).getPath(null);
+  }
+
+  @Test
+  public void testGetPath() throws Exception {
+    String delim = File.separator;
+    FileDelegate delegate = new MockFileDelegate(new MockFile("root", true));
+    assertEquals("root", delegate.getPath("root").toString());
+    assertEquals("root" + delim + "foo" + delim + "bar",
+        delegate.getPath("root/foo/bar").toString());
+    assertEquals(delim + "foo" + delim + "bar" + delim + "baz",
+        delegate.getPath("/foo/bar/baz").toString());
+    assertEquals("\\\\server\\share\\dir\\file.txt",
+        delegate.getPath("\\\\server\\share\\dir\\file.txt").toString());
+  }
+
+  @Test
+  public void testGetFileNotFound() throws Exception {
+    MockFile root = new MockFile("root", true).addChildren(new MockFile("foo"));
+    MockFileDelegate delegate = new MockFileDelegate(root);
+    thrown.expect(FileNotFoundException.class);
+    delegate.getFile(delegate.getPath("root/nonExistent"));
+  }
+
+  @Test
+  public void testGetFileRelativeRoot() throws Exception {
+    testGetFile("root");
+  }
+
+  @Test
+  public void testGetFileUncRoot() throws Exception {
+    testGetFile("\\\\host\\share");
+    testGetFile("\\\\host\\share\\");
+  }
+
+  @Test
+  public void testGetFileUnixRoot() throws Exception {
+    testGetFile("/");
+  }
+
+  @Test
+  public void testGetFileDosRoot() throws Exception {
+    testGetFile("C:\\");
+  }
+
+  private void testGetFile(String rootPath) throws Exception {
+    MockFile root = new MockFile(rootPath, true).addChildren(
+        new MockFile("dir1", true).addChildren(new MockFile("foo")),
+        new MockFile("dir2", true).addChildren(new MockFile("bar")),
+        new MockFile("test.txt"));
+    MockFileDelegate delegate = new MockFileDelegate(root);
+
+    assertSame(root, delegate.getFile(delegate.getPath(rootPath)));
+    testGetFile(delegate, getPath(rootPath, "test.txt"));
+    testGetFile(delegate, getPath(rootPath, "dir1"));
+    testGetFile(delegate, getPath(rootPath, "dir2"));
+    testGetFile(delegate, getPath(rootPath, "dir1/foo"));
+    testGetFile(delegate, getPath(rootPath, "dir2/bar"));
+  }
+
+  private String getPath(String parent, String child) {
+    return (parent.endsWith("/")) ? parent + child : parent + "/" + child;
+  }
+
+  private void testGetFile(MockFileDelegate delegate, String pathname)
+      throws Exception {
+    Path path = delegate.getPath(pathname);
+    MockFile file = delegate.getFile(path);
+    assertEquals(pathname, file.getPath());
+    MockFile parent = file.getParent();
+    if (parent != null) {
+      assertSame(parent, delegate.getFile(path.getParent()));
+    }
+  }
+
+  @Test
+  public void testNewDocId() throws Exception {
+    MockFile root = new MockFile("root", true).addChildren(
+        new MockFile("dir1", true).addChildren(new MockFile("foo")));
+    FileDelegate delegate = new MockFileDelegate(root);
+    assertEquals(new DocId("root/"), getDocId(delegate, "root"));
+    assertEquals(new DocId("root/dir1/"), getDocId(delegate, "root/dir1"));
+    assertEquals(new DocId("root/dir1/foo"),
+                 getDocId(delegate, "root/dir1/foo"));
+  }
+
+  private DocId getDocId(FileDelegate delegate, String pathname)
+      throws Exception {
+    return delegate.newDocId(delegate.getPath(pathname));
+  }
+
+  @Test
+  public void testSetLastAccessTime() throws Exception {
+    MockFile root = new MockFile("root", true)
+        .addChildren(new MockFile("test.txt"));
+    FileDelegate delegate = new MockFileDelegate(root);
+    Path path = delegate.getPath("root/test.txt");
+
+    BasicFileAttributes attrs = delegate.readBasicAttributes(path);
+    assertEquals(MockFile.DEFAULT_FILETIME, attrs.lastAccessTime());
+
+    FileTime accessTime = FileTime.fromMillis(40000);
+    delegate.setLastAccessTime(path, accessTime);
+    attrs = delegate.readBasicAttributes(path);
+    assertEquals(accessTime, attrs.lastAccessTime());
+  }
+
+  /**
+   * Most of the MockDelegate methods are simple passthrough calls to the
+   * MockFile methods.  This is a sanity check to make sure I'm calling the
+   * right ones.
+   */
+  @Test
+  public void testPassthroughGetters() throws Exception {
+    FileTime createTime = FileTime.fromMillis(20000);
+    FileTime modifyTime = FileTime.fromMillis(30000);
+    FileTime accessTime = FileTime.fromMillis(40000);
+    String content = "<html><title>Hello World</title></html>";
+    MockFile root = new MockFile("root", true)
+        .addChildren(
+            new MockFile("test.html").setCreationTime(createTime)
+            .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime)
+            .setFileContents(content).setContentType("text/html"));
+
+    FileDelegate delegate = new MockFileDelegate(root);
+    Path path = delegate.getPath("root");
+    assertTrue(delegate.isDirectory(path));
+    assertFalse(delegate.isRegularFile(path));
+    assertNull(delegate.getDfsUncActiveStorageUnc(path));
+    Path uncPath = delegate.getPath("\\\\server\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    assertEquals(uncPath, delegate.getDfsUncActiveStorageUnc(path));
+    assertNull(delegate.getDfsShareAclView(path));
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW,
+                 delegate.getDfsShareAclView(path));
+    AclFileAttributeViews aclViews = delegate.getAclViews(path);
+    assertNotNull(aclViews);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, aclViews.getDirectAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, aclViews.getInheritedAclView());
+
+    path = delegate.getPath("root/test.html");
+    assertTrue(delegate.isRegularFile(path));
+    assertFalse(delegate.isDirectory(path));
+    assertFalse(delegate.isHidden(path));
+    assertEquals("text/html", delegate.probeContentType(path));
+    assertEquals(content, readContents(delegate, path));
+    aclViews = delegate.getAclViews(path);
+    assertNotNull(aclViews);
+    assertEquals(MockFile.EMPTY_ACLVIEW, aclViews.getDirectAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, aclViews.getInheritedAclView());
+
+    BasicFileAttributes attrs = delegate.readBasicAttributes(path);
+    assertTrue(attrs.isRegularFile());
+    assertFalse(attrs.isDirectory());
+    assertFalse(attrs.isOther());
+    assertEquals(createTime, attrs.creationTime());
+    assertEquals(modifyTime, attrs.lastModifiedTime());
+    assertEquals(accessTime, attrs.lastAccessTime());
+    assertEquals(content.getBytes(Charsets.UTF_8).length, attrs.size());
+  }
+
+  private String readContents(FileDelegate delegate, Path path)
+      throws IOException {
+    return CharStreams.toString(
+        new InputStreamReader(delegate.newInputStream(path), Charsets.UTF_8));
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockFileTest.java b/test/com/google/enterprise/adaptor/fs/MockFileTest.java
new file mode 100644
index 0000000..c65992b
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockFileTest.java
@@ -0,0 +1,309 @@
+// Copyright 2014 Google Inc.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import static org.junit.Assert.*;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+
+/**
+ * Test cases for {@link MockFile}.
+ */
+public class MockFileTest {
+
+  private static final FileTime createTime = FileTime.fromMillis(20000);
+  private static final FileTime modifyTime = FileTime.fromMillis(30000);
+  private static final FileTime accessTime = FileTime.fromMillis(40000);
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstructorNullName() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFile(null);
+  }
+
+  /** Test constructor for regular files. */
+  @Test
+  public void testFileConstructor() throws Exception {
+    MockFile f = new MockFile("test");
+    checkDefaultConfig(f, "test", false);
+    assertEquals("Contents of test", readContents(f));
+    assertEquals("text/plain", f.getContentType());
+  }
+
+  /** Test constructor for directories . */
+  @Test
+  public void testDirectoryConstructor() throws Exception {
+    MockFile f = new MockFile("test", true);
+    checkDefaultConfig(f, "test", true);
+    DirectoryStream<Path> ds = f.newDirectoryStream();
+    assertNotNull(ds);
+    assertFalse(ds.iterator().hasNext());
+    ds.close();
+  }
+
+  /**
+   * Verifies some of the default file attributes (no explicit setters called).
+   */
+  private void checkDefaultConfig(MockFile file, String name, boolean isDir)
+      throws Exception {
+    assertEquals(name, file.getName());
+    assertEquals(isDir, file.isDirectory());
+    assertEquals(!isDir, file.isRegularFile());
+    assertFalse(file.isHidden());
+    assertNull(file.getParent());
+    assertEquals(MockFile.DEFAULT_FILETIME, file.getCreationTime());
+    assertEquals(MockFile.DEFAULT_FILETIME, file.getLastModifiedTime());
+    assertEquals(MockFile.DEFAULT_FILETIME, file.getLastAccessTime());
+  }
+
+  private String readContents(MockFile file) throws IOException {
+    return CharStreams.toString(
+        new InputStreamReader(file.newInputStream(), Charsets.UTF_8));
+  }
+
+  @Test
+  public void testSetName() throws Exception {
+    assertEquals("foo", new MockFile("test").setName("foo").getName());
+  }
+
+  @Test
+  public void testSetIsRegularFile() throws Exception {
+    MockFile f = new MockFile("test").setIsRegularFile(false);
+    assertFalse(f.isRegularFile());
+    assertFalse(f.isDirectory());
+
+    f.setIsRegularFile(true);
+    assertTrue(f.isRegularFile());
+    assertFalse(f.isDirectory());
+  }
+
+  @Test
+  public void testSetIsHidden() throws Exception {
+    MockFile f = new MockFile("test");
+    assertFalse(f.isHidden());  // default is false.
+    assertTrue(f.setIsHidden(true).isHidden());
+    assertFalse(f.setIsHidden(false).isHidden());
+  }
+
+  @Test
+  public void testSetFileTimes() throws Exception {
+    MockFile f = new MockFile("test").setCreationTime(createTime)
+        .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime);
+    assertEquals(createTime, f.getCreationTime());
+    assertEquals(modifyTime, f.getLastModifiedTime());
+    assertEquals(accessTime, f.getLastAccessTime());
+  }
+
+  @Test
+  public void testReadBasicAttributesRegularFile() throws Exception {
+    MockFile f = new MockFile("test").setCreationTime(createTime)
+        .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime);
+
+    BasicFileAttributes bfa = f.readBasicAttributes();
+    assertNotNull(bfa);
+    assertSame(f, bfa.fileKey());
+    assertEquals(createTime, bfa.creationTime());
+    assertEquals(modifyTime, bfa.lastModifiedTime());
+    assertEquals(accessTime, bfa.lastAccessTime());
+    assertTrue(bfa.isRegularFile());
+    assertFalse(bfa.isDirectory());
+    assertFalse(bfa.isSymbolicLink());
+    assertFalse(bfa.isOther());
+    assertEquals(readContents(f).length(), bfa.size());
+  }
+
+  @Test
+  public void testReadBasicAttributesDirectory() throws Exception {
+    MockFile f = new MockFile("test", true).setCreationTime(createTime)
+        .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime);
+
+    BasicFileAttributes bfa = f.readBasicAttributes();
+    assertNotNull(bfa);
+    assertSame(f, bfa.fileKey());
+    assertEquals(createTime, bfa.creationTime());
+    assertEquals(modifyTime, bfa.lastModifiedTime());
+    assertEquals(accessTime, bfa.lastAccessTime());
+    assertFalse(bfa.isRegularFile());
+    assertTrue(bfa.isDirectory());
+    assertFalse(bfa.isSymbolicLink());
+    assertFalse(bfa.isOther());
+    assertEquals(0L, bfa.size());
+  }
+
+  @Test
+  public void testReadBasicAttributesSpecialFile() throws Exception {
+    // If neither file, nor directory, then it is "special".
+    MockFile f = new MockFile("test", false).setIsRegularFile(false);
+    BasicFileAttributes bfa = f.readBasicAttributes();
+    assertFalse(bfa.isRegularFile());
+    assertFalse(bfa.isDirectory());
+    assertFalse(bfa.isSymbolicLink());
+    assertTrue(bfa.isOther());
+    assertEquals(0L, bfa.size());
+  }
+
+  @Test
+  public void testSetFileContents() throws Exception {
+    String expected = "Hello World";
+    MockFile f = new MockFile("test.txt").setFileContents(expected);
+    assertEquals(expected, readContents(f));
+  }
+
+  @Test
+  public void testSetFileContentsBytes() throws Exception {
+    String expected = "<html><title>Hello World</title></html>";
+    MockFile f = new MockFile("test.html")
+        .setFileContents(expected.getBytes(Charsets.UTF_8))
+        .setContentType("text/html");
+    assertEquals(expected, readContents(f));
+    assertEquals("text/html", f.getContentType());
+  }
+
+  @Test
+  public void testChildren() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile dir1 = new MockFile("dir1", true);
+    MockFile dir2 = new MockFile("dir2", true);
+    MockFile test = new MockFile("test.txt");
+    root.addChildren(test, dir1, dir2);
+    assertNull(root.getParent());
+    assertSame(root, dir1.getParent());
+    assertSame(root, dir2.getParent());
+    assertSame(root, test.getParent());
+    checkDirectoryListing(root, dir1, dir2, test);
+
+    // Test getChild().
+    assertSame(dir1, root.getChild("dir1"));
+    assertSame(dir2, root.getChild("dir2"));
+    assertSame(test, root.getChild("test.txt"));
+
+    // Add another file.
+    MockFile newer = new MockFile("newer.txt");
+    root.addChildren(newer);
+    checkDirectoryListing(root, dir1, dir2, newer, test);
+  }
+
+  private void checkDirectoryListing(MockFile parent, MockFile... children)
+      throws IOException {
+    DirectoryStream<Path> ds = parent.newDirectoryStream();
+    assertNotNull(ds);
+    Iterator<Path> iter = ds.iterator();
+    assertNotNull(iter);
+    for (MockFile child : children) {
+      assertEquals(child.getName(), iter.next().getFileName().toString());
+    }
+    assertFalse(iter.hasNext());
+    ds.close();
+  }
+
+  @Test
+  public void testGetPath() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile dir1 = new MockFile("dir1", true);
+    MockFile test = new MockFile("test.txt");
+    MockFile test1 = new MockFile("test.txt");
+    root.addChildren(test, dir1);
+    dir1.addChildren(test1);
+    assertEquals("root", root.getPath());
+    assertEquals("root/dir1", dir1.getPath());
+    assertEquals("root/test.txt", test.getPath());
+    assertEquals("root/dir1/test.txt", test1.getPath());
+  }
+
+  @Test
+  public void testGetChildNullName() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFile("root", true).getChild(null);
+  }
+
+  @Test
+  public void testGetChildNotFound() throws Exception {
+    thrown.expect(FileNotFoundException.class);
+    new MockFile("root", true).getChild("nonExistent");
+  }
+
+  @Test
+  public void testGetDfsUncActiveStorageUnc() throws Exception {
+    MockFile root = new MockFile("root", true);
+    assertNull(root.getDfsUncActiveStorageUnc());
+    Path uncPath = Paths.get("\\\\server\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    assertEquals(uncPath, root.getDfsUncActiveStorageUnc());
+  }
+
+  @Test
+  public void testGetDfsShareAclView() throws Exception {
+    MockFile root = new MockFile("root", true);
+    assertNull(root.getDfsShareAclView());
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, root.getDfsShareAclView());
+  }
+
+  @Test
+  public void testDefaultRootAclViews() throws Exception {
+    MockFile root = new MockFile("root", true);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, root.getShareAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, root.getAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, root.getInheritedAclView());
+  }
+
+  @Test
+  public void testDefaultNonRootAclViews() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile dir1 = new MockFile("dir1", true);
+    MockFile foo = new MockFile("foo");
+    MockFile bar = new MockFile("bar");
+    root.addChildren(dir1, foo);
+    dir1.addChildren(bar);
+    assertEquals(MockFile.EMPTY_ACLVIEW, dir1.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, dir1.getInheritedAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, foo.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, foo.getInheritedAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, bar.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, bar.getInheritedAclView());
+  }
+
+  @Test
+  public void testSetAclViews() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile foo = new MockFile("foo");
+    root.addChildren(foo);
+    assertEquals(MockFile.EMPTY_ACLVIEW, foo.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, foo.getInheritedAclView());
+
+    foo.setAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    foo.setInheritedAclView(MockFile.EMPTY_ACLVIEW);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, foo.getAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, foo.getInheritedAclView());
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockRequest.java b/test/com/google/enterprise/adaptor/fs/MockRequest.java
new file mode 100644
index 0000000..850d2fa
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockRequest.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.Request;
+
+import java.util.Date;
+
+/** A trivial implemenation of {@link Request} */
+class MockRequest implements Request {
+  private final DocId docid;
+  private final Date lastAccess;
+
+  MockRequest(DocId docid) {
+    this(docid, null);
+  }
+
+  MockRequest(DocId docid, Date lastAccess) {
+    this.docid = docid;
+    this.lastAccess = lastAccess;
+  }
+
+  @Override
+  public boolean hasChangedSinceLastAccess(Date lastModified) {
+    return lastModified.after(lastAccess);
+  }
+
+  @Override
+  public Date getLastAccessTime() {
+    return lastAccess;
+  }
+
+  @Override
+  public DocId getDocId() {
+    return docid;
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockResponse.java b/test/com/google/enterprise/adaptor/fs/MockResponse.java
new file mode 100644
index 0000000..edbbaaa
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockResponse.java
@@ -0,0 +1,124 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.Response;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An implementation of {@link Response} that implements only those items that
+ * the adaptor uses.
+ */
+class MockResponse implements Response {
+
+  boolean notModified = false;
+  boolean notFound = false;
+  String contentType;
+  Date lastModified;
+  URI displayUrl;
+  Acl acl;
+  Map<String, String> metadata = new HashMap<String, String>();
+  Map<String, Acl> namedResources = new HashMap<String, Acl>();
+  ByteArrayOutputStream content;
+
+  @Override
+  public void respondNotModified() throws IOException {
+    notModified = true;
+  }
+
+  @Override
+  public void respondNotFound() throws IOException {
+    notFound = true;
+  }
+
+  @Override
+  public OutputStream getOutputStream() throws IOException {
+    content = new ByteArrayOutputStream();
+    return content;
+  }
+
+  @Override
+  public void setContentType(String contentType) {
+    this.contentType = contentType;
+  }
+
+  @Override
+  public void setLastModified(Date lastModified) {
+    this.lastModified = lastModified;
+  }
+
+  @Override
+  public void addMetadata(String key, String value) {
+    metadata.put(key, value);
+  }
+
+  @Override
+  public void setAcl(Acl acl) {
+    this.acl = acl;
+  }
+
+  @Override
+  public void putNamedResource(String fragment, Acl acl) {
+    namedResources.put(fragment, acl);
+  }
+
+  @Override
+  public void setDisplayUrl(URI displayUrl) {
+    this.displayUrl = displayUrl;
+  }
+
+  @Override
+  public void setSecure(boolean secure) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addAnchor(URI uri, String text) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setNoIndex(boolean noIndex) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setNoFollow(boolean noFollow) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setNoArchive(boolean noArchive) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setCrawlOnce(boolean crawlOnce) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setLock(boolean lock) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/UnsupportedDocIdPusher.java b/test/com/google/enterprise/adaptor/fs/UnsupportedDocIdPusher.java
new file mode 100644
index 0000000..e9b3439
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/UnsupportedDocIdPusher.java
@@ -0,0 +1,83 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// 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 com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdPusher;
+import com.google.enterprise.adaptor.ExceptionHandler;
+import com.google.enterprise.adaptor.GroupPrincipal;
+import com.google.enterprise.adaptor.Principal;
+
+import java.util.Collection;
+import java.util.Map;
+
+/** Throws UnsupportedOperationException for all calls. */
+class UnsupportedDocIdPusher implements DocIdPusher {
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds,
+      ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records,
+      ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources,
+      ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public GroupPrincipal pushGroupDefinitions(
+      Map<GroupPrincipal, ? extends Collection<Principal>> defs,
+      boolean caseSensitive) throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public GroupPrincipal pushGroupDefinitions(
+      Map<GroupPrincipal, ? extends Collection<Principal>> defs,
+      boolean caseSensitive, ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+}