Merge branch 'master' of https://code.google.com/p/plexi.sharepoint
diff --git a/SiteData.wsdl b/SiteData.wsdl
index 75bc1ce..bc5cede 100644
--- a/SiteData.wsdl
+++ b/SiteData.wsdl
@@ -943,7 +943,7 @@
 
       <s:complexType name='permissionsForACL'>
         <s:sequence >
-          <s:element name='permission' type='tns:permission' maxOccurs='unbounded' />
+          <s:element name='permission' type='tns:permission' minOccurs='0' maxOccurs='unbounded' />
         </s:sequence>
       </s:complexType>
 
diff --git a/lib/plexi b/lib/plexi
index a90492e..32f1a1c 160000
--- a/lib/plexi
+++ b/lib/plexi
@@ -1 +1 @@
-Subproject commit a90492e5cc03a58f97aad3f5c5fbc5b6e35eb489
+Subproject commit 32f1a1c24a61b3e97f7a9b97dc9a62b5e39c0b81
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
index cc6f506..add21ef 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
@@ -187,6 +187,7 @@
       = new ConcurrentSkipListMap<String, String>();
   private final SiteDataFactory siteDataFactory;
   private final HttpClient httpClient;
+  private boolean xmlValidation;
   private NtlmAuthenticator ntlmAuthenticator;
 
   public SharePointAdaptor() {
@@ -215,6 +216,7 @@
     config.addKey("sharepoint.server", null);
     config.addKey("sharepoint.username", null);
     config.addKey("sharepoint.password", null);
+    config.addKey("sharepoint.xmlValidation", "true");
   }
 
   @Override
@@ -225,6 +227,8 @@
     String username = config.getValue("sharepoint.username");
     String password = context.getSensitiveValueDecoder().decodeValue(
         config.getValue("sharepoint.password"));
+    xmlValidation = Boolean.parseBoolean(
+        config.getValue("sharepoint.xmlValidation"));
 
     log.log(Level.CONFIG, "VirtualServer: {0}", virtualServer);
     log.log(Level.CONFIG, "Username: {0}", username);
@@ -506,24 +510,28 @@
       VirtualServer vs = getContentVirtualServer();
 
       final long necessaryPermissionMask = LIST_ITEM_MASK;
-      List<String> permitUsers = new ArrayList<String>();
-      List<String> denyUsers = new ArrayList<String>();
+      // A PolicyUser is either a user or group, but we aren't provided with
+      // which. Thus, we treat PolicyUsers as both a user and a group in ACLs
+      // and understand that only one of the two entries will have an effect.
+      List<String> permitIds = new ArrayList<String>();
+      List<String> denyIds = new ArrayList<String>();
       for (PolicyUser policyUser : vs.getPolicies().getPolicyUser()) {
         // TODO(ejona): special case NT AUTHORITY\LOCAL SERVICE.
         String loginName = policyUser.getLoginName();
         long grant = policyUser.getGrantMask().longValue();
         if ((necessaryPermissionMask & grant) == necessaryPermissionMask) {
-          permitUsers.add(loginName);
+          permitIds.add(loginName);
         }
         long deny = policyUser.getDenyMask().longValue();
         // If at least one necessary bit is masked, then deny user.
         if ((necessaryPermissionMask & deny) != 0) {
-          denyUsers.add(loginName);
+          denyIds.add(loginName);
         }
       }
       response.setAcl(new Acl.Builder()
           .setInheritanceType(Acl.InheritanceType.PARENT_OVERRIDES)
-          .setPermitUsers(permitUsers).setDenyUsers(denyUsers).build());
+          .setPermitUsers(permitIds).setPermitGroups(permitIds)
+          .setDenyUsers(denyIds).setDenyGroups(denyIds).build());
 
       response.setContentType("text/html");
       HtmlResponseWriter writer = createHtmlResponseWriter(response);
