Add local group feeding support

Both full push and update detection are supported.
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
index 3658863..8eaf59f 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
@@ -400,9 +400,45 @@
   }
 
   @Override
-  public void getDocIds(DocIdPusher pusher) throws InterruptedException {
+  public void getDocIds(DocIdPusher pusher) throws InterruptedException,
+      IOException {
     log.entering("SharePointAdaptor", "getDocIds", pusher);
     pusher.pushDocIds(Arrays.asList(virtualServerDocId));
+
+    SiteAdaptor vsAdaptor = getSiteAdaptor(virtualServer, virtualServer);
+    SiteDataClient vsClient = vsAdaptor.getSiteDataClient();
+    VirtualServer vs = vsClient.getContentVirtualServer();
+    Map<GroupPrincipal, Collection<Principal>> defs
+        = new HashMap<GroupPrincipal, Collection<Principal>>();
+    for (ContentDatabases.ContentDatabase cdcd
+        : vs.getContentDatabases().getContentDatabase()) {
+      ContentDatabase cd;
+      try {
+        cd = vsClient.getContentContentDatabase(cdcd.getID(), true);
+      } catch (IOException ex) {
+        log.log(Level.WARNING, "Failed to get local groups for database {0}",
+            cdcd.getID());
+        continue;
+      }
+      if (cd.getSites() == null) {
+        continue;
+      }
+      for (Sites.Site siteListing : cd.getSites().getSite()) {
+        String siteString
+            = vsAdaptor.encodeDocId(siteListing.getURL()).getUniqueId();
+        SiteAdaptor siteAdaptor = getSiteAdaptor(siteString, siteString);
+        Site site;
+        try {
+          site = siteAdaptor.getSiteDataClient().getContentSite();
+        } catch (IOException ex) {
+          log.log(Level.WARNING, "Failed to get local groups for site {0}",
+              siteString);
+          continue;
+        }
+        defs.putAll(siteAdaptor.computeMembersForGroups(site.getGroups()));
+      }
+    }
+    pusher.pushGroupDefinitions(defs, false);
     log.exiting("SharePointAdaptor", "getDocIds");
   }
 
@@ -487,6 +523,8 @@
           = client.getChangesContentDatabase(contentDatabase, changeId,
               isSp2007);
       Set<DocId> docIds = new HashSet<DocId>();
+      Map<GroupPrincipal, Collection<Principal>> groupDefs
+          = new HashMap<GroupPrincipal, Collection<Principal>>();
       try {
         while (true) {
           try {
@@ -494,7 +532,8 @@
             if (changes == null) {
               break;
             }
-            siteAdaptor.getModifiedDocIdsContentDatabase(changes, docIds);
+            siteAdaptor.getModifiedDocIdsContentDatabase(
+                changes, docIds, groupDefs);
           } catch (XmlProcessingException ex) {
             log.log(Level.WARNING, "Error parsing changes from content "
                 + "database: " + contentDatabase, ex);
@@ -519,6 +558,7 @@
         records.add(builder.setDocId(docId).build());
       }
       pusher.pushRecords(records);
+      pusher.pushGroupDefinitions(groupDefs, false);
     }
     log.exiting("SharePointAdaptor", "getModifiedDocIds", pusher);
   }
@@ -1628,7 +1668,9 @@
 
     @VisibleForTesting
     void getModifiedDocIdsContentDatabase(SPContentDatabase changes,
-        Collection<DocId> docIds) throws IOException {
+        Collection<DocId> docIds,
+        Map<GroupPrincipal, Collection<Principal>> groupDefs)
+        throws IOException {
       log.entering("SiteAdaptor", "getModifiedDocIdsContentDatabase",
           new Object[] {changes, docIds});
       if (!"Unchanged".equals(changes.getChange())) {
@@ -1638,17 +1680,22 @@
         String siteString
             = encodeDocId(site.getSite().getMetadata().getURL()).getUniqueId();
         SiteAdaptor siteAdaptor = getSiteAdaptor(siteString, siteString);
-        siteAdaptor.getModifiedDocIdsSite(site, docIds);
+        siteAdaptor.getModifiedDocIdsSite(site, docIds, groupDefs);
       }
       log.exiting("SiteAdaptor", "getModifiedDocIdsContentDatabase");
     }
 
-    private void getModifiedDocIdsSite(SPSite changes, Collection<DocId> docIds)
+    private void getModifiedDocIdsSite(SPSite changes, Collection<DocId> docIds,
+        Map<GroupPrincipal, Collection<Principal>> groupDefs)
         throws IOException {
       log.entering("SiteAdaptor", "getModifiedDocIdsSite",
           new Object[] {changes, docIds});
       if (isModified(changes.getChange())) {
         docIds.add(new DocId(siteUrl));
+        if (changes.getSite().getGroups() != null) {
+          groupDefs.putAll(computeMembersForGroups(
+              changes.getSite().getGroups()));
+        }
       }
       for (SPWeb web : changes.getSPWeb()) {
         String webString
@@ -1817,6 +1864,33 @@
       return mapping;
     }
 
+    private Map<GroupPrincipal, Collection<Principal>> computeMembersForGroups(
+        GroupMembership groups) {
+      Map<GroupPrincipal, Collection<Principal>> defs
+          = new HashMap<GroupPrincipal, Collection<Principal>>();
+      for (GroupMembership.Group group : groups.getGroup()) {
+        GroupPrincipal groupPrincipal = new GroupPrincipal(
+            group.getGroup().getName(), defaultNamespace + "_" + siteUrl);
+        Collection<Principal> members = new LinkedList<Principal>();
+        // We always provide membership details, even for empty groups.
+        defs.put(groupPrincipal, members);
+        if (group.getUsers() == null) {
+          continue;
+        }
+        for (UserDescription user : group.getUsers().getUser()) {
+          Principal principal = userDescriptionToPrincipal(user);
+          if (principal == null) {
+            log.log(Level.WARNING,
+                "Unable to determine login name. Skipping user with ID {0}",
+                user.getID());
+            continue;
+          }
+          members.add(principal);
+        }
+      }
+      return defs;
+    }
+
     private Principal userDescriptionToPrincipal(UserDescription user) {
       boolean isDomainGroup = (user.getIsDomainGroup() == TrueFalseType.TRUE);
       String userName
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
index 84c8fe6..7045ce7 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
@@ -1167,15 +1167,40 @@
 
   @Test
   public void testGetDocIds() throws Exception {
-    adaptor = new SharePointAdaptor(initableSoapFactory,
+    final Map<GroupPrincipal, Collection<Principal>> goldenGroups;
+    {
+      Map<GroupPrincipal, Collection<Principal>> tmp
+          = new TreeMap<GroupPrincipal, Collection<Principal>>();
+      tmp.put(SITES_SITECOLLECTION_OWNERS, Arrays.<Principal>asList(
+          GDC_PSL_ADMINISTRATOR));
+      tmp.put(SITES_SITECOLLECTION_MEMBERS, Arrays.asList(
+            new UserPrincipal("GDC-PSL\\spuser2", DEFAULT_NAMESPACE),
+            new GroupPrincipal("BUILTIN\\users", DEFAULT_NAMESPACE),
+            new UserPrincipal("GDC-PSL\\spuser4", DEFAULT_NAMESPACE)));
+      tmp.put(SITES_SITECOLLECTION_VISITORS, Arrays.<Principal>asList());
+      goldenGroups = Collections.unmodifiableMap(tmp);
+    }
+
+    adaptor = new SharePointAdaptor(MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
+        .endpoint(VS_ENDPOINT, MockSiteData.blank()
+          .register(VS_CONTENT_EXCHANGE)
+          .register(CD_CONTENT_EXCHANGE
+            .replaceInContent("<Site URL=\"http://localhost:1\"\n"
+              + " ID=\"{bb3bb2dd-6ea7-471b-a361-6fb67988755c}\" />", ""))
+          .register(SITES_SITECOLLECTION_SAW_EXCHANGE))
+        .endpoint(SITES_SITECOLLECTION_ENDPOINT, MockSiteData.blank()
+          .register(SITES_SITECOLLECTION_SC_CONTENT_EXCHANGE)),
         new UnsupportedHttpClient(), executorFactory);
     AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
     adaptor.init(new MockAdaptorContext(config, pusher));
     assertEquals(0, pusher.getRecords().size());
+    assertEquals(0, pusher.getGroups().size());
     adaptor.getDocIds(pusher);
-    assertEquals(1, pusher.getRecords().size());
-    assertEquals(new DocIdPusher.Record.Builder(new DocId("")).build(),
-        pusher.getRecords().get(0));
+    assertEquals(
+        Arrays.asList(new DocIdPusher.Record.Builder(new DocId("")).build()),
+        pusher.getRecords());
+    assertEquals(goldenGroups, pusher.getGroups());
   }
 
   @Test
@@ -1364,16 +1389,19 @@
     adaptor.init(new MockAdaptorContext(config, pusher));
     SPContentDatabase result = parseChanges(getChangesContentDatabase);
     List<DocId> docIds = new ArrayList<DocId>();
+    Map<GroupPrincipal, Collection<Principal>> groupDefs
+        = new HashMap<GroupPrincipal, Collection<Principal>>();
     adaptor.new SiteAdaptor(
         "http://localhost:1/sites/SiteCollection",
         "http://localhost:1/sites/SiteCollection", new UnsupportedSiteData(),
         new UnsupportedUserGroupSoap(), new UnsupportedPeopleSoap(),
         new UnsupportedCallable<MemberIdMapping>(),
         new UnsupportedCallable<MemberIdMapping>())
-        .getModifiedDocIdsContentDatabase(result, docIds);
+        .getModifiedDocIdsContentDatabase(result, docIds, groupDefs);
     assertEquals(Arrays.asList(
           new DocId("http://localhost:1/Lists/Announcements/2_.000")),
         docIds);
+    assertEquals(Collections.emptyMap(), groupDefs);
   }
 
   @Test
@@ -2207,9 +2235,12 @@
     }
 
     public ContentExchange replaceInContent(String match, String replacement) {
+      String result = getContentResult.replace(match, replacement);
+      if (getContentResult.equals(result)) {
+        fail("Replacement had not effect");
+      }
       return new ContentExchange(objectType, objectId, folderUrl, itemId,
-          retrieveChildItems, securityOnly, lastItemIdOnPage,
-          getContentResult.replace(match, replacement));
+          retrieveChildItems, securityOnly, lastItemIdOnPage, result);
     }
   }