Split SiteDataClient, along soap vs adaptor line

SiteDataClient is now a thin wrapper around the SOAP calls that performs
the necessary parsing and slightly improves the interfaces for our
usage. The rest of the methods are now in a SiteAdaptor class.
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
index f375629..87f1bdb 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
@@ -30,6 +30,9 @@
 import com.google.enterprise.adaptor.Request;
 import com.google.enterprise.adaptor.Response;
 import com.google.enterprise.adaptor.UserPrincipal;
+import com.google.enterprise.adaptor.sharepoint.SiteDataClient.CursorPaginator;
+import com.google.enterprise.adaptor.sharepoint.SiteDataClient.Paginator;
+import com.google.enterprise.adaptor.sharepoint.SiteDataClient.XmlProcessingException;
 
 import com.microsoft.schemas.sharepoint.soap.ContentDatabase;
 import com.microsoft.schemas.sharepoint.soap.ContentDatabases;
@@ -67,14 +70,11 @@
 import com.microsoft.schemas.sharepoint.soap.directory.UserGroupSoap;
 
 import org.w3c.dom.Attr;
-import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 
-import org.xml.sax.SAXException;
-
 import java.io.*;
 import java.net.*;
 import java.nio.charset.Charset;
@@ -83,22 +83,10 @@
 import java.util.logging.*;
 import java.util.regex.Pattern;
 
-import javax.xml.XMLConstants;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
 import javax.xml.namespace.QName;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.Source;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamSource;
-import javax.xml.validation.Schema;
-import javax.xml.validation.SchemaFactory;
 import javax.xml.ws.EndpointReference;
 import javax.xml.ws.Holder;
 import javax.xml.ws.Service;
-import javax.xml.ws.WebServiceException;
 import javax.xml.ws.wsaddressing.W3CEndpointReferenceBuilder;
 
 /**
@@ -108,9 +96,6 @@
     implements PollingIncrementalAdaptor {
   /** Charset used in generated HTML responses. */
   private static final Charset CHARSET = Charset.forName("UTF-8");
-  /** SharePoint's namespace. */
-  private static final String XMLNS
-      = "http://schemas.microsoft.com/sharepoint/soap/";
   private static final String XMLNS_DIRECTORY
       = "http://schemas.microsoft.com/sharepoint/soap/directory/";
 
@@ -163,7 +148,7 @@
   private static final String OWS_ATTACHMENTS_ATTRIBUTE = "ows_Attachments";
   /**
    * Matches a SP-encoded value that contains one or more values. See {@link
-   * SiteDataClient.addMetadata}.
+   * SiteAdaptor.addMetadata}.
    */
   private static final Pattern ALTERNATIVE_VALUE_PATTERN
       = Pattern.compile("^\\d+;#");
@@ -190,52 +175,16 @@
       = OPEN_MASK | VIEW_PAGES_MASK | VIEW_LIST_ITEMS_MASK | MANAGE_LIST_MASK;
 
   private static final int LIST_READ_SECURITY_ENABLED = 2;
-  /**
-   * The JAXBContext is expensive to initialize, so we share a copy between
-   * instances.
-   */
-  private static final JAXBContext jaxbContext;
-  /**
-   * XML Schema of requests and responses. Used to validate responses match
-   * expectations.
-   */
-  private static final Schema schema;
-
-  static {
-    try {
-      jaxbContext = JAXBContext.newInstance(
-          "com.microsoft.schemas.sharepoint.soap");
-    } catch (JAXBException ex) {
-      throw new RuntimeException("Could not initialize JAXBContext", ex);
-    }
-
-    try {
-      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-      dbf.setNamespaceAware(true);
-      Document doc = dbf.newDocumentBuilder()
-          .parse(SiteDataSoap.class.getResourceAsStream("SiteData.wsdl"));
-      String schemaNs = XMLConstants.W3C_XML_SCHEMA_NS_URI;
-      Node schemaNode = doc.getElementsByTagNameNS(schemaNs, "schema").item(0);
-      schema = SchemaFactory.newInstance(schemaNs).newSchema(
-          new DOMSource(schemaNode));
-    } catch (IOException ex) {
-      throw new RuntimeException("Could not initialize Schema", ex);
-    } catch (SAXException ex) {
-      throw new RuntimeException("Could not initialize Schema", ex);
-    } catch (ParserConfigurationException ex) {
-      throw new RuntimeException("Could not initialize Schema", ex);
-    }
-  }
 
   private static final Logger log
       = Logger.getLogger(SharePointAdaptor.class.getName());
 
   /**
-   * Map from Site or Web URL to SiteDataClient object used to communicate with
+   * Map from Site or Web URL to SiteAdaptor object used to communicate with
    * that Site/Web.
    */
-  private final ConcurrentMap<String, SiteDataClient> clients
-      = new ConcurrentSkipListMap<String, SiteDataClient>();
+  private final ConcurrentMap<String, SiteAdaptor> siteAdaptors
+      = new ConcurrentSkipListMap<String, SiteAdaptor>();
   private final DocId virtualServerDocId = new DocId("");
   private AdaptorContext context;
   /**
@@ -262,7 +211,7 @@
   private final ConcurrentSkipListMap<String, String> contentDatabaseChangeId
       = new ConcurrentSkipListMap<String, String>();
   /** Production factory for all SiteDataSoap communication objects. */
-  private final SiteDataFactory siteDataFactory;
+  private final SiteDataClient.SiteDataFactory siteDataFactory;
   private final UserGroupFactory userGroupFactory;
   /** Client for initiating raw HTTP connections. */
   private final HttpClient httpClient;
