Merge branch 'master' of https://code.google.com/p/plexi.sharepoint
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
index f692a68..78b6fbf 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
@@ -367,11 +367,6 @@
    */
   private boolean isSp2007;
   private NtlmAuthenticator ntlmAuthenticator;
-  /**
-   * Lock for refreshing MemberIdMapping. We use a unique lock because it is
-   * held while waiting on I/O.
-   */
-  private final Object refreshMemberIdMappingLock = new Object();
   
   private FormsAuthenticationHandler authenticationHandler;
   private static final TimeZone gmt = TimeZone.getTimeZone("GMT");
@@ -1304,6 +1299,18 @@
     private final Callable<MemberIdMapping> memberIdMappingCallable;
     private final Callable<MemberIdMapping> siteUserIdMappingCallable;
 
+    /**
+     * Lock for refreshing MemberIdMapping. We use a unique lock because it is
+     * held while waiting on I/O.
+     */
+    private final Object refreshMemberIdMappingLock = new Object();
+
+    /**
+     * Lock for refreshing SiteUserMapping. We use a unique lock because it is
+     * held while waiting on I/O.
+     */
+    private final Object refreshSiteUserMappingLock = new Object();
+
     public SiteAdaptor(String site, String web, SiteDataSoap siteDataSoap,
         UserGroupSoap userGroupSoap, PeopleSoap people,
         Callable<MemberIdMapping> memberIdMappingCallable,
@@ -1361,6 +1368,27 @@
       return getMemberIdMapping();
     }
 
