Merge branch 'master' of https://code.google.com/p/plexi.sharepoint
diff --git a/src/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriter.java b/src/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriter.java
index c99a78b..86be21c 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriter.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriter.java
@@ -15,20 +15,32 @@
 package com.google.enterprise.adaptor.sharepoint;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.CountingOutputStream;
 import com.google.enterprise.adaptor.DocId;
 import com.google.enterprise.adaptor.DocIdEncoder;
+import com.google.enterprise.adaptor.DocIdPusher;
 
 import com.microsoft.schemas.sharepoint.soap.ObjectType;
 
 import java.io.*;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.charset.Charset;
 import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Locale;
+import java.util.concurrent.Executor;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 class HtmlResponseWriter implements Closeable {
+  /**
+   * The number of bytes that may be buffered within the streams. It should
+   * error on the side of being too large.
+   */
+  private static final long POSSIBLY_BUFFERED_BYTES = 1024;
+
   private static final Logger log
       = Logger.getLogger(HtmlResponseWriter.class.getName());
 
@@ -51,13 +63,22 @@
   private final Writer writer;
   private final DocIdEncoder docIdEncoder;
   private final Locale locale;
+  private final long thresholdBytes;
+  private final CountingOutputStream countingOutputStream;
+  private final DocIdPusher pusher;
+  private final Executor executor;
   private DocId docId;
   private URI docUri;
   private State state = State.INITIAL;
+  private final Collection<DocId> overflowDocIds = new ArrayList<DocId>(1024);
 
-  public HtmlResponseWriter(Writer writer, DocIdEncoder docIdEncoder,
-      Locale locale) {
-    if (writer == null) {
+  public HtmlResponseWriter(OutputStream os, Charset charset,
+      DocIdEncoder docIdEncoder, Locale locale, long thresholdBytes,
+      DocIdPusher pusher, Executor executor) {
+    if (os == null) {
+      throw new NullPointerException();
+    }
+    if (charset == null) {
       throw new NullPointerException();
     }
     if (docIdEncoder == null) {
@@ -66,9 +87,19 @@
     if (locale == null) {
       throw new NullPointerException();
     }
-    this.writer = writer;
+    if (pusher == null) {
+      throw new NullPointerException();
+    }
+    if (executor == null) {
+      throw new NullPointerException();
+    }
+    countingOutputStream = new CountingOutputStream(os);
+    this.writer = new OutputStreamWriter(countingOutputStream, charset);
     this.docIdEncoder = docIdEncoder;
     this.locale = locale;
+    this.thresholdBytes = thresholdBytes;
+    this.pusher = pusher;
+    this.executor = executor;
   }
 
   /**
@@ -124,6 +155,10 @@
     if (doc == null) {
       throw new NullPointerException();
     }
+    if (countingOutputStream.getCount() + POSSIBLY_BUFFERED_BYTES
+        > thresholdBytes) {
+      overflowDocIds.add(doc);
+    }
     writer.write("<li><a href=\"");
     writer.write(escapeAttributeValue(encodeDocId(doc)));
     writer.write("\">");
@@ -139,6 +174,18 @@
     if (state != State.STARTED && state != State.IN_SECTION) {
       throw new IllegalStateException("In unexpected state: " + state);
     }
+    if (!overflowDocIds.isEmpty()) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            pusher.pushDocIds(overflowDocIds);
+          } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+          }
+        }
+      });
+    }
     checkAndCloseSection();
     writer.write("</body></html>");
     writer.flush();
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
index cc2cfdf..0d1bbe9 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
@@ -99,8 +99,6 @@
     implements PollingIncrementalAdaptor {
   /** Charset used in generated HTML responses. */
   private static final Charset CHARSET = Charset.forName("UTF-8");
-  private static final String GENERATED_HTML_CONTENT_TYPE
-      = "text/html; charset=utf-8";
   private static final String XMLNS_DIRECTORY
       = "http://schemas.microsoft.com/sharepoint/soap/directory/";
 
@@ -228,6 +226,7 @@
   private final Callable<ExecutorService> executorFactory;
   private ExecutorService executor;
   private boolean xmlValidation;
+  private long maxIndexableSize;
   /** Authenticator instance that authenticates with SP. */
   /**
    * Cached value of whether we are talking to a SP 2010 server or not. This
@@ -280,6 +279,9 @@
     // allow us to improve the schema itself, but also allow enable users to
     // enable checking as a form of debugging.
     config.addKey("sharepoint.xmlValidation", "false");
+    // 2 MB. We need to know how much of the generated HTML the GSA will index,
+    // because the GSA won't see links outside of that content.
+    config.addKey("sharepoint.maxIndexableSize", "2097152");
   }
 
   @Override
@@ -292,6 +294,8 @@
         config.getValue("sharepoint.password"));
     xmlValidation = Boolean.parseBoolean(
         config.getValue("sharepoint.xmlValidation"));
+    maxIndexableSize = Integer.parseInt(
+        config.getValue("sharepoint.maxIndexableSize"));
 
     log.log(Level.CONFIG, "VirtualServer: {0}", virtualServer);
     log.log(Level.CONFIG, "Username: {0}", username);
@@ -506,12 +510,19 @@
     // turn them into URIs separately, and then turn everything into a
     // properly-escaped string.
     String[] parts = url.split("/", 4);
-    String host = parts[0] + "/" + parts[1] + "/" + parts[2] + "/";
+    if (parts.length < 3) {
+      throw new IllegalArgumentException("Too few '/'s: " + url);
+    }
+    String host = parts[0] + "/" + parts[1] + "/" + parts[2];
     // Host must be properly-encoded already.
     URI hostUri = URI.create(host);
+    if (parts.length == 3) {
+      // There was no path.
+      return hostUri;
+    }
     URI pathUri;
     try {
-      pathUri = new URI(null, null, parts[3], null);
+      pathUri = new URI(null, null, "/" + parts[3], null);
     } catch (URISyntaxException ex) {
       throw new IOException(ex);
     }
@@ -707,7 +718,6 @@
           .setPermitUsers(permitUsers).setPermitGroups(permitGroups)
           .setDenyUsers(denyUsers).setDenyGroups(denyGroups).build());
 
-      response.setContentType(GENERATED_HTML_CONTENT_TYPE);
       HtmlResponseWriter writer = createHtmlResponseWriter(response);
       writer.start(request.getDocId(), ObjectType.VIRTUAL_SERVER,
           vs.getMetadata().getURL());
@@ -815,7 +825,6 @@
       }
 
       response.setDisplayUrl(spUrlToUri(w.getMetadata().getURL()));
-      response.setContentType(GENERATED_HTML_CONTENT_TYPE);
       HtmlResponseWriter writer = createHtmlResponseWriter(response);
       writer.start(request.getDocId(), ObjectType.SITE,
           w.getMetadata().getTitle());
@@ -909,7 +918,6 @@
 
       response.setDisplayUrl(sharePointUrlToUri(
           l.getMetadata().getDefaultViewUrl()));
-      response.setContentType(GENERATED_HTML_CONTENT_TYPE);
       HtmlResponseWriter writer = createHtmlResponseWriter(response);
       writer.start(request.getDocId(), ObjectType.LIST,
           l.getMetadata().getTitle());
@@ -996,7 +1004,8 @@
       return attrs;
     }
 
-    private void addMetadata(Response response, String name, String value) {
+    private long addMetadata(Response response, String name, String value) {
+      long size = 0;
       if (name.startsWith("ows_")) {
         name = name.substring("ows_".length());
       }
@@ -1009,6 +1018,9 @@
             continue;
           }
           response.addMetadata(name, parts[i]);
+          // +30 for per-metadata-possible overhead, just to make sure that we
+          // don't count too few.
+          size += name.length() + parts[i].length() + 30;
         }
       } else if (value.startsWith(";#") && value.endsWith(";#")) {
         // This is a multi-choice field. Values will be in the form:
@@ -1018,10 +1030,17 @@
             continue;
           }
           response.addMetadata(name, part);
+          // +30 for per-metadata-possible overhead, just to make sure that we
+          // don't count too few.
+          size += name.length() + part.length() + 30;
         }
       } else {
         response.addMetadata(name, value);
+        // +30 for per-metadata-possible overhead, just to make sure that we
+        // don't count too few.
+        size += name.length() + value.length() + 30;
       }
+      return size;
     }
 
     private Acl.Builder generateAcl(List<Permission> permissions,
@@ -1349,8 +1368,10 @@
       String title = row.getAttribute(OWS_TITLE_ATTRIBUTE);
       String serverUrl = row.getAttribute(OWS_SERVERURL_ATTRIBUTE);
 
+      long metadataLength = 0;
       for (Attr attribute : getAllAttributes(row)) {
-        addMetadata(response, attribute.getName(), attribute.getValue());
+        metadataLength
+            += addMetadata(response, attribute.getName(), attribute.getValue());
       }
 
       if (isFolder) {
@@ -1376,8 +1397,8 @@
         } catch (URISyntaxException ex) {
           throw new IOException(ex);
         }
-        response.setContentType(GENERATED_HTML_CONTENT_TYPE);
-        HtmlResponseWriter writer = createHtmlResponseWriter(response);
+        HtmlResponseWriter writer
+            = createHtmlResponseWriter(response, metadataLength);
         writer.start(request.getDocId(), ObjectType.FOLDER, null);
         processFolder(listId, folder.substring(root.length()), writer);
         writer.finish();
@@ -1400,8 +1421,8 @@
         } catch (URISyntaxException ex) {
           throw new IOException(ex);
         }
-        response.setContentType(GENERATED_HTML_CONTENT_TYPE);
-        HtmlResponseWriter writer = createHtmlResponseWriter(response);
+        HtmlResponseWriter writer
+            = createHtmlResponseWriter(response, metadataLength);
         writer.start(request.getDocId(), ObjectType.LIST_ITEM, title);
         String strAttachments = row.getAttribute(OWS_ATTACHMENTS_ATTRIBUTE);
         int attachments = (strAttachments == null || "".equals(strAttachments))
@@ -1727,11 +1748,17 @@
 
     private HtmlResponseWriter createHtmlResponseWriter(Response response)
         throws IOException {
-      Writer writer
-          = new OutputStreamWriter(response.getOutputStream(), CHARSET);
+      return createHtmlResponseWriter(response, 0);
+    }
+
+    private HtmlResponseWriter createHtmlResponseWriter(
+        Response response, long metadataLength) throws IOException {
+      response.setContentType("text/html; charset=utf-8");
       // TODO(ejona): Get locale from request.
-      return new HtmlResponseWriter(writer, context.getDocIdEncoder(),
-          Locale.ENGLISH);
+      return new HtmlResponseWriter(response.getOutputStream(), CHARSET,
+          context.getDocIdEncoder(), Locale.ENGLISH,
+          maxIndexableSize - metadataLength, context.getDocIdPusher(),
+          executor);
     }
 
     public SiteDataClient getSiteDataClient() {
diff --git a/test/com/google/enterprise/adaptor/sharepoint/DelegatingDocIdPusher.java b/test/com/google/enterprise/adaptor/sharepoint/DelegatingDocIdPusher.java
new file mode 100644
index 0000000..3ae9d57
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/sharepoint/DelegatingDocIdPusher.java
@@ -0,0 +1,66 @@
+// 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.sharepoint;
+
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdPusher;
+import com.google.enterprise.adaptor.PushErrorHandler;
+
+import java.util.*;
+
+/** Forwards all calls to delegate. */
+abstract class DelegatingDocIdPusher implements DocIdPusher {
+  protected abstract DocIdPusher delegate();
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds)
+      throws InterruptedException {
+    return delegate().pushDocIds(docIds, null);
+  }
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds,
+                          PushErrorHandler handler)
+      throws InterruptedException {
+    return delegate().pushDocIds(docIds, handler);
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records)
+      throws InterruptedException {
+    return delegate().pushRecords(records, null);
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records,
+                            PushErrorHandler handler)
+      throws InterruptedException {
+    return delegate().pushRecords(records, handler);
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources)
+      throws InterruptedException {
+    return delegate().pushNamedResources(resources, null);
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources,
+                                  PushErrorHandler handler)
+      throws InterruptedException {
+    return delegate().pushNamedResources(resources, handler);
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriterTest.java b/test/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriterTest.java
index 8d043ae..7ae96f6 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriterTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/HtmlResponseWriterTest.java
@@ -18,16 +18,19 @@
 
 import com.google.enterprise.adaptor.Config;
 import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdPusher;
 
 import com.microsoft.schemas.sharepoint.soap.ObjectType;
 
 import org.junit.*;
 import org.junit.rules.ExpectedException;
 
-import java.io.StringWriter;
+import java.io.ByteArrayOutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.Locale;
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
 
 /**
  * Test cases for {@link HtmlResponseWriter}.
@@ -36,28 +39,95 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  private DocIdPusher docIdPusher = new UnsupportedDocIdPusher();
   private MockAdaptorContext context = new MockAdaptorContext(
-      new Config(), new AccumulatingDocIdPusher());
-  private StringWriter stringWriter = new StringWriter();
-  private HtmlResponseWriter writer = new HtmlResponseWriter(stringWriter,
-      context.getDocIdEncoder(), Locale.ENGLISH);
+      new Config(), docIdPusher);
+  private ExecutorService executor = new CallerRunsExecutor();
+  private Charset charset = Charset.forName("UTF-8");
+  private ByteArrayOutputStream baos = new ByteArrayOutputStream();
+  private HtmlResponseWriter writer = new HtmlResponseWriter(baos, charset,
+      context.getDocIdEncoder(), Locale.ENGLISH, 1024 * 1024, docIdPusher,
+      executor);
+
+  @After
+  public void shutdown() {
+    executor.shutdownNow();
+  }
 
   @Test
-  public void testConstructorNullWriter() {
+  public void testConstructorNullOutputStream() {
     thrown.expect(NullPointerException.class);
-    new HtmlResponseWriter(null, context.getDocIdEncoder(), Locale.ENGLISH);
+    new HtmlResponseWriter(null, charset, context.getDocIdEncoder(),
+        Locale.ENGLISH, 1024 * 1024, docIdPusher, executor);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testConstructorNullCharset() {
+    new HtmlResponseWriter(baos, null, context.getDocIdEncoder(),
+        Locale.ENGLISH, 1024 * 1024, docIdPusher, executor);
   }
 
   @Test
   public void testConstructorNullDocIdEncoder() {
     thrown.expect(NullPointerException.class);
-    new HtmlResponseWriter(new StringWriter(), null, Locale.ENGLISH);
+    new HtmlResponseWriter(baos, charset, null,
+        Locale.ENGLISH, 1024 * 1024, docIdPusher, executor);
   }
 
   @Test
   public void testConstructorNullLocale() {
     thrown.expect(NullPointerException.class);
-    new HtmlResponseWriter(new StringWriter(), context.getDocIdEncoder(), null);
+    new HtmlResponseWriter(baos, charset, context.getDocIdEncoder(),
+        null, 1024 * 1024, docIdPusher, executor);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testConstructorNullPusher() {
+    new HtmlResponseWriter(baos, charset, context.getDocIdEncoder(),
+        Locale.ENGLISH, 1024 * 1024, null, executor);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testConstructorNullExecutor() {
+    new HtmlResponseWriter(baos, charset, context.getDocIdEncoder(),
+        Locale.ENGLISH, 1024 * 1024, docIdPusher, null);
+  }
+
+  @Test
+  public void testBasicFlow() throws Exception {
+    final String golden = "<!DOCTYPE html>\n"
+        + "<html><head><title>Site s</title></head>"
+        + "<body><h1>Site s</h1>"
+        + "<p>Lists</p>"
+        + "<ul><li><a href=\"s/l\">My List</a></li></ul>"
+        + "</body></html>";
+    writer.start(new DocId("s"), ObjectType.SITE, null);
+    writer.startSection(ObjectType.LIST);
+    writer.addLink(new DocId("s/l"), "My List");
+    writer.finish();
+    assertEquals(golden, new String(baos.toByteArray(), charset));
+  }
+
+  @Test
+  public void testOverflowToDocIdPusher() throws Exception {
+    final String golden = "<!DOCTYPE html>\n"
+        + "<html><head><title>Site s</title></head>"
+        + "<body><h1>Site s</h1>"
+        + "<p>Lists</p>"
+        + "<ul><li><a href=\"s/l\">My List</a></li></ul>"
+        + "</body></html>";
+    final List<DocIdPusher.Record> goldenRecords = Arrays.asList(
+        new DocIdPusher.Record.Builder(new DocId("s/l")).build());
+    AccumulatingDocIdPusher docIdPusher = new AccumulatingDocIdPusher();
+    writer = new HtmlResponseWriter(baos, charset,
+        context.getDocIdEncoder(), Locale.ENGLISH, 1, docIdPusher,
+        executor);
+    writer.start(new DocId("s"), ObjectType.SITE, null);
+    writer.startSection(ObjectType.LIST);
+    writer.addLink(new DocId("s/l"), "My List");
+    writer.finish();
+    assertEquals(golden, new String(baos.toByteArray(), charset));
+    assertEquals(goldenRecords, docIdPusher.getRecords());
   }
 
   @Test
@@ -101,7 +171,7 @@
     writer.start(new DocId("a"), ObjectType.SITE, "");
     writer.finish();
     writer.close();
-    assertEquals(golden, stringWriter.toString());
+    assertEquals(golden, new String(baos.toByteArray(), charset));
   }
 
   @Test
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
index 1a14c13..789b111 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
@@ -171,6 +171,7 @@
   private final Charset charset = Charset.forName("UTF-8");
   private Config config;
   private SharePointAdaptor adaptor;
+  private DocIdPusher pusher = new UnsupportedDocIdPusher();
   private Callable<ExecutorService> executorFactory
       = new Callable<ExecutorService>() {
         @Override
@@ -293,12 +294,35 @@
     adaptor = new SharePointAdaptor(initableSiteDataFactory,
         new UnsupportedUserGroupFactory(),
         new UnsupportedHttpClient(), executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     adaptor.destroy();
     adaptor = null;
   }
 
   @Test
+  public void testSpUrlToUriPassthrough() throws Exception {
+    assertEquals("http://somehost:1/path/file",
+        SharePointAdaptor.spUrlToUri("http://somehost:1/path/file").toString());
+  }
+
+  @Test
+  public void testSpUrlToUriSpace() throws Exception {
+    assertEquals("http://somehost/A%20space",
+        SharePointAdaptor.spUrlToUri("http://somehost/A space").toString());
+  }
+
+  @Test
+  public void testSpUrlToUriPassthroughNoPath() throws Exception {
+    assertEquals("https://somehost",
+        SharePointAdaptor.spUrlToUri("https://somehost").toString());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testSpUrlToUriNoSceme() throws Exception {
+    SharePointAdaptor.spUrlToUri("http:/");
+  }
+
+  @Test
   public void testGetDocContentWrongServer() throws Exception {
     SiteDataFactory siteDataFactory = MockSiteDataFactory.blank()
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
@@ -309,7 +333,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(),
         new UnsupportedHttpClient(), executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://wronghost:1/"));
@@ -332,7 +356,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(new DocId(wrongPage));
     GetContentsResponse response = new GetContentsResponse(baos);
@@ -350,7 +374,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsResponse response = new GetContentsResponse(baos);
     adaptor.getDocContent(new GetContentsRequest(new DocId("")), response);
@@ -390,7 +414,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection"));
@@ -450,7 +474,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection"));
@@ -486,7 +510,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection"));
@@ -529,7 +553,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
 
     // This populates the cache, but otherwise doesn't test anything new.
     siteData.setSiteDataSoap(siteDataState1);
@@ -573,7 +597,7 @@
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection"));
@@ -603,7 +627,7 @@
     adaptor = new SharePointAdaptor(initableSiteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
@@ -647,7 +671,7 @@
     adaptor = new SharePointAdaptor(initableSiteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
@@ -690,7 +714,7 @@
         return new FileInfo.Builder(contents).setHeaders(headers).build();
       }
     }, executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId(attachmentId));
@@ -740,7 +764,7 @@
     adaptor = new SharePointAdaptor(initableSiteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
@@ -855,7 +879,7 @@
     adaptor = new SharePointAdaptor(initableSiteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
@@ -908,7 +932,7 @@
           .endpoint(SITES_SITECOLLECTION_ENDPOINT, new UnsupportedSiteData()),
         mockUserGroupFactory, new UnsupportedHttpClient(), executorFactory);
     final AccumulatingDocIdPusher docIdPusher = new AccumulatingDocIdPusher();
-    adaptor.init(new MockAdaptorContext(config, null) {
+    adaptor.init(new MockAdaptorContext(config, pusher) {
       @Override
       public DocIdPusher getDocIdPusher() {
         return docIdPusher;
@@ -980,7 +1004,7 @@
     adaptor = new SharePointAdaptor(initableSiteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
@@ -1024,7 +1048,7 @@
     adaptor = new SharePointAdaptor(initableSiteDataFactory,
         new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
         executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
+    adaptor.init(new MockAdaptorContext(config, pusher));
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     GetContentsRequest request = new GetContentsRequest(
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
diff --git a/test/com/google/enterprise/adaptor/sharepoint/UnsupportedDocIdPusher.java b/test/com/google/enterprise/adaptor/sharepoint/UnsupportedDocIdPusher.java
new file mode 100644
index 0000000..c7e2fc0
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/sharepoint/UnsupportedDocIdPusher.java
@@ -0,0 +1,25 @@
+// 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.sharepoint;
+
+import com.google.enterprise.adaptor.DocIdPusher;
+
+/** Throws UnsupportedOperationException for all calls. */
+class UnsupportedDocIdPusher extends DelegatingDocIdPusher {
+  @Override
+  protected DocIdPusher delegate() {
+    throw new UnsupportedOperationException();
+  }
+}