Add ACL transform support to feeds

Named resources now are transformed as would be expected. Group
definitions took more work to define what was expected.

Group definitions aren't ACLs, so AclTransform turned into more of a
"Principal Transform", but its name is being kept. Since going through
the transform can cause duplicates, we now sort and remove duplicates of
the outgoing principals for each group (like we do for ACLs). However,
removing/merging duplicate group principal keys does not occur, because
there is no way to handle this properly.

Therefore, ACL transforms have the additional limitation that you must
not cause collisions of group names whose definitions are being sent in
feeds. There is no checking to warn the user if a collision occurs.
diff --git a/src/com/google/enterprise/adaptor/AclTransform.java b/src/com/google/enterprise/adaptor/AclTransform.java
index af740b1..dba8b7c 100644
--- a/src/com/google/enterprise/adaptor/AclTransform.java
+++ b/src/com/google/enterprise/adaptor/AclTransform.java
@@ -37,23 +37,45 @@
       return acl;
     }
     return new Acl.Builder(acl)
-        .setPermits(transform(acl.getPermits()))
-        .setDenies(transform(acl.getDenies()))
+        .setPermits(transformInternal(acl.getPermits()))
+        .setDenies(transformInternal(acl.getDenies()))
         .build();
   }
 
-  private Set<Principal> transform(Set<Principal> principals) {
-    Set<Principal> newPrincipals = new TreeSet<Principal>();
-    for (Principal principal : principals) {
-      ParsedPrincipal parsed = principal.parse();
-      for (Rule rule : rules) {
-        if (rule.match.matches(parsed)) {
-          parsed = rule.replace.replace(parsed);
-        }
-      }
-      newPrincipals.add(parsed.toPrincipal());
+  public <T extends Principal> Collection<T> transform(
+      Collection<T> principals) {
+    if (rules.isEmpty()) {
+      return principals;
     }
-    return Collections.unmodifiableSet(newPrincipals);
+    return transformInternal(principals);
+  }
+
+  public <T extends Principal> T transform(T principal) {
+    if (rules.isEmpty()) {
+      return principal;
+    }
+    return transformInternal(principal);
+  }
+
+  private <T extends Principal> Collection<T> transformInternal(
+      Collection<T> principals) {
+    Collection<T> newPrincipals = new ArrayList<T>(principals.size());
+    for (T principal : principals) {
+      newPrincipals.add(transformInternal(principal));
+    }
+    return Collections.unmodifiableCollection(newPrincipals);
+  }
+
+  private <T extends Principal> T transformInternal(T principal) {
+    ParsedPrincipal parsed = principal.parse();
+    for (Rule rule : rules) {
+      if (rule.match.matches(parsed)) {
+        parsed = rule.replace.replace(parsed);
+      }
+    }
+    @SuppressWarnings("unchecked")
+    T principalNew = (T) parsed.toPrincipal();
+    return principalNew;
   }
 
   @Override
diff --git a/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java b/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java
index 1980a42..7527a1b 100644
--- a/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java
+++ b/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java
@@ -214,7 +214,8 @@
     GsaFeedFileSender fileSender = new GsaFeedFileSender(
         config.getGsaHostname(), config.isServerSecure(),
         config.getGsaCharacterEncoding());
-    GsaFeedFileMaker fileMaker = new GsaFeedFileMaker(docIdCodec,
+    AclTransform aclTransform = createAclTransform();
+    GsaFeedFileMaker fileMaker = new GsaFeedFileMaker(docIdCodec, aclTransform,
         config.isGsa614FeedWorkaroundEnabled(),
         config.isGsa70AuthMethodWorkaroundEnabled());
     docIdSender
@@ -309,7 +310,7 @@
         docIdCodec, docIdCodec, journal, adaptor, authzAuthority,
         config.getGsaHostname(),
         config.getServerFullAccessHosts(),
-        samlServiceProvider, createTransformPipeline(), createAclTransform(),
+        samlServiceProvider, createTransformPipeline(), aclTransform,
         config.isServerToUseCompression(), watchdog,
         asyncDocIdSender, 
         config.doesGsaAcceptDocControlsHeader(),
diff --git a/src/com/google/enterprise/adaptor/GsaFeedFileMaker.java b/src/com/google/enterprise/adaptor/GsaFeedFileMaker.java
index 19471d2..52bfe01 100644
--- a/src/com/google/enterprise/adaptor/GsaFeedFileMaker.java
+++ b/src/com/google/enterprise/adaptor/GsaFeedFileMaker.java
@@ -47,17 +47,19 @@
       };
 
   private final DocIdEncoder idEncoder;
+  private final AclTransform aclTransform;
   private final boolean separateClosingRecordTagWorkaround;
   private final boolean useAuthMethodWorkaround;
 
-  public GsaFeedFileMaker(DocIdEncoder encoder) {
-    this(encoder, false, false);
+  public GsaFeedFileMaker(DocIdEncoder encoder, AclTransform aclTransform) {
+    this(encoder, aclTransform, false, false);
   }
 
-  public GsaFeedFileMaker(DocIdEncoder encoder,
+  public GsaFeedFileMaker(DocIdEncoder encoder, AclTransform aclTransform,
       boolean separateClosingRecordTagWorkaround,
       boolean useAuthMethodWorkaround) {
     this.idEncoder = encoder;
+    this.aclTransform = aclTransform;
     this.separateClosingRecordTagWorkaround
         = separateClosingRecordTagWorkaround;
     this.useAuthMethodWorkaround = useAuthMethodWorkaround;
@@ -145,6 +147,7 @@
     }
     aclElement.setAttribute("url", uri.toString());
     Acl acl = docAcl.getAcl();
+    acl = aclTransform.transform(acl);
     if (acl.getInheritFrom() != null) {
       URI inheritFrom = idEncoder.encodeDocId(acl.getInheritFrom());
       try {
@@ -270,6 +273,8 @@
   private void constructSingleMembership(Document doc, Element root,
       GroupPrincipal groupPrincipal, Collection<Principal> members,
       boolean caseSensitiveMembers) {
+    groupPrincipal = aclTransform.transform(groupPrincipal);
+    members = new TreeSet<Principal>(aclTransform.transform(members));
     Element groupWithDef = doc.createElement("membership");
     root.appendChild(groupWithDef);
     Element groupKey = doc.createElement("principal");
diff --git a/test/com/google/enterprise/adaptor/AclTransformTest.java b/test/com/google/enterprise/adaptor/AclTransformTest.java
index d23ad61..73de819 100644
--- a/test/com/google/enterprise/adaptor/AclTransformTest.java
+++ b/test/com/google/enterprise/adaptor/AclTransformTest.java
@@ -43,8 +43,12 @@
 
   @Test
   public void testEmptyTransform() {
-    assertSame(baseAcl,
-        new AclTransform(Arrays.<Rule>asList()).transform(baseAcl));
+    AclTransform transform = new AclTransform(Arrays.<Rule>asList());
+    assertSame(baseAcl, transform.transform(baseAcl));
+    Principal principal = new UserPrincipal("user");
+    List<Principal> principals = Arrays.asList(principal);
+    assertSame(principals, transform.transform(principals));
+    assertSame(principal, transform.transform(principal));
   }
 
   @Test
@@ -62,7 +66,7 @@
           new MatchData(null, "anyprincipal", "anydomain", "anyns")));
     AclTransform transform = new AclTransform(rules);
     // Test null passthrough and re-use of AclTransform
-    assertNull(transform.transform(null));
+    assertNull(transform.transform((Acl) null));
     assertEquals(new Acl.Builder(baseAcl)
           .setPermits(Arrays.asList(
             new UserPrincipal("anydomain\\anyprincipal", "anyns"),
diff --git a/test/com/google/enterprise/adaptor/DocIdSenderTest.java b/test/com/google/enterprise/adaptor/DocIdSenderTest.java
index 8f364f5..a417303 100644
--- a/test/com/google/enterprise/adaptor/DocIdSenderTest.java
+++ b/test/com/google/enterprise/adaptor/DocIdSenderTest.java
@@ -316,7 +316,7 @@
     int i;
 
     public MockGsaFeedFileMaker() {
-      super(null);
+      super(null, new AclTransform(Arrays.<AclTransform.Rule>asList()));
     }
 
     @Override
diff --git a/test/com/google/enterprise/adaptor/GsaFeedFileMakerTest.java b/test/com/google/enterprise/adaptor/GsaFeedFileMakerTest.java
index fc7a256..79d63dc 100644
--- a/test/com/google/enterprise/adaptor/GsaFeedFileMakerTest.java
+++ b/test/com/google/enterprise/adaptor/GsaFeedFileMakerTest.java
@@ -28,7 +28,9 @@
   public ExpectedException thrown = ExpectedException.none();
 
   private DocIdEncoder encoder = new MockDocIdCodec();
-  private GsaFeedFileMaker meker = new GsaFeedFileMaker(encoder);
+  private AclTransform aclTransform
+      = new AclTransform(Arrays.<AclTransform.Rule>asList());
+  private GsaFeedFileMaker meker = new GsaFeedFileMaker(encoder, aclTransform);
 
   @Test
   public void testEmptyMetadataAndUrl() {
@@ -198,6 +200,39 @@
   }
 
   @Test
+  public void testNamedResourcesAclTransform() {
+    String golden
+        = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+        + "<!DOCTYPE gsafeed PUBLIC \"-//Google//DTD GSA Feeds//EN\" \"\">\n"
+        + "<gsafeed>\n"
+        + "<!--GSA EasyConnector-->\n"
+        + "<header>\n"
+        + "<datasource>test</datasource>\n"
+        + "<feedtype>metadata-and-url</feedtype>\n"
+        + "</header>\n"
+        + "<group>\n"
+        + "<acl url=\"http://localhost/docid2\">\n"
+        + "<principal access=\"permit\" scope=\"user\">pu2</principal>\n"
+        + "</acl>\n"
+        + "</group>\n"
+        + "</gsafeed>\n";
+
+    List<DocIdSender.AclItem> acls = new ArrayList<DocIdSender.AclItem>();
+    acls.add(new DocIdSender.AclItem(new DocId("docid2"), new Acl.Builder()
+        .setPermitUsers(
+            Arrays.asList(new UserPrincipal("pu1")))
+        .build()));
+
+    aclTransform = new AclTransform(Arrays.asList(new AclTransform.Rule(
+        new AclTransform.MatchData(null, null, null, null),
+        new AclTransform.MatchData(null, "pu2", null, null))));
+    meker = new GsaFeedFileMaker(encoder, aclTransform);
+    String xml = meker.makeMetadataAndUrlXml("test", acls);
+    xml = xml.replaceAll("\r\n", "\n");
+    assertEquals(golden, xml);
+  }
+
+  @Test
   public void testUnsupportedDocIdSenderItemMetadataAndUrl() {
     class UnsupportedItem implements DocIdSender.Item {};
     List<UnsupportedItem> items = new ArrayList<UnsupportedItem>();
@@ -226,7 +261,8 @@
     List<DocIdPusher.Record> records = new ArrayList<DocIdPusher.Record>();
     records.add(new DocIdPusher.Record.Builder(new DocId("docid1")).build());
 
-    meker = new GsaFeedFileMaker(encoder, true /* 6.14 workaround */, false);
+    meker = new GsaFeedFileMaker(encoder, aclTransform,
+        true /* 6.14 workaround */, false);
     String xml = meker.makeMetadataAndUrlXml("test", records);
     xml = xml.replace("\r\n", "\n");
     assertEquals(golden, xml);
@@ -252,7 +288,8 @@
     List<DocIdPusher.Record> records = new ArrayList<DocIdPusher.Record>();
     records.add(new DocIdPusher.Record.Builder(new DocId("docid1")).build());
 
-    meker = new GsaFeedFileMaker(encoder, false, true /* 7.0 workaround */);
+    meker = new GsaFeedFileMaker(encoder, aclTransform, false,
+        true /* 7.0 workaround */);
     String xml = meker.makeMetadataAndUrlXml("test", records);
     xml = xml.replace("\r\n", "\n");
     assertEquals(golden, xml);
@@ -355,12 +392,12 @@
         + " case-sensitivity-type=\"EVERYTHING_CASE_INSENSITIVE\""
         + " namespace=\"Default\""
         + " scope=\"USER\""
-        + ">splat</principal>\n"
+        + ">plump</principal>\n"
         + "<principal" 
         + " case-sensitivity-type=\"EVERYTHING_CASE_INSENSITIVE\""
         + " namespace=\"Default\""
         + " scope=\"USER\""
-        + ">plump</principal>\n"
+        + ">splat</principal>\n"
         + "</members>\n"
         + "</membership>\n"
         + "</xmlgroups>\n";
@@ -389,16 +426,16 @@
         + "<membership>\n"
         + "<principal namespace=\"Default\" scope=\"GROUP\">immortals</principal>\n"
         + "<members>\n"
+        + "<principal"
+        + " case-sensitivity-type=\"EVERYTHING_CASE_SENSITIVE\""
+        + " namespace=\"3vil\""
+        + " scope=\"GROUP\""
+        + ">badguys</principal>\n"
         + "<principal" 
         + " case-sensitivity-type=\"EVERYTHING_CASE_SENSITIVE\""
         + " namespace=\"goodguys\""
         + " scope=\"USER\""
         + ">MacLeod\\Duncan</principal>\n"
-        + "<principal" 
-        + " case-sensitivity-type=\"EVERYTHING_CASE_SENSITIVE\""
-        + " namespace=\"3vil\""
-        + " scope=\"GROUP\""
-        + ">badguys</principal>\n"
         + "</members>\n"
         + "</membership>\n"
         + "</xmlgroups>\n";
@@ -412,4 +449,38 @@
     xml = xml.replaceAll("\r\n", "\n");
     assertEquals(golden, xml);
   }
+
+  @Test
+  public void testGroupsDefinitionsAclTransform() {
+    String golden =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+        + "<!DOCTYPE xmlgroups PUBLIC \"-//Google//DTD GSA Feeds//EN\" \"\">\n"
+        + "<xmlgroups>\n"
+        + "<!--GSA EasyConnector-->\n"
+        + "<membership>\n"
+        + "<principal namespace=\"Default\" scope=\"GROUP\">Clan MacLeod"
+        +  "</principal>\n"
+        + "<members>\n"
+        + "<principal case-sensitivity-type=\"EVERYTHING_CASE_INSENSITIVE\""
+        + " namespace=\"Default\" scope=\"USER\">MacLeod\\Connor</principal>\n"
+        + "</members>\n"
+        + "</membership>\n"
+        + "</xmlgroups>\n";
+    Map<GroupPrincipal, List<Principal>> groupDefs
+        = new TreeMap<GroupPrincipal, List<Principal>>();
+    List<Principal> members = new ArrayList<Principal>();
+    members.add(new UserPrincipal("MacLeod\\Duncan"));
+    groupDefs.put(new GroupPrincipal("immortals"), members);
+    aclTransform = new AclTransform(Arrays.asList(
+        new AclTransform.Rule(
+          new AclTransform.MatchData(null, "Duncan", null, null),
+          new AclTransform.MatchData(null, "Connor", null, null)),
+        new AclTransform.Rule(
+          new AclTransform.MatchData(null, "immortals", null, null),
+          new AclTransform.MatchData(null, "Clan MacLeod", null, null))));
+    meker = new GsaFeedFileMaker(encoder, aclTransform);
+    String xml = meker.makeGroupsDefinitionsXml(groupDefs.entrySet(), false);
+    xml = xml.replaceAll("\r\n", "\n");
+    assertEquals(golden, xml);
+  }
 }