+    /**
+     * Provide a more recent SiteUserMapping than {@code mapping}, because the
+     * mapping is known to be out-of-date.
+     */
+    private MemberIdMapping refreshSiteUserMapping(MemberIdMapping mapping)
+        throws IOException {
+      // Synchronize callers to prevent a rush of invalidations due to multiple
+      // callers noticing that the map was out of date at the same time.
+      synchronized (refreshSiteUserMappingLock) {
+        // NOTE: This may block on I/O, so we must be wary of what locks are
+        // held.
+        MemberIdMapping maybeNewMapping = getSiteUserMapping();
+        if (mapping != maybeNewMapping) {
+          // The map has already been refreshed.
+          return maybeNewMapping;
+        }
+        siteUserCache.invalidate(siteUrl);
+      }
+      return getSiteUserMapping();
+    }
+
      private MemberIdMapping getSiteUserMapping() throws IOException {
       try {
         return siteUserIdMappingCallable.call();
@@ -1924,7 +1952,9 @@
         final long necessaryPermissionMask) throws IOException {
       List<Principal> permits = new LinkedList<Principal>();
       MemberIdMapping mapping = getMemberIdMapping();
-      MemberIdMapping newMapping = null;
+      boolean memberIdMappingRefreshed = false;
+      MemberIdMapping siteUserMapping = null;
+      boolean siteUserMappingRefreshed = false;
       for (Permission permission : permissions) {
         // Although it is named "mask", this is really a bit-field of
         // permissions.
@@ -1935,13 +1965,31 @@
         Integer id = permission.getMemberid();
         Principal principal = mapping.getPrincipal(id);
         if (principal == null) {
-          if (newMapping == null) {
-            newMapping = refreshMemberIdMapping(mapping);
-          }
-          principal = newMapping.getPrincipal(id);
+          log.log(Level.FINE, "Member id {0} is not available in memberid"
+              + " mapping for Web [{1}] under Site Collection [{2}].",
+              new Object[] {id, webUrl, siteUrl});
+          if (siteUserMapping == null) {
+            siteUserMapping = getSiteUserMapping();
+          }          
+          principal = siteUserMapping.getPrincipal(id);
         }
+        if (principal == null && !memberIdMappingRefreshed) {
+          // Try to refresh member id mapping and check again.
+          mapping = refreshMemberIdMapping(mapping);
+          memberIdMappingRefreshed = true;
+          principal = mapping.getPrincipal(id);
+        }        
+        if (principal == null && !siteUserMappingRefreshed) {
+          // Try to refresh site user mapping and check again.
+          siteUserMapping = refreshSiteUserMapping(siteUserMapping);
+          siteUserMappingRefreshed = true;
+          principal = siteUserMapping.getPrincipal(id);
+        }
+
         if (principal == null) {
-          log.log(Level.WARNING, "Could not resolve member id {0}", id);
+          log.log(Level.WARNING, "Could not resolve member id {0} for Web "
+              + "[{1}] under Site Collection [{2}].", 
+              new Object[] {id, webUrl, siteUrl});
           continue;
         }
         permits.add(principal);
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
index 9ce1fb3..ae5d523 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
@@ -836,6 +836,74 @@
     assertEquals(URI.create("http://localhost:1/sites/SiteCollection"),
         response.getDisplayUrl());
   }
+  
+  @Test
+  public void testGetDocContentSubSiteUniquePermissions() throws Exception {
+    String subSiteUrl = "http://localhost:1/sites/SiteCollection/SubSite";
+    Users users = new Users();
+    users.getUser().add(createUserGroupUser(1, "GDC-PSL\\administrator",
+        "S-1-5-21-7369146", "Administrator", "admin@domain.com", false, true));
+    users.getUser().add(createUserGroupUser(7, "GDC-PSL\\User1",
+        "S-1-5-21-736911", "User1", "User1@domain.com", false, false));
+    users.getUser().add(createUserGroupUser(500, "GDC-PSL\\User500",
+        "S-1-5-21-7369500", "User500", "User11@domain.com", false, false));
+    users.getUser().add(createUserGroupUser(1073741823, "System.Account",
+        "S-1-5-21-7369343", "System Account", "System.Account@domain.com",
+        false, true));
+
+    MockUserGroupSoap mockUserGroupSoap = new MockUserGroupSoap(users);
+    SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(VS_ENDPOINT, MockSiteData.blank()
+            .register(VS_CONTENT_EXCHANGE)
+            .register(CD_CONTENT_EXCHANGE)
+            .register(SITES_SITECOLLECTION_SAW_EXCHANGE)
+            .register(new SiteAndWebExchange(subSiteUrl, 0,
+                "http://localhost:1/sites/SiteCollection", subSiteUrl)))
+        .endpoint(SITES_SITECOLLECTION_ENDPOINT, MockSiteData.blank()
+            .register(SITES_SITECOLLECTION_URLSEG_EXCHANGE)
+            .register(SITES_SITECOLLECTION_S_CONTENT_EXCHANGE)
+            .register(SITES_SITECOLLECTION_SC_CONTENT_EXCHANGE))
+        .endpoint(subSiteUrl + "/_vti_bin/SiteData.asmx", MockSiteData.blank()
+                .register(new URLSegmentsExchange(
+                    subSiteUrl, true, "WebId", null, null, null))
+                .register(SITES_SITECOLLECTION_S_CONTENT_EXCHANGE
+                    .replaceInContent("/SiteCollection",
+                        "/SiteCollection/SubSite")
+                    .replaceInContent(
+                        "ScopeID=\"{01abac8c-66c8-4fed-829c-8dd02bbf40dd}\"",
+                        "ScopeID=\"{O7ac581ea-fdd1-4b0d-a5de-fc1b69e57a8d}\"")
+                    .replaceInContent(
+                        "<permission memberid='4' mask='756052856929' />",
+                        "<permission memberid='4' mask='0' />")
+                    .replaceInContent("</permissions>",
+                        "<permission memberid='500' mask='756052856929' />"
+                            + "</permissions>"))
+                .register(SITES_SITECOLLECTION_SC_CONTENT_EXCHANGE))
+        .endpoint("http://localhost:1/sites/SiteCollection/"
+            + "_vti_bin/UserGroup.asmx", mockUserGroupSoap);
+
+    adaptor = new SharePointAdaptor(siteDataFactory,
+        new UnsupportedHttpClient(), executorFactory,
+        new MockAuthenticationClientFactoryForms());
+    adaptor.init(new MockAdaptorContext(config, pusher));
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    GetContentsRequest request = new GetContentsRequest(
+        new DocId("http://localhost:1/sites/SiteCollection/SubSite"));
+    GetContentsResponse response = new GetContentsResponse(baos);
+    adaptor.getDocContent(request, response);
+   
+    assertEquals(new Acl.Builder()
+        .setEverythingCaseInsensitive()
+        .setInheritFrom(new DocId("http://localhost:1/sites/SiteCollection"),
+          "admin")
+        .setInheritanceType(Acl.InheritanceType.PARENT_OVERRIDES)
+        .setPermitGroups(Arrays.asList(
+            SITES_SITECOLLECTION_MEMBERS,
+            SITES_SITECOLLECTION_OWNERS))
+        .setPermitUsers(Arrays.asList(GDC_PSL_SPUSER1, 
+            new UserPrincipal("GDC-PSL\\User500", DEFAULT_NAMESPACE))).build(),
+        response.getAcl());
+  }
 
   @Test
   public void testGetDocContentSiteCollectionWithAdGroup() throws Exception {
@@ -931,12 +999,16 @@
   public void testGetDocContentSiteCollectionWithOutOfDateMemberCache()
       throws Exception {
     ReferenceSiteData siteData = new ReferenceSiteData();
+    Users users = new Users();
+    MockUserGroupSoap mockUserGroupSoap = new MockUserGroupSoap(users);
     SoapFactory siteDataFactory = MockSoapFactory.blank()
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(CD_CONTENT_EXCHANGE)
             .register(SITES_SITECOLLECTION_SAW_EXCHANGE))
-        .endpoint(SITES_SITECOLLECTION_ENDPOINT, siteData);
+        .endpoint(SITES_SITECOLLECTION_ENDPOINT, siteData)
+        .endpoint("http://localhost:1/sites/SiteCollection/"
+            + "_vti_bin/UserGroup.asmx", mockUserGroupSoap);
     SiteDataSoap siteDataState1 = MockSiteData.blank()
             .register(SITES_SITECOLLECTION_URLSEG_EXCHANGE)
             .register(SITES_SITECOLLECTION_S_CONTENT_EXCHANGE)