@@ -278,12 +227,12 @@
   private NtlmAuthenticator ntlmAuthenticator;
 
   public SharePointAdaptor() {
-    this(new SiteDataFactoryImpl(), new UserGroupFactoryImpl(),
+    this(new SiteDataClient.SiteDataFactoryImpl(), new UserGroupFactoryImpl(),
         new HttpClientImpl(), new CachedThreadPoolFactory());
   }
 
   @VisibleForTesting
-  SharePointAdaptor(SiteDataFactory siteDataFactory,
+  SharePointAdaptor(SiteDataClient.SiteDataFactory siteDataFactory,
       UserGroupFactory userGroupFactory, HttpClient httpClient,
       Callable<ExecutorService> executorFactory) {
     if (siteDataFactory == null || httpClient == null
@@ -341,7 +290,8 @@
 
     // Test out configuration.
     try {
-      getSiteDataClient(virtualServer, virtualServer).getContentVirtualServer();
+      getSiteAdaptor(virtualServer, virtualServer).getSiteDataClient()
+          .getContentVirtualServer();
     } catch (Exception e) {
       // Don't leak the executor.
       destroy();
@@ -369,20 +319,20 @@
     log.entering("SharePointAdaptor", "getDocContent",
         new Object[] {request, response});
     DocId id = request.getDocId();
-    SiteDataClient virtualServerClient
-        = getSiteDataClient(virtualServer, virtualServer);
+    SiteAdaptor virtualServerSiteAdaptor
+        = getSiteAdaptor(virtualServer, virtualServer);
     if (id.equals(virtualServerDocId)) {
-      virtualServerClient.getVirtualServerDocContent(request, response);
+      virtualServerSiteAdaptor.getVirtualServerDocContent(request, response);
     } else {
-      SiteDataClient client
-          = virtualServerClient.getClientForUrl(id.getUniqueId());
-      if (client == null) {
+      SiteAdaptor siteAdaptor
+          = virtualServerSiteAdaptor.getAdaptorForUrl(id.getUniqueId());
+      if (siteAdaptor == null) {
         log.log(Level.FINE, "responding not found");
         response.respondNotFound();
         log.exiting("SharePointAdaptor", "getDocContent");
         return;
       }
-      client.getDocContent(request, response);
+      siteAdaptor.getDocContent(request, response);
     }
     log.exiting("SharePointAdaptor", "getDocContent");
   }
@@ -398,7 +348,8 @@
   public void getModifiedDocIds(DocIdPusher pusher)
       throws InterruptedException, IOException {
     log.entering("SharePointAdaptor", "getModifiedDocIds", pusher);
-    SiteDataClient client = getSiteDataClient(virtualServer, virtualServer);
+    SiteAdaptor siteAdaptor = getSiteAdaptor(virtualServer, virtualServer);
+    SiteDataClient client = siteAdaptor.getSiteDataClient();
     VirtualServer vs = null;
     try {
       vs = client.getContentVirtualServer();
@@ -479,7 +430,7 @@
             if (changes == null) {
               break;
             }
-            client.getModifiedDocIds(changes, pusher);
+            siteAdaptor.getModifiedDocIds(changes, pusher);
           } catch (XmlProcessingException ex) {
             log.log(Level.WARNING, "Error parsing changes from content "
                 + "database: " + contentDatabase, ex);
@@ -500,14 +451,14 @@
     log.exiting("SharePointAdaptor", "getModifiedDocIds", pusher);
   }
 
-  private SiteDataClient getSiteDataClient(String site, String web)
+  private SiteAdaptor getSiteAdaptor(String site, String web)
       throws IOException {
     if (web.endsWith("/")) {
       // Always end without a '/' for a canonical form.
       web = web.substring(0, web.length() - 1);
     }
-    SiteDataClient client = clients.get(web);
-    if (client == null) {
+    SiteAdaptor siteAdaptor = siteAdaptors.get(web);
+    if (siteAdaptor == null) {
       if (site.endsWith("/")) {
         // Always end without a '/' for a canonical form.
         site = site.substring(0, site.length() - 1);
@@ -520,16 +471,16 @@
       UserGroupSoap userGroupSoap
           = userGroupFactory.newUserGroup(endpointUserGroup);
 
-      client = new SiteDataClient(site, web, siteDataSoap, userGroupSoap,
+      siteAdaptor = new SiteAdaptor(site, web, siteDataSoap, userGroupSoap,
           new MemberIdMappingCallable(site),
           new SiteUserIdMappingCallable(site));
-      clients.putIfAbsent(web, client);
-      client = clients.get(web);
+      siteAdaptors.putIfAbsent(web, siteAdaptor);
+      siteAdaptor = siteAdaptors.get(web);
     }
-    return client;
+    return siteAdaptor;
   }
 
-  private static URI spUrlToUri(String url) throws IOException {
+  static URI spUrlToUri(String url) throws IOException {
     // Because SP is silly, the path of the URI is unencoded, but the rest of
     // the URI is correct. Thus, we split up the path from the host, and then
     // turn them into URIs separately, and then turn everything into a
@@ -552,8 +503,8 @@
   }
 
   @VisibleForTesting
-  class SiteDataClient {
-    private final CheckedExceptionSiteDataSoap siteData;
+  class SiteAdaptor {
+    private final SiteDataClient siteDataClient;
     private final UserGroupSoap userGroup;
     private final String siteUrl;
     private final String webUrl;
@@ -565,11 +516,11 @@
     private final Callable<MemberIdMapping> memberIdMappingCallable;
     private final Callable<MemberIdMapping> siteUserIdMappingCallable;
 
-    public SiteDataClient(String site, String web, SiteDataSoap siteDataSoap,
+    public SiteAdaptor(String site, String web, SiteDataSoap siteDataSoap,
         UserGroupSoap userGroupSoap,
         Callable<MemberIdMapping> memberIdMappingCallable,
         Callable<MemberIdMapping> siteUserIdMappingCallable) {
-      log.entering("SiteDataClient", "SiteDataClient",
+      log.entering("SiteAdaptor", "SiteAdaptor",
           new Object[] {site, web, siteDataSoap});
       if (site.endsWith("/")) {
         throw new AssertionError();
@@ -577,17 +528,16 @@
       if (web.endsWith("/")) {
         throw new AssertionError();
       }
-      if (siteDataSoap == null || memberIdMappingCallable == null) {
+      if (memberIdMappingCallable == null) {
         throw new NullPointerException();
       }
       this.siteUrl = site;
       this.webUrl = web;
-      siteDataSoap = LoggingWSHandler.create(SiteDataSoap.class, siteDataSoap);
       this.userGroup = userGroupSoap;
-      this.siteData = new CheckedExceptionSiteDataSoapAdapter(siteDataSoap);
+      this.siteDataClient = new SiteDataClient(siteDataSoap, xmlValidation);
       this.memberIdMappingCallable = memberIdMappingCallable;
       this.siteUserIdMappingCallable = siteUserIdMappingCallable;
-      log.exiting("SiteDataClient", "SiteDataClient");
+      log.exiting("SiteAdaptor", "SiteAdaptor");
     }
 
     private MemberIdMapping getMemberIdMapping() throws IOException {
@@ -612,23 +562,22 @@
 
     public void getDocContent(Request request, Response response)
         throws IOException {
-      log.entering("SiteDataClient", "getDocContent",
+      log.entering("SiteAdaptor", "getDocContent",
           new Object[] {request, response});
       String url = request.getDocId().getUniqueId();
       if (getAttachmentDocContent(request, response)) {
         // Success, it was an attachment.
-        log.exiting("SiteDataClient", "getDocContent");
+        log.exiting("SiteAdaptor", "getDocContent");
         return;
       }
 
       Holder<String> listId = new Holder<String>();
       Holder<String> itemId = new Holder<String>();
-      Holder<Boolean> result = new Holder<Boolean>();
       // No need to retrieve webId, since it isn't populated when you contact a
       // web's SiteData.asmx page instead of its parent site's.
-      siteData.getURLSegments(request.getDocId().getUniqueId(), result, null,
-          null, listId, itemId);
-      if (!result.value) {
+      boolean result = siteDataClient.getUrlSegments(
+          request.getDocId().getUniqueId(), listId, itemId);
+      if (!result) {
         // It may still be an aspx page.
         if (request.getDocId().getUniqueId().toLowerCase(Locale.ENGLISH)
             .endsWith(".aspx")) {
@@ -637,7 +586,7 @@
           log.log(Level.FINE, "responding not found");
           response.respondNotFound();
         }
-        log.exiting("SiteDataClient", "getDocContent");
+        log.exiting("SiteAdaptor", "getDocContent");
         return;
       }
       if (itemId.value != null) {
@@ -648,11 +597,11 @@
         // Assume it is a top-level site.
         getSiteDocContent(request, response);
       }
-      log.exiting("SiteDataClient", "getDocContent");
+      log.exiting("SiteAdaptor", "getDocContent");
     }
 
     private DocId encodeDocId(String url) {
-      log.entering("SiteDataClient", "encodeDocId", url);
+      log.entering("SiteAdaptor", "encodeDocId", url);
       if (url.toLowerCase().startsWith("https://")
           || url.toLowerCase().startsWith("http://")) {
         // Leave as-is.
@@ -665,7 +614,7 @@
         url = parts[0] + "//" + parts[2] + url;
       }
       DocId docId = new DocId(url);
-      log.exiting("SiteDataClient", "encodeDocId", docId);
+      log.exiting("SiteAdaptor", "encodeDocId", docId);
       return docId;
     }
 
@@ -684,9 +633,9 @@
 
     private void getVirtualServerDocContent(Request request, Response response)
         throws IOException {
-      log.entering("SiteDataClient", "getVirtualServerDocContent",
+      log.entering("SiteAdaptor", "getVirtualServerDocContent",
           new Object[] {request, response});
-      VirtualServer vs = getContentVirtualServer();
+      VirtualServer vs = siteDataClient.getContentVirtualServer();
 
       final long necessaryPermissionMask = LIST_ITEM_MASK;
       // A PolicyUser is either a user or group, but we aren't provided with
@@ -726,7 +675,8 @@
       DocIdEncoder encoder = context.getDocIdEncoder();
       for (ContentDatabases.ContentDatabase cdcd
           : vs.getContentDatabases().getContentDatabase()) {
-        ContentDatabase cd = getContentContentDatabase(cdcd.getID(), true);
+        ContentDatabase cd
+            = siteDataClient.getContentContentDatabase(cdcd.getID(), true);
         if (cd.getSites() != null) {
           for (Sites.Site site : cd.getSites().getSite()) {
             writer.addLink(encodeDocId(site.getURL()), null);
@@ -734,7 +684,7 @@
         }
       }
       writer.finish();
-      log.exiting("SiteDataClient", "getVirtualServerDocContent");
+      log.exiting("SiteAdaptor", "getVirtualServerDocContent");
     }
 
     /**
@@ -768,19 +718,20 @@
       if (isWebSiteCollection()) {
         return false;
       }
-      return isWebNoIndex(getClientForUrl(getWebParentUrl()).getContentWeb());
+      return isWebNoIndex(
+          getAdaptorForUrl(getWebParentUrl()).siteDataClient.getContentWeb());
     }
 
     private void getSiteDocContent(Request request, Response response)
         throws IOException {
-      log.entering("SiteDataClient", "getSiteDocContent",
+      log.entering("SiteAdaptor", "getSiteDocContent",
           new Object[] {request, response});
-      Web w = getContentWeb();
+      Web w = siteDataClient.getContentWeb();
 
       if (isWebNoIndex(w)) {
         log.fine("Document marked for NoIndex");
         response.respondNotFound();
-        log.exiting("SiteDataClient", "getSiteDocContent");
+        log.exiting("SiteAdaptor", "getSiteDocContent");
         return;
       }
 
@@ -793,8 +744,8 @@
       // policy is additional web service call.
       // TODO(ejona): Add caching for web application policy.
       if (allowAnonymousAccess) {
-        allowAnonymousAccess 
-            = !isDenyAnonymousAcessOnVirtualServer(getContentVirtualServer());
+        allowAnonymousAccess = !isDenyAnonymousAcessOnVirtualServer(
+            siteDataClient.getContentVirtualServer());
       }
 
       if (!allowAnonymousAccess) {
@@ -802,8 +753,8 @@
         if (isWebSiteCollection()) {
           includePermissions = true;
         } else {
-          SiteDataClient parentClient = getClientForUrl(getWebParentUrl());
-          Web parentW = parentClient.getContentWeb();
+          SiteAdaptor parentSiteAdaptor = getAdaptorForUrl(getWebParentUrl());
+          Web parentW = parentSiteAdaptor.siteDataClient.getContentWeb();
           String parentScopeId
               = parentW.getMetadata().getScopeID().toLowerCase(Locale.ENGLISH);
           String scopeId
@@ -868,21 +819,22 @@
         }
       }
       writer.finish();
-      log.exiting("SiteDataClient", "getSiteDocContent");
+      log.exiting("SiteAdaptor", "getSiteDocContent");
     }
 
     private void getListDocContent(Request request, Response response,
         String id) throws IOException {
-      log.entering("SiteDataClient", "getListDocContent",
+      log.entering("SiteAdaptor", "getListDocContent",
           new Object[] {request, response, id});
-      com.microsoft.schemas.sharepoint.soap.List l = getContentList(id);
-      Web w = getContentWeb();
+      com.microsoft.schemas.sharepoint.soap.List l
+          = siteDataClient.getContentList(id);
+      Web w = siteDataClient.getContentWeb();
 
       if (TrueFalseType.TRUE.equals(l.getMetadata().getNoIndex())
           || isWebNoIndex(w)) {
         log.fine("Document marked for NoIndex");
         response.respondNotFound();
-        log.exiting("SiteDataClient", "getListDocContent");
+        log.exiting("SiteAdaptor", "getListDocContent");
         return;
       }
 
@@ -890,8 +842,8 @@
           = isAllowAnonymousReadForList(l) && isAllowAnonymousPeekForWeb(w);
 
       if (allowAnonymousAccess) {
-        allowAnonymousAccess 
-            = !isDenyAnonymousAcessOnVirtualServer(getContentVirtualServer());
+        allowAnonymousAccess = !isDenyAnonymousAcessOnVirtualServer(
+            siteDataClient.getContentVirtualServer());
       }
 
       if (!allowAnonymousAccess) {
@@ -922,7 +874,7 @@
           l.getMetadata().getTitle());
       processFolder(id, "", writer);
       writer.finish();
-      log.exiting("SiteDataClient", "getListDocContent");
+      log.exiting("SiteAdaptor", "getListDocContent");
     }
 
     /**
@@ -931,10 +883,10 @@
      */
     private void processFolder(String listGuid, String folderPath,
         HtmlResponseWriter writer) throws IOException {
-      log.entering("SiteDataClient", "processFolder",
+      log.entering("SiteAdaptor", "processFolder",
           new Object[] {listGuid, folderPath, writer});
       Paginator<ItemData> folderPaginator
-          = getContentFolderChildren(listGuid, folderPath);
+          = siteDataClient.getContentFolderChildren(listGuid, folderPath);
       writer.startSection(ObjectType.LIST_ITEM);
       ItemData folder;
       while ((folder = folderPaginator.next()) != null) {
@@ -947,7 +899,7 @@
           writer.addLink(encodeDocId(rowUrl), rowTitle);
         }
       }
-      log.exiting("SiteDataClient", "processFolder");
+      log.exiting("SiteAdaptor", "processFolder");
     }
 
     private boolean elementHasName(Element ele, QName name) {
@@ -1139,17 +1091,17 @@
 
     private void getAspxDocContent(Request request, Response response)
         throws IOException {
-      log.entering("SiteDataClient", "getAspxDocContent",
+      log.entering("SiteAdaptor", "getAspxDocContent",
           new Object[] {request, response});
-      Web w = getContentWeb();
+      Web w = siteDataClient.getContentWeb();
       boolean allowAnonymousAccess = isAllowAnonymousReadForWeb(w);
       // Check if anonymous access is denied by web application policy
       // only if anonymous access is enabled for web as checking web application
       // policy is additional web service call.
       // TODO(ejona): Add caching for web application policy.
       if (allowAnonymousAccess) {
-        allowAnonymousAccess 
-            = !isDenyAnonymousAcessOnVirtualServer(getContentVirtualServer());
+        allowAnonymousAccess = !isDenyAnonymousAcessOnVirtualServer(
+            siteDataClient.getContentVirtualServer());
       }
       if (!allowAnonymousAccess) {
         String aspxId = request.getDocId().getUniqueId();
@@ -1159,7 +1111,7 @@
             .build());
       }
       getFileDocContent(request, response);
-      log.exiting("SiteDataClient", "getAspxDocContent");
+      log.exiting("SiteAdaptor", "getAspxDocContent");
     }
 
     /**
@@ -1171,7 +1123,7 @@
      */
     private void getFileDocContent(Request request, Response response)
         throws IOException {
-      log.entering("SiteDataClient", "getFileDocContent",
+      log.entering("SiteAdaptor", "getFileDocContent",
           new Object[] {request, response});
       URI displayUrl = docIdToUri(request.getDocId());
       FileInfo fi = httpClient.issueGetRequest(displayUrl.toURL());
@@ -1189,27 +1141,28 @@
       } finally {
         fi.getContents().close();
       }
-      log.exiting("SiteDataClient", "getFileDocContent");
+      log.exiting("SiteAdaptor", "getFileDocContent");
     }
 
     private void getListItemDocContent(Request request, Response response,
         String listId, String itemId) throws IOException {
-      log.entering("SiteDataClient", "getListItemDocContent",
+      log.entering("SiteAdaptor", "getListItemDocContent",
           new Object[] {request, response, listId, itemId});
-      com.microsoft.schemas.sharepoint.soap.List l = getContentList(listId);
-      Web w = getContentWeb();
+      com.microsoft.schemas.sharepoint.soap.List l
+          = siteDataClient.getContentList(listId);
+      Web w = siteDataClient.getContentWeb();
 
       if (TrueFalseType.TRUE.equals(l.getMetadata().getNoIndex())
           || isWebNoIndex(w)) {
         log.fine("Document marked for NoIndex");
         response.respondNotFound();
-        log.exiting("SiteDataClient", "getListItemDocContent");
+        log.exiting("SiteAdaptor", "getListItemDocContent");
         return;
       }
 
       boolean applyReadSecurity =
           (l.getMetadata().getReadSecurity() == LIST_READ_SECURITY_ENABLED);
-      ItemData i = getContentItem(listId, itemId);
+      ItemData i = siteDataClient.getContentItem(listId, itemId);
 
       Xml xml = i.getXml();
       Element data = getFirstChildWithName(xml, DATA_ELEMENT);
@@ -1235,9 +1188,19 @@
           && scopeId.equals(listScopeId)
           && isAllowAnonymousPeekForWeb(w);
 
+      // Check anomymous access on web only if anonymous access is applicable
+      // for list and list item.
       if (allowAnonymousAccess) {
-        allowAnonymousAccess 
-            = !isDenyAnonymousAcessOnVirtualServer(getContentVirtualServer());
+        // Even if anonymous access is enabled on list, it can be turned off
+        // on Web level by setting Anonymous access to "Nothing" on Web.
+        // Anonymous User must have minimum "Open" permission on Web
+        // for anonymous access to work on List and List Items.
+        allowAnonymousAccess = isAllowAnonymousPeekForWeb(w);
+      }
+
+      if (allowAnonymousAccess) {
+        allowAnonymousAccess = !isDenyAnonymousAcessOnVirtualServer(
+            siteDataClient.getContentVirtualServer());
       }
 
       if (!allowAnonymousAccess) {
@@ -1264,26 +1227,26 @@
         if (parentIsList || scopeId.equals(listScopeId)) {
           parentScopeId = listScopeId;
         } else {
-            // Instead of using getURLSegments and getContent(ListItem),
-            // we could use just getContent(Folder).
-            // However, getContent(Folder) always returns children which could
-            // make the call very expensive. In addition, getContent(ListItem)
-            // returns all the metadata for the folder instead of just its scope
-            // so if in the future we need more metadata we will already have
-            // it. GetContentEx(Folder) may provide a way to get the folder's
-            // scope without its children, but it wasn't investigated.
+          // Instead of using getUrlSegments and getContent(ListItem), we could
+          // use just getContent(Folder). However, getContent(Folder) always
+          // returns children which could make the call very expensive. In
+          // addition, getContent(ListItem) returns all the metadata for the
+          // folder instead of just its scope so if in the future we need more
+          // metadata we will already have it. GetContentEx(Folder) may provide
+          // a way to get the folder's scope without its children, but it wasn't
+          // investigated.
           Holder<String> folderListId = new Holder<String>();
           Holder<String> folderItemId = new Holder<String>();
-          Holder<Boolean> result = new Holder<Boolean>();
-          siteData.getURLSegments(folderDocId.getUniqueId(), result, null,
-              null, folderListId, folderItemId);
-          if (!result.value) {
+          boolean result = siteDataClient.getUrlSegments(
+              folderDocId.getUniqueId(), folderListId, folderItemId);
+          if (!result) {
             throw new IOException("Could not find parent folder's itemId");
           }
           if (!listId.equals(folderListId.value)) {
             throw new AssertionError("Unexpected listId value");
           }
-          ItemData folderItem = getContentItem(listId, folderItemId.value);
+          ItemData folderItem
+              = siteDataClient.getContentItem(listId, folderItemId.value);
           Element folderData = getFirstChildWithName(
               folderItem.getXml(), DATA_ELEMENT);
           Element folderRow
@@ -1399,7 +1362,7 @@
         writer.start(request.getDocId(), ObjectType.FOLDER, null);
         processFolder(listId, folder.substring(root.length()), writer);
         writer.finish();
-        log.exiting("SiteDataClient", "getListItemDocContent");
+        log.exiting("SiteAdaptor", "getListItemDocContent");
         return;
       }
       String contentTypeId = row.getAttribute(OWS_CONTENTTYPEID_ATTRIBUTE);
@@ -1427,24 +1390,25 @@
             ? 0 : Integer.parseInt(strAttachments);
         if (attachments > 0) {
           writer.startSection(ObjectType.LIST_ITEM_ATTACHMENTS);
-          Item item = getContentListItemAttachments(listId, itemId);
+          Item item
+              = siteDataClient.getContentListItemAttachments(listId, itemId);
           for (Item.Attachment attachment : item.getAttachment()) {
             writer.addLink(encodeDocId(attachment.getURL()), null);
           }
         }
         writer.finish();
       }
-      log.exiting("SiteDataClient", "getListItemDocContent");
+      log.exiting("SiteAdaptor", "getListItemDocContent");
     }
 
     private boolean getAttachmentDocContent(Request request, Response response)
         throws IOException {
-      log.entering("SiteDataClient", "getAttachmentDocContent", new Object[] {
+      log.entering("SiteAdaptor", "getAttachmentDocContent", new Object[] {
           request, response});
       String url = request.getDocId().getUniqueId();
       if (!url.contains("/Attachments/")) {
         log.fine("Not an attachment: does not contain /Attachments/");
-        log.exiting("SiteDataClient", "getAttachmentDocContent", false);
+        log.exiting("SiteAdaptor", "getAttachmentDocContent", false);
         return false;
       }
       String[] parts = url.split("/Attachments/", 2);
@@ -1452,27 +1416,27 @@
       parts = parts[1].split("/", 2);
       if (parts.length != 2) {
         log.fine("Could not separate attachment file name and list item id");
-        log.exiting("SiteDataClient", "getAttachmentDocContent", false);
+        log.exiting("SiteAdaptor", "getAttachmentDocContent", false);
         return false;
       }
       String itemId = parts[0];
       log.log(Level.FINE, "Detected possible attachment: "
           + "listUrl={0}, itemId={1}", new Object[] {listUrl, itemId});
       Holder<String> listIdHolder = new Holder<String>();
-      Holder<Boolean> result = new Holder<Boolean>();
-      siteData.getURLSegments(listUrl, result, null, null, listIdHolder, null);
-      if (!result.value) {
+      boolean result
+          = siteDataClient.getUrlSegments(listUrl, listIdHolder, null);
+      if (!result) {
         log.fine("Could not get list id from list url");
-        log.exiting("SiteDataClient", "getAttachmentDocContent", false);
+        log.exiting("SiteAdaptor", "getAttachmentDocContent", false);
         return false;
       }
       String listId = listIdHolder.value;
       if (listId == null) {
         log.fine("List URL does not point to a list");
-        log.exiting("SiteDataClient", "getAttachmentDocContent", false);
+        log.exiting("SiteAdaptor", "getAttachmentDocContent", false);
         return false;
       }
-      Item item = getContentListItemAttachments(listId, itemId);
+      Item item = siteDataClient.getContentListItemAttachments(listId, itemId);
       boolean verifiedIsAttachment = false;
       for (Item.Attachment attachment : item.getAttachment()) {
         if (url.equals(attachment.getURL())) {
@@ -1482,21 +1446,22 @@
       }
       if (!verifiedIsAttachment) {
         log.fine("Suspected attachment not listed in item's attachment list");
-        log.exiting("SiteDataClient", "getAttachmentDocContent", false);
+        log.exiting("SiteAdaptor", "getAttachmentDocContent", false);
         return false;
       }
       log.fine("Suspected attachment verified as being a real attachment. "
           + "Proceeding to provide content.");
-      com.microsoft.schemas.sharepoint.soap.List l = getContentList(listId);
-      Web w = getContentWeb();
+      com.microsoft.schemas.sharepoint.soap.List l
+          = siteDataClient.getContentList(listId);
+      Web w = siteDataClient.getContentWeb();
       if (TrueFalseType.TRUE.equals(l.getMetadata().getNoIndex())
           || isWebNoIndex(w)) {
         log.fine("Document marked for NoIndex");
         response.respondNotFound();
-        log.exiting("SiteDataClient", "getAttachmentDocContent", true);
+        log.exiting("SiteAdaptor", "getAttachmentDocContent", true);
         return true;
       }
-      ItemData itemData = getContentItem(listId, itemId);
+      ItemData itemData = siteDataClient.getContentItem(listId, itemId);
       Xml xml = itemData.getXml();
       Element data = getFirstChildWithName(xml, DATA_ELEMENT);
       Element row = getChildrenWithName(data, ROW_ELEMENT).get(0);
@@ -1510,8 +1475,8 @@
           && scopeId.equals(listScopeId)
           && isAllowAnonymousPeekForWeb(w);
       if (allowAnonymousAccess) {
-        allowAnonymousAccess 
-            = !isDenyAnonymousAcessOnVirtualServer(getContentVirtualServer());
+        allowAnonymousAccess = !isDenyAnonymousAcessOnVirtualServer(
+            siteDataClient.getContentVirtualServer());
       }
       if (!allowAnonymousAccess) {
         String listItemUrl = row.getAttribute(OWS_SERVERURL_ATTRIBUTE);
@@ -1520,14 +1485,14 @@
             .build());
       }
       getFileDocContent(request, response);
-      log.exiting("SiteDataClient", "getAttachmentDocContent", true);
+      log.exiting("SiteAdaptor", "getAttachmentDocContent", true);
       return true;
     }
 
     @VisibleForTesting
     void getModifiedDocIds(SPContentDatabase changes, DocIdPusher pusher)
         throws IOException, InterruptedException {
-      log.entering("SiteDataClient", "getModifiedDocIds",
+      log.entering("SiteAdaptor", "getModifiedDocIds",
           new Object[] {changes, pusher});
       List<DocId> docIds = new ArrayList<DocId>();
       getModifiedDocIdsContentDatabase(changes, docIds);
@@ -1540,12 +1505,12 @@
         records.add(builder.setDocId(docId).build());
       }
       pusher.pushRecords(records);
-      log.exiting("SiteDataClient", "getModifiedDocIds");
+      log.exiting("SiteAdaptor", "getModifiedDocIds");
     }
 
     private void getModifiedDocIdsContentDatabase(SPContentDatabase changes,
         List<DocId> docIds) {
-      log.entering("SiteDataClient", "getModifiedDocIdsContentDatabase",
+      log.entering("SiteAdaptor", "getModifiedDocIdsContentDatabase",
           new Object[] {changes, docIds});
       if (!"Unchanged".equals(changes.getChange())) {
         docIds.add(virtualServerDocId);
@@ -1553,11 +1518,11 @@
       for (SPSite site : changes.getSPSite()) {
         getModifiedDocIdsSite(site, docIds);
       }
-      log.exiting("SiteDataClient", "getModifiedDocIdsContentDatabase");
+      log.exiting("SiteAdaptor", "getModifiedDocIdsContentDatabase");
     }
 
     private void getModifiedDocIdsSite(SPSite changes, List<DocId> docIds) {
-      log.entering("SiteDataClient", "getModifiedDocIdsSite",
+      log.entering("SiteAdaptor", "getModifiedDocIdsSite",
           new Object[] {changes, docIds});
       if (isModified(changes.getChange())) {
         docIds.add(new DocId(changes.getSite().getMetadata().getURL()));
@@ -1565,11 +1530,11 @@
       for (SPWeb web : changes.getSPWeb()) {
         getModifiedDocIdsWeb(web, docIds);
       }
-      log.exiting("SiteDataClient", "getModifiedDocIdsSite");
+      log.exiting("SiteAdaptor", "getModifiedDocIdsSite");
     }
 
     private void getModifiedDocIdsWeb(SPWeb changes, List<DocId> docIds) {
-      log.entering("SiteDataClient", "getModifiedDocIdsWeb",
+      log.entering("SiteAdaptor", "getModifiedDocIdsWeb",
           new Object[] {changes, docIds});
       if (isModified(changes.getChange())) {
         docIds.add(new DocId(changes.getWeb().getMetadata().getURL()));
@@ -1585,20 +1550,20 @@
           getModifiedDocIdsFile((SPFile) choice, docIds);
         }
       }
-      log.exiting("SiteDataClient", "getModifiedDocIdsWeb");
+      log.exiting("SiteAdaptor", "getModifiedDocIdsWeb");
     }
 
     private void getModifiedDocIdsFolder(SPFolder changes, List<DocId> docIds) {
-      log.entering("SiteDataClient", "getModifiedDocIdsFolder",
+      log.entering("SiteAdaptor", "getModifiedDocIdsFolder",
           new Object[] {changes, docIds});
       if (isModified(changes.getChange())) {
         docIds.add(encodeDocId(changes.getDisplayUrl()));
       }
-      log.exiting("SiteDataClient", "getModifiedDocIdsFolder");
+      log.exiting("SiteAdaptor", "getModifiedDocIdsFolder");
     }
 
     private void getModifiedDocIdsList(SPList changes, List<DocId> docIds) {
-      log.entering("SiteDataClient", "getModifiedDocIdsList",
+      log.entering("SiteAdaptor", "getModifiedDocIdsList",
           new Object[] {changes, docIds});
       if (isModified(changes.getChange())) {
         docIds.add(encodeDocId(changes.getDisplayUrl()));
@@ -1610,12 +1575,12 @@
           getModifiedDocIdsListItem((SPListItem) choice, docIds);
         }
       }
-      log.exiting("SiteDataClient", "getModifiedDocIdsList");
+      log.exiting("SiteAdaptor", "getModifiedDocIdsList");
     }
 
     private void getModifiedDocIdsListItem(SPListItem changes,
         List<DocId> docIds) {
-      log.entering("SiteDataClient", "getModifiedDocIdsListItem",
+      log.entering("SiteAdaptor", "getModifiedDocIdsListItem",
           new Object[] {changes, docIds});
       if (isModified(changes.getChange())) {
         Object oData = changes.getListItem().getAny();
@@ -1633,16 +1598,16 @@
           }
         }
       }
-      log.exiting("SiteDataClient", "getModifiedDocIdsListItem");
+      log.exiting("SiteAdaptor", "getModifiedDocIdsListItem");
     }
 
     private void getModifiedDocIdsFile(SPFile changes, List<DocId> docIds) {
-      log.entering("SiteDataClient", "getModifiedDocIdsFile",
+      log.entering("SiteAdaptor", "getModifiedDocIdsFile",
           new Object[] {changes, docIds});
       if (isModified(changes.getChange())) {
         docIds.add(encodeDocId(changes.getDisplayUrl()));
       }
-      log.exiting("SiteDataClient", "getModifiedDocIdsFile");
+      log.exiting("SiteAdaptor", "getModifiedDocIdsFile");
     }
 
     private boolean isModified(String change) {
@@ -1650,8 +1615,8 @@
     }
 
     private MemberIdMapping retrieveMemberIdMapping() throws IOException {
-      log.entering("SiteDataClient", "retrieveMemberIdMapping");
-      Site site = getContentSite();
+      log.entering("SiteAdaptor", "retrieveMemberIdMapping");
+      Site site = siteDataClient.getContentSite();
       Map<Integer, String> groupMap = new HashMap<Integer, String>();
       for (GroupMembership.Group group : site.getGroups().getGroup()) {
         GroupDescription gd = group.getGroup();
@@ -1666,13 +1631,13 @@
         }
       }
       MemberIdMapping mapping = new MemberIdMapping(userMap, groupMap);
-      log.exiting("SiteDataClient", "retrieveMemberIdMapping", mapping);
+      log.exiting("SiteAdaptor", "retrieveMemberIdMapping", mapping);
       return mapping;
     }
 
     private MemberIdMapping retrieveSiteUserMapping()
         throws IOException {
-      log.entering("SiteDataClient", "retrieveSiteUserMapping");
+      log.entering("SiteAdaptor", "retrieveSiteUserMapping");
       GetUserCollectionFromSiteResponse.GetUserCollectionFromSiteResult result
           = userGroup.getUserCollectionFromSite();
       Map<Integer, String> userMap = new HashMap<Integer, String>();
@@ -1680,218 +1645,37 @@
       MemberIdMapping mapping;
       if (result == null) {
         mapping = new MemberIdMapping(userMap, groupMap);
-        log.exiting("SiteDataClient", "retrieveSiteUserMapping", mapping);
+        log.exiting("SiteAdaptor", "retrieveSiteUserMapping", mapping);
         return mapping;
       }
       GetUserCollectionFromSiteResult.GetUserCollectionFromSite siteUsers
            = result.getGetUserCollectionFromSite();
       if (siteUsers.getUsers() == null) {
         mapping = new MemberIdMapping(userMap, groupMap);
-        log.exiting("SiteDataClient", "retrieveSiteUserMapping", mapping);
+        log.exiting("SiteAdaptor", "retrieveSiteUserMapping", mapping);
         return mapping;
       }
       for (User user : siteUsers.getUsers().getUser()) {
         userMap.put((int) user.getID(), user.getLoginName());
       }
       mapping = new MemberIdMapping(userMap, groupMap);
-      log.exiting("SiteDataClient", "retrieveSiteUserMapping", mapping);
+      log.exiting("SiteAdaptor", "retrieveSiteUserMapping", mapping);
       return mapping;
     }
 
-    private VirtualServer getContentVirtualServer() throws IOException {
-      log.entering("SiteDataClient", "getContentVirtualServer");
-      Holder<String> result = new Holder<String>();
-      siteData.getContent(ObjectType.VIRTUAL_SERVER, null, null, null, true,
-          false, null, result);
-      String xml = result.value;
-      xml = xml.replace("<VirtualServer>",
-          "<VirtualServer xmlns='" + XMLNS + "'>");
-      VirtualServer vs = jaxbParse(xml, VirtualServer.class);
-      log.exiting("SiteDataClient", "getContentVirtualServer", vs);
-      return vs;
-    }
-
-    private SiteDataClient getClientForUrl(String url) throws IOException {
-      log.entering("SiteDataClient", "getClientForUrl", url);
-      Holder<Long> result = new Holder<Long>();
+    private SiteAdaptor getAdaptorForUrl(String url) throws IOException {
+      log.entering("SiteAdaptor", "getAdaptorForUrl", url);
       Holder<String> site = new Holder<String>();
       Holder<String> web = new Holder<String>();
-      siteData.getSiteAndWeb(url, result, site, web);
+      long result = siteDataClient.getSiteAndWeb(url, site, web);
 
-      if (result.value != 0) {
-        log.exiting("SiteDataClient", "getClientForUrl", null);
+      if (result != 0) {
+        log.exiting("SiteAdaptor", "getAdaptorForUrl", null);
         return null;
       }
-      SiteDataClient client = getSiteDataClient(site.value, web.value);
-      log.exiting("SiteDataClient", "getClientForUrl", client);
-      return client;
-    }
-
-    private ContentDatabase getContentContentDatabase(String id,
-        boolean retrieveChildItems) throws IOException {
-      log.entering("SiteDataClient", "getContentContentDatabase", id);
-      Holder<String> result = new Holder<String>();
-      siteData.getContent(ObjectType.CONTENT_DATABASE, id, null, null,
-          retrieveChildItems, false, null, result);
-      String xml = result.value;
-      xml = xml.replace("<ContentDatabase>",
-          "<ContentDatabase xmlns='" + XMLNS + "'>");
-      ContentDatabase cd = jaxbParse(xml, ContentDatabase.class);
-      log.exiting("SiteDataClient", "getContentContentDatabase", cd);
-      return cd;
-    }
-
-    private Site getContentSite() throws IOException {
-      log.entering("SiteDataClient", "getContentSite");
-      Holder<String> result = new Holder<String>();
-      final boolean retrieveChildItems = true;
-      // When ObjectType is SITE_COLLECTION, retrieveChildItems is the only
-      // input value consulted.
-      siteData.getContent(ObjectType.SITE_COLLECTION, null, null, null,
-          retrieveChildItems, false, null, result);
-      String xml = result.value;
-      xml = xml.replace("<Site>", "<Site xmlns='" + XMLNS + "'>");
-      Site site = jaxbParse(xml, Site.class);
-      log.exiting("SiteDataClient", "getContentSite", site);
-      return site;
-    }
-
-    private Web getContentWeb() throws IOException {
-      log.entering("SiteDataClient", "getContentWeb");
-      Holder<String> result = new Holder<String>();
-      siteData.getContent(ObjectType.SITE, null, null, null, true, false, null,
-          result);
-      String xml = result.value;
-      xml = xml.replace("<Web>", "<Web xmlns='" + XMLNS + "'>");
-      Web web = jaxbParse(xml, Web.class);
-      log.exiting("SiteDataClient", "getContentWeb", web);
-      return web;
-    }
-
-    private com.microsoft.schemas.sharepoint.soap.List getContentList(String id)
-        throws IOException {
-      log.entering("SiteDataClient", "getContentList", id);
-      Holder<String> result = new Holder<String>();
-      siteData.getContent(ObjectType.LIST, id, null, null, false, false, null,
-          result);
-      String xml = result.value;
-      xml = xml.replace("<List>", "<List xmlns='" + XMLNS + "'>");
-      com.microsoft.schemas.sharepoint.soap.List list = jaxbParse(xml,
-          com.microsoft.schemas.sharepoint.soap.List.class);
-      log.exiting("SiteDataClient", "getContentList", list);
-      return list;
-    }
-
-    private ItemData getContentItem(String listId, String itemId)
-        throws IOException {
-      log.entering("SiteDataClient", "getContentItem",
-          new Object[] {listId, itemId});
-      Holder<String> result = new Holder<String>();
-      siteData.getContent(ObjectType.LIST_ITEM, listId, "", itemId, false,
-          false, null, result);
-      String xml = result.value;
-      xml = xml.replace("<Item>", "<ItemData xmlns='" + XMLNS + "'>");
-      xml = xml.replace("</Item>", "</ItemData>");
-      ItemData data = jaxbParse(xml, ItemData.class);
-      log.exiting("SiteDataClient", "getContentItem", data);
-      return data;
-    }
-
-    private Paginator<ItemData> getContentFolderChildren(final String guid,
-        final String url) {
-      log.entering("SiteDataClient", "getContentFolderChildren",
-          new Object[] {guid, url});
-      final Holder<String> lastItemIdOnPage = new Holder<String>("");
-      log.exiting("SiteDataClient", "getContentFolderChildren");
-      return new Paginator<ItemData>() {
-        @Override
-        public ItemData next() throws IOException {
-          if (lastItemIdOnPage.value == null) {
-            return null;
-          }
-          Holder<String> result = new Holder<String>();
-          siteData.getContent(ObjectType.FOLDER, guid, url, null, true, false,
-              lastItemIdOnPage, result);
-          String xml = result.value;
-          xml = xml.replace("<Folder>", "<Folder xmlns='" + XMLNS + "'>");
-          return jaxbParse(xml, ItemData.class);
-        }
-      };
-    }
-
-    private Item getContentListItemAttachments(String listId, String itemId)
-        throws IOException {
-      log.entering("SiteDataClient", "getContentListItemAttachments",
-          new Object[] {listId, itemId});
-      Holder<String> result = new Holder<String>();
-      siteData.getContent(ObjectType.LIST_ITEM_ATTACHMENTS, listId, "",
-          itemId, true, false, null, result);
-      String xml = result.value;
-      xml = xml.replace("<Item ", "<Item xmlns='" + XMLNS + "' ");
-      Item item = jaxbParse(xml, Item.class);
-      log.exiting("SiteDataClient", "getContentListItemAttachments", item);
-      return item;
-    }
-
-    /**
-     * Get a paginator that allows looping over all the changes since {@code
-     * startChangeId}. If next() throws an XmlProcessingException, it is
-     * guaranteed to be after state has been updated so that a subsequent call
-     * to next() will provide the next page and not repeat the erroring page.
-     */
-    private CursorPaginator<SPContentDatabase, String>
-        getChangesContentDatabase(final String contentDatabaseGuid,
-            String startChangeId, final boolean isSp2010) {
-      log.entering("SiteDataClient", "getChangesContentDatabase",
-          new Object[] {contentDatabaseGuid, startChangeId});
-      final Holder<String> lastChangeId = new Holder<String>(startChangeId);
-      final Holder<String> lastLastChangeId = new Holder<String>();
-      final Holder<String> currentChangeId = new Holder<String>();
-      final Holder<Boolean> moreChanges = new Holder<Boolean>(true);
-      log.exiting("SiteDataClient", "getChangesContentDatabase");
-      return new CursorPaginator<SPContentDatabase, String>() {
-        @Override
-        public SPContentDatabase next() throws IOException {
-          if (!moreChanges.value) {
-            return null;
-          }
-          lastLastChangeId.value = lastChangeId.value;
-          Holder<String> result = new Holder<String>();
-          // In non-SP2010, the timeout is a number of seconds. In SP2010, the
-          // timeout is n * 60, where n is the number of items you want
-          // returned. However, in SP2010, asking for more than 10 items seems
-          // to lose results. If timeout is less than 60 in SP 2010, then it
-          // causes an infinite loop.
-          int timeout = isSp2010 ? 10 * 60 : 15;
-          siteData.getChanges(ObjectType.CONTENT_DATABASE, contentDatabaseGuid,
-              lastChangeId, currentChangeId, timeout, result, moreChanges);
-          // XmlProcessingExceptions fine after this point.
-          String xml = result.value;
-          xml = xml.replace("<SPContentDatabase ",
-              "<SPContentDatabase xmlns='" + XMLNS + "' ");
-          return jaxbParse(xml, SPContentDatabase.class);
-        }
-
-        @Override
-        public String getCursor() {
-          return lastChangeId.value;
-        }
-      };
-    }
-
-    @VisibleForTesting
-    <T> T jaxbParse(String xml, Class<T> klass)
-        throws XmlProcessingException {
-      Source source = new StreamSource(new StringReader(xml));
-      try {
-        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
-        if (xmlValidation) {
-          unmarshaller.setSchema(schema);
-        }
-        return unmarshaller.unmarshal(source, klass).getValue();
-      } catch (JAXBException ex) {
-        throw new XmlProcessingException(ex, xml);
-      }
+      SiteAdaptor siteAdaptor = getSiteAdaptor(site.value, web.value);
+      log.exiting("SiteAdaptor", "getAdaptorForUrl", siteAdaptor);
+      return siteAdaptor;
     }
 
     private HtmlResponseWriter createHtmlResponseWriter(Response response)
@@ -1902,59 +1686,12 @@
       return new HtmlResponseWriter(writer, context.getDocIdEncoder(),
           Locale.ENGLISH);
     }
-  }
 
-  /**
-   * Container exception for wrapping xml processing exceptions in IOExceptions.
-   */
-  private static class XmlProcessingException extends IOException {
-    public XmlProcessingException(JAXBException cause, String xml) {
-      super("Error when parsing xml: " + xml, cause);
+    public SiteDataClient getSiteDataClient() {
+      return siteDataClient;
     }
   }
 
-  /**
-   * Container exception for wrapping WebServiceExceptions in a checked
-   * exception.
-   */
-  private static class WebServiceIOException extends IOException {
-    public WebServiceIOException(WebServiceException cause) {
-      super(cause);
-    }
-  }
-
-  /**
-   * An object that can be paged through.
-   *
-   * @param <E> element type returned by {@link #next}
-   */
-  private interface Paginator<E> {
-    /**
-     * Get the next page of the series. If an exception is thrown, the state of
-     * the paginator is undefined.
-     *
-     * @return the next page of data, or {@code null} if no more pages available
-     */
-    public E next() throws IOException;
-  }
-
-  /**
-   * An object that can be paged through, but also provide a cursor for learning
-   * its current position.
-   *
-   * @param <E> element type returned by {@link #next}
-   * @param <C> cursor type
-   */
-  private interface CursorPaginator<E, C> extends Paginator<E> {
-    /**
-     * Provides a cursor for the current position. The intent is that you could
-     * get a cursor (even in the event of {@link #next} throwing an exception)
-     * and use it to create a query that would continue without repeating
-     * results.
-     */
-    public C getCursor();
-  }
-
   @VisibleForTesting
   static class FileInfo {
     /** Non-null contents. */
@@ -2081,32 +1818,6 @@
   }
 
   @VisibleForTesting
-  interface SiteDataFactory {
-    /**
-     * The {@code endpoint} string is a SharePoint URL, meaning that spaces are
-     * not encoded.
-     */
-    public SiteDataSoap newSiteData(String endpoint) throws IOException;
-  }
-
-  static class SiteDataFactoryImpl implements SiteDataFactory {
-    private final Service siteDataService;
-
-    public SiteDataFactoryImpl() {
-      URL url = SiteDataSoap.class.getResource("SiteData.wsdl");
-      QName qname = new QName(XMLNS, "SiteData");
-      this.siteDataService = Service.create(url, qname);
-    }
-
-    @Override
-    public SiteDataSoap newSiteData(String endpoint) throws IOException {
-      EndpointReference endpointRef = new W3CEndpointReferenceBuilder()
-          .address(spUrlToUri(endpoint).toString()).build();
-      return siteDataService.getPort(endpointRef, SiteDataSoap.class);
-    }
-  }
-
-  @VisibleForTesting
   interface UserGroupFactory {
     public UserGroupSoap newUserGroup(String endpoint);
   }
@@ -2163,91 +1874,6 @@
     }
   }
 
-  /**
-   * A subset of SiteDataSoap that throws WebServiceIOExceptions instead of the
-   * WebServiceException (which is a RuntimeException).
-   */
-  private static interface CheckedExceptionSiteDataSoap {
-    public void getSiteAndWeb(String strUrl, Holder<Long> getSiteAndWebResult,
-        Holder<String> strSite, Holder<String> strWeb)
-        throws WebServiceIOException;
-
-    public void getURLSegments(String strURL,
-        Holder<Boolean> getURLSegmentsResult, Holder<String> strWebID,
-        Holder<String> strBucketID, Holder<String> strListID,
-        Holder<String> strItemID) throws WebServiceIOException;
-
-    public void getContent(ObjectType objectType, String objectId,
-        String folderUrl, String itemId, boolean retrieveChildItems,
-        boolean securityOnly, Holder<String> lastItemIdOnPage,
-        Holder<String> getContentResult) throws WebServiceIOException;
-
-    public void getChanges(ObjectType objectType, String contentDatabaseId,
-        Holder<String> lastChangeId, Holder<String> currentChangeId,
-        Integer timeout, Holder<String> getChangesResult,
-        Holder<Boolean> moreChanges) throws WebServiceIOException;
-  }
-
-  private static class CheckedExceptionSiteDataSoapAdapter
-      implements CheckedExceptionSiteDataSoap {
-    private final SiteDataSoap siteData;
-
-    public CheckedExceptionSiteDataSoapAdapter(SiteDataSoap siteData) {
-      this.siteData = siteData;
-    }
-
-    @Override
-    public void getSiteAndWeb(String strUrl, Holder<Long> getSiteAndWebResult,
-        Holder<String> strSite, Holder<String> strWeb)
-        throws WebServiceIOException {
-      try {
-        siteData.getSiteAndWeb(strUrl, getSiteAndWebResult, strSite, strWeb);
-      } catch (WebServiceException ex) {
-        throw new WebServiceIOException(ex);
-      }
-    }
-
-    @Override
-    public void getURLSegments(String strURL,
-        Holder<Boolean> getURLSegmentsResult, Holder<String> strWebID,
-        Holder<String> strBucketID, Holder<String> strListID,
-        Holder<String> strItemID) throws WebServiceIOException {
-      try {
-        siteData.getURLSegments(strURL, getURLSegmentsResult, strWebID,
-            strBucketID, strListID, strItemID);
-      } catch (WebServiceException ex) {
-        throw new WebServiceIOException(ex);
-      }
-    }
-
-    @Override
-    public void getContent(ObjectType objectType, String objectId,
-        String folderUrl, String itemId, boolean retrieveChildItems,
-        boolean securityOnly, Holder<String> lastItemIdOnPage,
-        Holder<String> getContentResult) throws WebServiceIOException {
-      try {
-        siteData.getContent(objectType, objectId, folderUrl, itemId,
-            retrieveChildItems, securityOnly, lastItemIdOnPage,
-            getContentResult);
-      } catch (WebServiceException ex) {
-        throw new WebServiceIOException(ex);
-      }
-    }
-
-    @Override
-    public void getChanges(ObjectType objectType, String contentDatabaseId,
-        Holder<String> lastChangeId, Holder<String> currentChangeId,
-        Integer timeout, Holder<String> getChangesResult,
-        Holder<Boolean> moreChanges) throws WebServiceIOException {
-      try {
-        siteData.getChanges(objectType, contentDatabaseId, lastChangeId,
-            currentChangeId, timeout, getChangesResult, moreChanges);
-      } catch (WebServiceException ex) {
-        throw new WebServiceIOException(ex);
-      }
-    }
-  }
-
   private class MemberIdMappingCallable implements Callable<MemberIdMapping> {
     private final String siteUrl;
 
@@ -2312,7 +1938,7 @@
 
     @Override
     public MemberIdMapping load(String site) throws IOException {
-      return getSiteDataClient(site, site).retrieveMemberIdMapping();
+      return getSiteAdaptor(site, site).retrieveMemberIdMapping();
     }
   }
 
@@ -2325,7 +1951,7 @@
 
     @Override
     public MemberIdMapping load(String site) throws IOException {
-      return getSiteDataClient(site, site).retrieveSiteUserMapping();
+      return getSiteAdaptor(site, site).retrieveSiteUserMapping();
     }
   }
 
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SiteDataClient.java b/src/com/google/enterprise/adaptor/sharepoint/SiteDataClient.java
new file mode 100644
index 0000000..a5bd810
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/sharepoint/SiteDataClient.java
@@ -0,0 +1,471 @@
+// 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.common.annotations.VisibleForTesting;
+
+import com.microsoft.schemas.sharepoint.soap.ContentDatabase;
+import com.microsoft.schemas.sharepoint.soap.Item;
+import com.microsoft.schemas.sharepoint.soap.ItemData;
+import com.microsoft.schemas.sharepoint.soap.ObjectType;
+import com.microsoft.schemas.sharepoint.soap.SPContentDatabase;
+import com.microsoft.schemas.sharepoint.soap.Site;
+import com.microsoft.schemas.sharepoint.soap.SiteDataSoap;
+import com.microsoft.schemas.sharepoint.soap.VirtualServer;
+import com.microsoft.schemas.sharepoint.soap.Web;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URL;
+import java.util.logging.*;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.ws.EndpointReference;
+import javax.xml.ws.Holder;
+import javax.xml.ws.Service;
+import javax.xml.ws.WebServiceException;
+import javax.xml.ws.wsaddressing.W3CEndpointReferenceBuilder;
+
+class SiteDataClient {
+  /** SharePoint's namespace. */
+  private static final String XMLNS
+      = "http://schemas.microsoft.com/sharepoint/soap/";
+
+  private static final Logger log
+      = Logger.getLogger(SiteDataClient.class.getName());
+  /**
+   * The JAXBContext is expensive to initialize, so we share a copy between
+   * instances.
+   */
+  private static final JAXBContext jaxbContext;
+  /**
+   * XML Schema of requests and responses. Used to validate responses match
+   * expectations.
+   */
+  private static final Schema schema;
+
+  static {
+    try {
+      jaxbContext = JAXBContext.newInstance(
+          "com.microsoft.schemas.sharepoint.soap");
+    } catch (JAXBException ex) {
+      throw new RuntimeException("Could not initialize JAXBContext", ex);
+    }
+
+    try {
+      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+      dbf.setNamespaceAware(true);
+      Document doc = dbf.newDocumentBuilder()
+          .parse(SiteDataSoap.class.getResourceAsStream("SiteData.wsdl"));
+      String schemaNs = XMLConstants.W3C_XML_SCHEMA_NS_URI;
+      Node schemaNode = doc.getElementsByTagNameNS(schemaNs, "schema").item(0);
+      schema = SchemaFactory.newInstance(schemaNs).newSchema(
+          new DOMSource(schemaNode));
+    } catch (IOException ex) {
+      throw new RuntimeException("Could not initialize Schema", ex);
+    } catch (SAXException ex) {
+      throw new RuntimeException("Could not initialize Schema", ex);
+    } catch (ParserConfigurationException ex) {
+      throw new RuntimeException("Could not initialize Schema", ex);
+    }
+  }
+
+  private final CheckedExceptionSiteDataSoap siteData;
+  private final boolean xmlValidation;
+
+  public SiteDataClient(SiteDataSoap siteDataSoap, boolean xmlValidation) {
+    if (siteDataSoap == null) {
+      throw new NullPointerException();
+    }
+    siteDataSoap = LoggingWSHandler.create(SiteDataSoap.class, siteDataSoap);
+    this.siteData = new CheckedExceptionSiteDataSoapAdapter(siteDataSoap);
+    this.xmlValidation = xmlValidation;
+  }
+
+  public long getSiteAndWeb(String strUrl, Holder<String> strSite,
+      Holder<String> strWeb) throws IOException {
+    Holder<Long> getSiteAndWebResult = new Holder<Long>();
+    siteData.getSiteAndWeb(strUrl, getSiteAndWebResult, strSite, strWeb);
+    return getSiteAndWebResult.value;
+  }
+
+  public boolean getUrlSegments(String strURL, Holder<String> strListID,
+      Holder<String> strItemID) throws IOException {
+    Holder<Boolean> getURLSegmentsResult = new Holder<Boolean>();
+    // The returned web is not useful because we already know the web containing
+    // the URL. We don't have a use for the returned bucket.
+    siteData.getURLSegments(strURL, getURLSegmentsResult, null, null,
+        strListID, strItemID);
+    return getURLSegmentsResult.value;
+  }
+
+  public VirtualServer getContentVirtualServer() throws IOException {
+    log.entering("SiteDataClient", "getContentVirtualServer");
+    Holder<String> result = new Holder<String>();
+    siteData.getContent(ObjectType.VIRTUAL_SERVER, null, null, null, true,
+        false, null, result);
+    String xml = result.value;
+    xml = xml.replace("<VirtualServer>",
+        "<VirtualServer xmlns='" + XMLNS + "'>");
+    VirtualServer vs = jaxbParse(xml, VirtualServer.class);
+    log.exiting("SiteDataClient", "getContentVirtualServer", vs);
+    return vs;
+  }
+
+  public ContentDatabase getContentContentDatabase(String id,
+      boolean retrieveChildItems) throws IOException {
+    log.entering("SiteDataClient", "getContentContentDatabase", id);
+    Holder<String> result = new Holder<String>();
+    siteData.getContent(ObjectType.CONTENT_DATABASE, id, null, null,
+        retrieveChildItems, false, null, result);
+    String xml = result.value;
+    xml = xml.replace("<ContentDatabase>",
+        "<ContentDatabase xmlns='" + XMLNS + "'>");
+    ContentDatabase cd = jaxbParse(xml, ContentDatabase.class);
+    log.exiting("SiteDataClient", "getContentContentDatabase", cd);
+    return cd;
+  }
+
+  public Site getContentSite() throws IOException {
+    log.entering("SiteDataClient", "getContentSite");
+    Holder<String> result = new Holder<String>();
+    final boolean retrieveChildItems = true;
+    // When ObjectType is SITE_COLLECTION, retrieveChildItems is the only
+    // input value consulted.
+    siteData.getContent(ObjectType.SITE_COLLECTION, null, null, null,
+        retrieveChildItems, false, null, result);
+    String xml = result.value;
+    xml = xml.replace("<Site>", "<Site xmlns='" + XMLNS + "'>");
+    Site site = jaxbParse(xml, Site.class);
+    log.exiting("SiteDataClient", "getContentSite", site);
+    return site;
+  }
+
+  public Web getContentWeb() throws IOException {
+    log.entering("SiteDataClient", "getContentWeb");
+    Holder<String> result = new Holder<String>();
+    siteData.getContent(ObjectType.SITE, null, null, null, true, false, null,
+        result);
+    String xml = result.value;
+    xml = xml.replace("<Web>", "<Web xmlns='" + XMLNS + "'>");
+    Web web = jaxbParse(xml, Web.class);
+    log.exiting("SiteDataClient", "getContentWeb", web);
+    return web;
+  }
+
+  public com.microsoft.schemas.sharepoint.soap.List getContentList(String id)
+      throws IOException {
+    log.entering("SiteDataClient", "getContentList", id);
+    Holder<String> result = new Holder<String>();
+    siteData.getContent(ObjectType.LIST, id, null, null, false, false, null,
+        result);
+    String xml = result.value;
+    xml = xml.replace("<List>", "<List xmlns='" + XMLNS + "'>");
+    com.microsoft.schemas.sharepoint.soap.List list = jaxbParse(xml,
+        com.microsoft.schemas.sharepoint.soap.List.class);
+    log.exiting("SiteDataClient", "getContentList", list);
+    return list;
+  }
+
+  public ItemData getContentItem(String listId, String itemId)
+      throws IOException {
+    log.entering("SiteDataClient", "getContentItem",
+        new Object[] {listId, itemId});
+    Holder<String> result = new Holder<String>();
+    siteData.getContent(ObjectType.LIST_ITEM, listId, "", itemId, false,
+        false, null, result);
+    String xml = result.value;
+    xml = xml.replace("<Item>", "<ItemData xmlns='" + XMLNS + "'>");
+    xml = xml.replace("</Item>", "</ItemData>");
+    ItemData data = jaxbParse(xml, ItemData.class);
+    log.exiting("SiteDataClient", "getContentItem", data);
+    return data;
+  }
+
+  public Paginator<ItemData> getContentFolderChildren(final String guid,
+      final String url) {
+    log.entering("SiteDataClient", "getContentFolderChildren",
+        new Object[] {guid, url});
+    final Holder<String> lastItemIdOnPage = new Holder<String>("");
+    log.exiting("SiteDataClient", "getContentFolderChildren");
+    return new Paginator<ItemData>() {
+      @Override
+      public ItemData next() throws IOException {
+        if (lastItemIdOnPage.value == null) {
+          return null;
+        }
+        Holder<String> result = new Holder<String>();
+        siteData.getContent(ObjectType.FOLDER, guid, url, null, true, false,
+            lastItemIdOnPage, result);
+        String xml = result.value;
+        xml = xml.replace("<Folder>", "<Folder xmlns='" + XMLNS + "'>");
+        return jaxbParse(xml, ItemData.class);
+      }
+    };
+  }
+
+  public Item getContentListItemAttachments(String listId, String itemId)
+      throws IOException {
+    log.entering("SiteDataClient", "getContentListItemAttachments",
+        new Object[] {listId, itemId});
+    Holder<String> result = new Holder<String>();
+    siteData.getContent(ObjectType.LIST_ITEM_ATTACHMENTS, listId, "",
+        itemId, true, false, null, result);
+    String xml = result.value;
+    xml = xml.replace("<Item ", "<Item xmlns='" + XMLNS + "' ");
+    Item item = jaxbParse(xml, Item.class);
+    log.exiting("SiteDataClient", "getContentListItemAttachments", item);
+    return item;
+  }
+
+  /**
+   * Get a paginator that allows looping over all the changes since {@code
+   * startChangeId}. If next() throws an XmlProcessingException, it is
+   * guaranteed to be after state has been updated so that a subsequent call
+   * to next() will provide the next page and not repeat the erroring page.
+   */
+  public CursorPaginator<SPContentDatabase, String>
+      getChangesContentDatabase(final String contentDatabaseGuid,
+          String startChangeId, final boolean isSp2010) {
+    log.entering("SiteDataClient", "getChangesContentDatabase",
+        new Object[] {contentDatabaseGuid, startChangeId});
+    final Holder<String> lastChangeId = new Holder<String>(startChangeId);
+    final Holder<String> lastLastChangeId = new Holder<String>();
+    final Holder<String> currentChangeId = new Holder<String>();
+    final Holder<Boolean> moreChanges = new Holder<Boolean>(true);
+    log.exiting("SiteDataClient", "getChangesContentDatabase");
+    return new CursorPaginator<SPContentDatabase, String>() {
+      @Override
+      public SPContentDatabase next() throws IOException {
+        if (!moreChanges.value) {
+          return null;
+        }
+        lastLastChangeId.value = lastChangeId.value;
+        Holder<String> result = new Holder<String>();
+        // In non-SP2010, the timeout is a number of seconds. In SP2010, the
+        // timeout is n * 60, where n is the number of items you want
+        // returned. However, in SP2010, asking for more than 10 items seems
+        // to lose results. If timeout is less than 60 in SP 2010, then it
+        // causes an infinite loop.
+        int timeout = isSp2010 ? 10 * 60 : 15;
+        siteData.getChanges(ObjectType.CONTENT_DATABASE, contentDatabaseGuid,
+            lastChangeId, currentChangeId, timeout, result, moreChanges);
+        // XmlProcessingExceptions fine after this point.
+        String xml = result.value;
+        xml = xml.replace("<SPContentDatabase ",
+            "<SPContentDatabase xmlns='" + XMLNS + "' ");
+        return jaxbParse(xml, SPContentDatabase.class);
+      }
+
+      @Override
+      public String getCursor() {
+        return lastChangeId.value;
+      }
+    };
+  }
+
+  @VisibleForTesting
+  <T> T jaxbParse(String xml, Class<T> klass)
+      throws XmlProcessingException {
+    Source source = new StreamSource(new StringReader(xml));
+    try {
+      Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
+      if (xmlValidation) {
+        unmarshaller.setSchema(schema);
+      }
+      return unmarshaller.unmarshal(source, klass).getValue();
+    } catch (JAXBException ex) {
+      throw new XmlProcessingException(ex, xml);
+    }
+  }
+
+  /**
+   * Container exception for wrapping xml processing exceptions in IOExceptions.
+   */
+  public static class XmlProcessingException extends IOException {
+    public XmlProcessingException(JAXBException cause, String xml) {
+      super("Error when parsing xml: " + xml, cause);
+    }
+  }
+
+  /**
+   * Container exception for wrapping WebServiceExceptions in a checked
+   * exception.
+   */
+  public static class WebServiceIOException extends IOException {
+    public WebServiceIOException(WebServiceException cause) {
+      super(cause);
+    }
+  }
+
+  /**
+   * An object that can be paged through.
+   *
+   * @param <E> element type returned by {@link #next}
+   */
+  public interface Paginator<E> {
+    /**
+     * Get the next page of the series. If an exception is thrown, the state of
+     * the paginator is undefined.
+     *
+     * @return the next page of data, or {@code null} if no more pages available
+     */
+    public E next() throws IOException;
+  }
+
+  /**
+   * An object that can be paged through, but also provide a cursor for learning
+   * its current position.
+   *
+   * @param <E> element type returned by {@link #next}
+   * @param <C> cursor type
+   */
+  public interface CursorPaginator<E, C> extends Paginator<E> {
+    /**
+     * Provides a cursor for the current position. The intent is that you could
+     * get a cursor (even in the event of {@link #next} throwing an exception)
+     * and use it to create a query that would continue without repeating
+     * results.
+     */
+    public C getCursor();
+  }
+
+  @VisibleForTesting
+  interface SiteDataFactory {
+    /**
+     * The {@code endpoint} string is a SharePoint URL, meaning that spaces are
+     * not encoded.
+     */
+    public SiteDataSoap newSiteData(String endpoint) throws IOException;
+  }
+
+  static class SiteDataFactoryImpl implements SiteDataFactory {
+    private final Service siteDataService;
+
+    public SiteDataFactoryImpl() {
+      URL url = SiteDataSoap.class.getResource("SiteData.wsdl");
+      QName qname = new QName(XMLNS, "SiteData");
+      this.siteDataService = Service.create(url, qname);
+    }
+
+    @Override
+    public SiteDataSoap newSiteData(String endpoint) throws IOException {
+      EndpointReference endpointRef = new W3CEndpointReferenceBuilder()
+          .address(SharePointAdaptor.spUrlToUri(endpoint).toString()).build();
+      return siteDataService.getPort(endpointRef, SiteDataSoap.class);
+    }
+  }
+
+  /**
+   * A subset of SiteDataSoap that throws WebServiceIOExceptions instead of the
+   * WebServiceException (which is a RuntimeException).
+   */
+  private static interface CheckedExceptionSiteDataSoap {
+    public void getSiteAndWeb(String strUrl, Holder<Long> getSiteAndWebResult,
+        Holder<String> strSite, Holder<String> strWeb)
+        throws WebServiceIOException;
+
+    public void getURLSegments(String strURL,
+        Holder<Boolean> getURLSegmentsResult, Holder<String> strWebID,
+        Holder<String> strBucketID, Holder<String> strListID,
+        Holder<String> strItemID) throws WebServiceIOException;
+
+    public void getContent(ObjectType objectType, String objectId,
+        String folderUrl, String itemId, boolean retrieveChildItems,
+        boolean securityOnly, Holder<String> lastItemIdOnPage,
+        Holder<String> getContentResult) throws WebServiceIOException;
+
+    public void getChanges(ObjectType objectType, String contentDatabaseId,
+        Holder<String> lastChangeId, Holder<String> currentChangeId,
+        Integer timeout, Holder<String> getChangesResult,
+        Holder<Boolean> moreChanges) throws WebServiceIOException;
+  }
+
+  private static class CheckedExceptionSiteDataSoapAdapter
+      implements CheckedExceptionSiteDataSoap {
+    private final SiteDataSoap siteData;
+
+    public CheckedExceptionSiteDataSoapAdapter(SiteDataSoap siteData) {
+      this.siteData = siteData;
+    }
+
+    @Override
+    public void getSiteAndWeb(String strUrl, Holder<Long> getSiteAndWebResult,
+        Holder<String> strSite, Holder<String> strWeb)
+        throws WebServiceIOException {
+      try {
+        siteData.getSiteAndWeb(strUrl, getSiteAndWebResult, strSite, strWeb);
+      } catch (WebServiceException ex) {
+        throw new WebServiceIOException(ex);
+      }
+    }
+
+    @Override
+    public void getURLSegments(String strURL,
+        Holder<Boolean> getURLSegmentsResult, Holder<String> strWebID,
+        Holder<String> strBucketID, Holder<String> strListID,
+        Holder<String> strItemID) throws WebServiceIOException {
+      try {
+        siteData.getURLSegments(strURL, getURLSegmentsResult, strWebID,
+            strBucketID, strListID, strItemID);
+      } catch (WebServiceException ex) {
+        throw new WebServiceIOException(ex);
+      }
+    }
+
+    @Override
+    public void getContent(ObjectType objectType, String objectId,
+        String folderUrl, String itemId, boolean retrieveChildItems,
+        boolean securityOnly, Holder<String> lastItemIdOnPage,
+        Holder<String> getContentResult) throws WebServiceIOException {
+      try {
+        siteData.getContent(objectType, objectId, folderUrl, itemId,
+            retrieveChildItems, securityOnly, lastItemIdOnPage,
+            getContentResult);
+      } catch (WebServiceException ex) {
+        throw new WebServiceIOException(ex);
+      }
+    }
+
+    @Override
+    public void getChanges(ObjectType objectType, String contentDatabaseId,
+        Holder<String> lastChangeId, Holder<String> currentChangeId,
+        Integer timeout, Holder<String> getChangesResult,
+        Holder<Boolean> moreChanges) throws WebServiceIOException {
+      try {
+        siteData.getChanges(objectType, contentDatabaseId, lastChangeId,
+            currentChangeId, timeout, getChangesResult, moreChanges);
+      } catch (WebServiceException ex) {
+        throw new WebServiceIOException(ex);
+      }
+    }
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
index 78bbb20..74b2b16 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.enterprise.adaptor.sharepoint.SharePointAdaptor.FileInfo;
 import static com.google.enterprise.adaptor.sharepoint.SharePointAdaptor.HttpClient;
-import static com.google.enterprise.adaptor.sharepoint.SharePointAdaptor.SiteDataFactory;
+import static com.google.enterprise.adaptor.sharepoint.SiteDataClient.SiteDataFactory;
 import static org.junit.Assert.*;
 
 import com.google.common.base.Objects;
@@ -255,8 +255,8 @@
 
   @Test
   public void testSiteDataFactoryImpl() throws IOException {
-    SharePointAdaptor.SiteDataFactoryImpl sdfi
-        = new SharePointAdaptor.SiteDataFactoryImpl();
+    SiteDataClient.SiteDataFactoryImpl sdfi
+        = new SiteDataClient.SiteDataFactoryImpl();
     assertNotNull(
         sdfi.newSiteData("http://localhost:1/_vti_bin/SiteData.asmx"));
     // Test a site with a space.
@@ -522,7 +522,7 @@
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
           + "AllItems.aspx"));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
           "http://localhost:1/sites/SiteCollection", siteData,
           new UnsupportedUserGroupSoap(), Callables.returning(memberIdMapping),
           new UnsupportedCallable<MemberIdMapping>())
@@ -566,7 +566,7 @@
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
           + "AllItems.aspx"));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
           "http://localhost:1/sites/SiteCollection", siteData,
           new UnsupportedUserGroupSoap(),
           new UnsupportedCallable<MemberIdMapping>(),
@@ -609,7 +609,7 @@
     GetContentsRequest request = new GetContentsRequest(
         new DocId(attachmentId));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
           "http://localhost:1/sites/SiteCollection", siteData,
           new UnsupportedUserGroupSoap(),
           new UnsupportedCallable<MemberIdMapping>(),
@@ -660,7 +660,7 @@
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
           + "Test Folder/2_.000"));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
           "http://localhost:1/sites/SiteCollection", siteData,
           new UnsupportedUserGroupSoap(), Callables.returning(memberIdMapping),
           new UnsupportedCallable<MemberIdMapping>())
@@ -775,7 +775,7 @@
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
           + "Test Folder/2_.000"));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
           "http://localhost:1/sites/SiteCollection", siteData,
           new UnsupportedUserGroupSoap(), Callables.returning(memberIdMapping),
           new UnsupportedCallable<MemberIdMapping>())
@@ -833,7 +833,7 @@
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
             + "Test Folder/2_.000"));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
           "http://localhost:1/sites/SiteCollection", siteData,
           mockUserGroupFactory.newUserGroup(
               "http://localhost:1/sites/SiteCollection"),
@@ -900,7 +900,7 @@
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
           + "Test Folder/2_.000"));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
         "http://localhost:1/sites/SiteCollection",
         siteData, new UnsupportedUserGroupSoap(),
         Callables.returning(memberIdMapping),
@@ -944,7 +944,7 @@
         new DocId("http://localhost:1/sites/SiteCollection/Lists/Custom List/"
           + "Test Folder"));
     GetContentsResponse response = new GetContentsResponse(baos);
-    adaptor.new SiteDataClient("http://localhost:1/sites/SiteCollection",
+    adaptor.new SiteAdaptor("http://localhost:1/sites/SiteCollection",
           "http://localhost:1/sites/SiteCollection",
           siteData, new UnsupportedUserGroupSoap(),
         Callables.returning(memberIdMapping),
@@ -1221,17 +1221,14 @@
         executorFactory);
     AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
     adaptor.init(new MockAdaptorContext(config, pusher));
-    SharePointAdaptor.SiteDataClient client = adaptor.new SiteDataClient(
+    SPContentDatabase result = parseChanges(getChangesContentDatabase);
+    adaptor.new SiteAdaptor(
         "http://localhost:1/sites/SiteCollection",
         "http://localhost:1/sites/SiteCollection", new UnsupportedSiteData(),
         new UnsupportedUserGroupSoap(),
         new UnsupportedCallable<MemberIdMapping>(),
-        new UnsupportedCallable<MemberIdMapping>());
-
-    SPContentDatabase result
-        = parseChanges(client, getChangesContentDatabase);
-
-    client.getModifiedDocIds(result, pusher);
+        new UnsupportedCallable<MemberIdMapping>())
+        .getModifiedDocIds(result, pusher);
     assertEquals(1, pusher.getRecords().size());
     assertEquals(new DocIdPusher.Record.Builder(new DocId(
           "http://localhost:1/Lists/Announcements/2_.000"))
@@ -1240,15 +1237,8 @@
 
   @Test
   public void testParseError() throws Exception {
-    adaptor = new SharePointAdaptor(initableSiteDataFactory,
-        new UnsupportedUserGroupFactory(), new UnsupportedHttpClient(),
-        executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
-    SharePointAdaptor.SiteDataClient client = adaptor.new SiteDataClient(
-        "http://localhost:1", "http://localhost:1",
-        new UnsupportedSiteData(), new UnsupportedUserGroupSoap(),
-        new UnsupportedCallable<MemberIdMapping>(),
-        new UnsupportedCallable<MemberIdMapping>());
+    SiteDataClient client = new SiteDataClient(
+        new UnsupportedSiteData(), false);
     String xml = "<broken";
     thrown.expect(IOException.class);
     client.jaxbParse(xml, SPContentDatabase.class);
@@ -1256,16 +1246,8 @@
 
   @Test
   public void testValidationError() throws Exception {
-    config.overrideKey("sharepoint.xmlValidation", "true");
-    adaptor = new SharePointAdaptor(initableSiteDataFactory,
-        new UnsupportedUserGroupFactory(),
-        new UnsupportedHttpClient(), executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
-    SharePointAdaptor.SiteDataClient client = adaptor.new SiteDataClient(
-        "http://localhost:1", "http://localhost:1",
-        new UnsupportedSiteData(), new UnsupportedUserGroupSoap(),
-        new UnsupportedCallable<MemberIdMapping>(),
-        new UnsupportedCallable<MemberIdMapping>());
+    SiteDataClient client = new SiteDataClient(
+        new UnsupportedSiteData(), true);
     // Lacks required child element.
     String xml = "<SPContentDatabase"
         + " xmlns='http://schemas.microsoft.com/sharepoint/soap/'/>";
@@ -1275,16 +1257,8 @@
 
   @Test
   public void testDisabledValidation() throws Exception {
-    adaptor = new SharePointAdaptor(initableSiteDataFactory,
-        new UnsupportedUserGroupFactory(),
-        new UnsupportedHttpClient(), executorFactory);
-    config.overrideKey("sharepoint.xmlValidation", "false");
-    adaptor.init(new MockAdaptorContext(config, null));
-    SharePointAdaptor.SiteDataClient client = adaptor.new SiteDataClient(
-        "http://localhost:1", "http://localhost:1",
-        new UnsupportedSiteData(), new UnsupportedUserGroupSoap(),
-        new UnsupportedCallable<MemberIdMapping>(),
-        new UnsupportedCallable<MemberIdMapping>());
+    SiteDataClient client = new SiteDataClient(
+        new UnsupportedSiteData(), false);
     // Lacks required child element.
     String xml = "<SPContentDatabase"
         + " xmlns='http://schemas.microsoft.com/sharepoint/soap/'/>";
@@ -1294,16 +1268,8 @@
 
   @Test
   public void testParseUnknownXml() throws Exception {
-    config.overrideKey("sharepoint.xmlValidation", "true");
-    adaptor = new SharePointAdaptor(initableSiteDataFactory,
-        new UnsupportedUserGroupFactory(),
-        new UnsupportedHttpClient(), executorFactory);
-    adaptor.init(new MockAdaptorContext(config, null));
-    SharePointAdaptor.SiteDataClient client = adaptor.new SiteDataClient(
-        "http://localhost:1", "http://localhost:1",
-        new UnsupportedSiteData(), new UnsupportedUserGroupSoap(),
-        new UnsupportedCallable<MemberIdMapping>(),
-        new UnsupportedCallable<MemberIdMapping>());
+    SiteDataClient client = new SiteDataClient(
+        new UnsupportedSiteData(), true);
     // Valid XML, but not any class that we know about.
     String xml = "<html/>";
     thrown.expect(IOException.class);
@@ -1346,8 +1312,8 @@
     }
   }
 
-  private SPContentDatabase parseChanges(
-      SharePointAdaptor.SiteDataClient client, String xml) throws IOException {
+  private SPContentDatabase parseChanges(String xml) throws IOException {
+    SiteDataClient client = new SiteDataClient(new UnsupportedSiteData(), true);
     String xmlns = "http://schemas.microsoft.com/sharepoint/soap/";
     xml = xml.replace("<SPContentDatabase ",
         "<SPContentDatabase xmlns='" + xmlns + "' ");