Send feeds when generated HTML grows too long

This allows the GSA to still discover URLs that are past the point that
it actually indexes.

This code does suffer from an issue where a failure to write the HTML
prevents the feeds from being sent. The full solution would probably
entail stopping the generation of HTML and in a separate thread
continuing to loop through results.
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 de17f43..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);
@@ -714,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());
@@ -822,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());
@@ -916,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());
@@ -1003,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());
       }
@@ -1016,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:
@@ -1025,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,
@@ -1356,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) {
@@ -1383,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();
@@ -1407,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))
@@ -1734,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 ee15924..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,7 +294,7 @@
     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;
   }
@@ -332,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/"));
@@ -355,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);
@@ -373,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);
@@ -413,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"));
@@ -473,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"));
@@ -509,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"));
@@ -552,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);
@@ -596,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"));
@@ -626,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/"
@@ -670,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/"
@@ -713,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));
@@ -763,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/"
@@ -878,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/"
@@ -931,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;
@@ -1003,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/"
@@ -1047,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();
+  }
+}