@@ -1280,18 +1288,22 @@
       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 {
-          // SharePoint 2010 (at least sometimes) does not set
-          // lastChangeId = currentChangeId when paging is complete, even
-          // though this is a "MUST" requirement in the documentation.
-          if (!moreChanges.value) {
+          // SharePoint 2010 sometimes does not set lastChangeId=currentChangeId
+          // nor moreChanges=false when paging is complete, even though both of
+          // these conditions are a "MUST" requirement in the documentation.
+          // Thus, we make sure that each call changes the lastChangeId.
+          if (!moreChanges.value
+              || lastChangeId.value.equals(lastLastChangeId.value)) {
             return null;
           }
+          lastLastChangeId.value = lastChangeId.value;
           Holder<String> result = new Holder<String>();
           siteData.getChanges(ObjectType.CONTENT_DATABASE, contentDatabaseGuid,
               lastChangeId, currentChangeId, 15, result, moreChanges);
@@ -1315,7 +1327,9 @@
       Source source = new StreamSource(new StringReader(xml));
       try {
         Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
-        unmarshaller.setSchema(schema);
+        if (xmlValidation) {
+          unmarshaller.setSchema(schema);
+        }
         return unmarshaller.unmarshal(source, klass).getValue();
       } catch (JAXBException ex) {
         throw new XmlProcessingException(ex, xml);
@@ -1375,7 +1389,7 @@
   static class FileInfo {
     /** Non-null contents. */
     private final InputStream contents;
-    /** Non-null headers. */
+    /** Non-null headers. Alternates between header name and header value. */
     private final List<String> headers;
 
     private FileInfo(InputStream contents, List<String> headers) {
@@ -1387,6 +1401,10 @@
       return contents;
     }
 
+    public List<String> getHeaders() {
+      return headers;
+    }
+
     public int getHeaderCount() {
       return headers.size() / 2;
     }
@@ -1430,6 +1448,10 @@
         return this;
       }
 
+      /**
+       * Sets the headers recieved as a response. List must alternate between
+       * header name and header value.
+       */
       public Builder setHeaders(List<String> headers) {
         if (headers == null) {
           throw new NullPointerException();
@@ -1459,7 +1481,7 @@
     public FileInfo issueGetRequest(URL url) throws IOException;
   }
 
-  private static class HttpClientImpl implements HttpClient {
+  static class HttpClientImpl implements HttpClient {
     @Override
     public FileInfo issueGetRequest(URL url) throws IOException {
       HttpURLConnection conn = (HttpURLConnection) url.openConnection();
@@ -1493,7 +1515,7 @@
     public SiteDataSoap newSiteData(String endpoint);
   }
 
-  private static class SiteDataFactoryImpl implements SiteDataFactory {
+  static class SiteDataFactoryImpl implements SiteDataFactory {
     private final Service siteDataService;
 
     public SiteDataFactoryImpl() {
diff --git a/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java b/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java
index 7f820cf..d43c183 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java
@@ -27,6 +27,8 @@
   private String contentType;
   private Metadata metadata = new Metadata();
   private Acl acl;
+  private boolean secure;
+  private Date lastModified;
   private List<URI> anchorUris = new ArrayList<URI>();
   private List<String> anchorTexts = new ArrayList<String>();
   private boolean notFound;
@@ -69,6 +71,16 @@
   }
 
   @Override
+  public void setSecure(boolean secure) {
+    this.secure = secure;
+  }
+
+  @Override
+  public void setLastModified(Date lastModified) {
+    this.lastModified = lastModified;
+  }
+
+  @Override
   public void addAnchor(URI uri, String text) {
     anchorUris.add(uri);
     anchorTexts.add(text);
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
index 9bf39f2..fdb76a8 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
@@ -256,10 +256,11 @@
         + "SiteCollection</a></li>"
         + "</ul></body></html>";
     assertEquals(golden, responseString);
+    List<String> permit = Arrays.asList("GDC-PSL\\Administrator",
+        "GDC-PSL\\spuser1", "NT AUTHORITY\\LOCAL SERVICE");
     assertEquals(new Acl.Builder()
         .setInheritanceType(Acl.InheritanceType.PARENT_OVERRIDES)
-        .setPermitUsers(Arrays.asList("GDC-PSL\\Administrator",
-            "GDC-PSL\\spuser1", "NT AUTHORITY\\LOCAL SERVICE")).build(),
+        .setPermitUsers(permit).setPermitGroups(permit).build(),
         response.getAcl());
   }
 
@@ -3100,7 +3101,9 @@
         setValue(currentChangeId, "1;0;4fb7dea1-2912-4927-9eda-1ea2f0977cf9;634"
             + "727056595000000;604");
         setValue(getChangesResult, getChangesContentDatabase4fb);
-        setValue(moreChanges, false);
+        // Purposefully make moreChanges=true even though there are no more
+        // pages, because SP 2010 has been known to do this.
+        setValue(moreChanges, true);
       }
     };
     SiteDataFactory siteDataFactory = new SingleSiteDataFactory(siteData,
@@ -3331,6 +3334,22 @@
   }
 
   @Test
+  public void testDisabledValidation() throws Exception {
+    adaptor = new SharePointAdaptor(new UnsupportedSiteDataFactory(),
+        new UnsupportedHttpClient());
+    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 UnsupportedCallable<MemberIdMapping>());
+    // Lacks required child element.
+    String xml = "<SPContentDatabase"
+        + " xmlns='http://schemas.microsoft.com/sharepoint/soap/'/>";
+    assertNotNull(client.jaxbParse(xml, SPContentDatabase.class));
+  }
+
+
+  @Test
   public void testParseUnknownXml() throws Exception {
     adaptor = new SharePointAdaptor(new UnsupportedSiteDataFactory(),
         new UnsupportedHttpClient());