Clean up and add unit tests for AdEntity.java

Future submissions will similary improve AdAdaptor.java.
diff --git a/src/com/google/enterprise/adaptor/ad/AdEntity.java b/src/com/google/enterprise/adaptor/ad/AdEntity.java
index 3697849..143ed9a 100644
--- a/src/com/google/enterprise/adaptor/ad/AdEntity.java
+++ b/src/com/google/enterprise/adaptor/ad/AdEntity.java
@@ -1,10 +1,9 @@
 package com.google.enterprise.adaptor.ad;
 
-import java.util.logging.Logger;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
 
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
@@ -12,6 +11,7 @@
 import javax.naming.directory.Attributes;
 import javax.naming.directory.SearchResult;
 
+/** Representation of a single user or group from Active Directory. */
 public class AdEntity {
   private static final Logger log =
       Logger.getLogger(AdEntity.class.getName());
@@ -26,6 +26,8 @@
   private long uSNChanged;
   private boolean wellKnown;
   private boolean allMembershipsRetrieved;
+  private final Pattern attrMemberPattern =
+      Pattern.compile("member;range=[0-9]+-.*", Pattern.CASE_INSENSITIVE);
 
   private Object getAttribute(Attributes attributes, String name)
       throws NamingException {
@@ -39,7 +41,7 @@
 
   private Attribute getMemberAttr(Attributes attrs) throws NamingException {
     allMembershipsRetrieved = true;
-    Attribute member = attrs.get(AdConstants.ATTR_MEMBER);
+    Attribute member = attrs.get("member");
     if (member != null && member.size() != 0) {
       return member;
     }
@@ -47,7 +49,7 @@
     NamingEnumeration<String> ids = attrs.getIDs();
     while (ids.hasMore()) {
       String id = ids.next();
-      if (AdConstants.ATTR_MEMBER_PATTERN.matcher(id).matches()) {
+      if (attrMemberPattern.matcher(id).matches()) {
         allMembershipsRetrieved = id.endsWith("*");
         return attrs.get(id);
       }
@@ -66,18 +68,15 @@
     dn = searchResult.getNameInNamespace();
     wellKnown = false;
     Attributes attrs = searchResult.getAttributes();
-    sAMAccountName =
-        (String) getAttribute(attrs, AdConstants.ATTR_SAMACCOUNTNAME);
-    objectGUID =
-        getTextGuid((byte[]) getAttribute(attrs, AdConstants.ATTR_OBJECTGUID));
-    sid = getTextSid((byte[]) getAttribute(attrs, AdConstants.ATTR_OBJECTSID));
-    String s = (String) getAttribute(attrs, AdConstants.ATTR_USNCHANGED);
+    sAMAccountName = (String) getAttribute(attrs, "sAMAccountName");
+    objectGUID = getTextGuid((byte[]) getAttribute(attrs, "objectGUID;binary"));
+    sid = getTextSid((byte[]) getAttribute(attrs, "objectSid;binary"));
+    String s = (String) getAttribute(attrs, "uSNChanged");
     if (s != null) {
       uSNChanged = Long.parseLong(s);
     }
-    primaryGroupId =
-        (String) getAttribute(attrs, AdConstants.ATTR_PRIMARYGROUPID);
-    userPrincipalName = (String) getAttribute(attrs, AdConstants.ATTR_UPN);
+    primaryGroupId = (String) getAttribute(attrs, "primaryGroupId");
+    userPrincipalName = (String) getAttribute(attrs, "userPrincipalName");
 
     members = new HashSet<String>();
     if (isGroup()) {
@@ -106,19 +105,19 @@
   }
 
   /**
-   * Appends additional memberships from search result 
+   * Appends additional memberships from search result
    * @param searchResult which contains additional groups
    * @return number of groups found
    * @throws NamingException
    */
   public int appendGroups(SearchResult searchResult)
       throws NamingException {
-    Attribute member = getMemberAttr(searchResult.getAttributes()); 
+    Attribute member = getMemberAttr(searchResult.getAttributes());
     if (member != null) {
       for (int i = 0; i < member.size(); ++i) {
         members.add(member.get(i).toString());
       }
-      return member.size(); 
+      return member.size();
     } else {
       return 0;
     }
@@ -133,17 +132,14 @@
   public String getCommonName() {
     // LDAP queries return escaped commas to avoid ambiguity, find first not
     // escaped comma
-    int comma = dn.indexOf(AdConstants.COMMA);
-    while (comma > 0 && comma < dn.length()
-        && (dn.charAt(comma - 1) == AdConstants.BACKSLASH_CHAR)) {
-      comma = dn.indexOf(AdConstants.COMMA, comma + 1);
+    int comma = dn.indexOf(",");
+    while (comma > 0 && comma < dn.length() - 1 &&
+        (dn.charAt(comma - 1) == '\\')) {
+      comma = dn.indexOf(",", comma + 1);
     }
     String tmpGroupName = dn.substring(0, comma > 0 ? comma : dn.length());
-    tmpGroupName =
-        tmpGroupName.substring(
-        tmpGroupName.indexOf(AdConstants.EQUALS_CHAR) + 1);
-    tmpGroupName =
-        tmpGroupName.replace(AdConstants.BACKSLASH, AdConstants.EMPTY);
+    tmpGroupName = tmpGroupName.substring(tmpGroupName.indexOf('=') + 1);
+    tmpGroupName = tmpGroupName.replace("\\", "");
     return tmpGroupName;
   }
 
@@ -158,7 +154,7 @@
     if (objectSid == null) {
       return null;
     }
-    StringBuilder strSID = new StringBuilder(AdConstants.SID_START);
+    StringBuilder strSID = new StringBuilder("S-");
     long version = objectSid[0];
     strSID.append(Long.toString(version));
     long authority = objectSid[4];
@@ -167,7 +163,7 @@
       authority <<= 8;
       authority += objectSid[4 + i] & 0xFF;
     }
-    strSID.append(AdConstants.HYPHEN_CHAR).append(Long.toString(authority));
+    strSID.append('-').append(Long.toString(authority));
     long count = objectSid[2];
     count <<= 8;
     count += objectSid[1] & 0xFF;
@@ -179,34 +175,12 @@
         rid <<= 8;
         rid += objectSid[11 - k + (j * 4)] & 0xFF;
       }
-      strSID.append(AdConstants.HYPHEN_CHAR).append(Long.toString(rid));
+      strSID.append('-').append(Long.toString(rid));
     }
     return strSID.toString();
   }
 
   /**
-   * Generate properties to be used for parameter binding in JDBC
-   * @return map of names and properties of current object
-   */
-  public Map<String, Object> getSqlParams() {
-    HashMap<String, Object> map = new HashMap<String, Object>();
-    map.put(AdConstants.DB_DN, dn);
-    map.put(AdConstants.DB_SAMACCOUNTNAME, sAMAccountName.toLowerCase());
-    map.put(AdConstants.DB_UPN, userPrincipalName);
-    map.put(AdConstants.DB_PRIMARYGROUPID, primaryGroupId);
-    if (sid != null) {
-      map.put(AdConstants.DB_DOMAINSID,
-          sid.substring(0, sid.lastIndexOf(AdConstants.HYPHEN_CHAR)));
-      map.put(AdConstants.DB_RID,
-          sid.substring(sid.lastIndexOf(AdConstants.HYPHEN_CHAR) + 1));
-    }
-    map.put(AdConstants.DB_OBJECTGUID, objectGUID);
-    map.put(AdConstants.DB_USNCHANGED, uSNChanged);
-    map.put(AdConstants.DB_WELLKNOWN, wellKnown ? 1 : 0);
-    return map;
-  }
-
-  /**
    * Parses the binary GUID retrieved from LDAP and converts to textual
    * representation. Text version is used to avoid dealing with different
    * BLOB types between databases.
@@ -214,7 +188,7 @@
    * @return string containing the GUID
    */
   public static String getTextGuid(byte[] binaryGuid) {
-    StringBuilder sb = new StringBuilder(AdConstants.GUID_START);
+    StringBuilder sb = new StringBuilder("0x");
     for (byte b : binaryGuid) {
       sb.append(Integer.toHexString(b & 0xFF));
     }
@@ -253,7 +227,7 @@
   public boolean isGroup() {
     return primaryGroupId == null;
   }
-  
+
   public boolean isWellKnown() {
     return wellKnown;
   }
@@ -270,7 +244,7 @@
   }
 
   public String getPrimaryGroupSid() {
-    int index = sid.lastIndexOf(AdConstants.HYPHEN_CHAR) + 1;
+    int index = sid.lastIndexOf('-') + 1;
     return sid.substring(0,  index) + primaryGroupId;
   }
 
diff --git a/test/com/google/enterprise/adaptor/ad/AdEntityTest.java b/test/com/google/enterprise/adaptor/ad/AdEntityTest.java
new file mode 100644
index 0000000..cfca6d4
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/ad/AdEntityTest.java
@@ -0,0 +1,132 @@
+// 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.ad;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import java.util.*;
+
+import javax.naming.directory.*;
+
+/** Test cases for {@link AdEntity}. */
+public class AdEntityTest {
+  @Test
+  public void testStandardConstructor() throws Exception {
+    Attributes attrs = new BasicAttributes();
+    attrs.put("objectGUID;binary",
+        AdServerTest.hexStringToByteArray("000102030405060708090a0b0c"));
+    attrs.put("objectSid;binary", // S-1-0-0
+        AdServerTest.hexStringToByteArray("010100000000000000000000"));
+    attrs.put("uSNChanged", "12345678");
+    attrs.put("primaryGroupId", "users");
+
+    SearchResult sr = new SearchResult("SR name", attrs, attrs);
+    sr.setNameInNamespace("cn=user,ou=Users,dc=example,dc=com");
+    AdEntity adEntity = new AdEntity(sr);
+    assertEquals("user", adEntity.getCommonName());
+    assertEquals("S-1-0-0", adEntity.getSid());
+    assertEquals("cn=user,ou=Users,dc=example,dc=com", adEntity.getDn());
+    assertFalse(adEntity.isWellKnown());
+    assertEquals(0, adEntity.getMembers().size());
+    assertEquals("S-1-0-users", adEntity.getPrimaryGroupSid());
+  }
+
+  @Test
+  public void testWellKnownConstructor() throws Exception {
+    AdEntity adEntity = new AdEntity("S-1-1-1",
+        "dn=escaped\\,cn=users,ou=Users,dc=example,dc=com");
+    assertEquals("escaped,cn=users", adEntity.getCommonName());
+    assertEquals("S-1-1-1", adEntity.getSid());
+    assertEquals("dn=escaped\\,cn=users,ou=Users,dc=example,dc=com",
+        adEntity.getDn());
+    assertTrue(adEntity.isWellKnown());
+    assertEquals(0, adEntity.getMembers().size());
+  }
+
+  @Test
+  public void testWellKnownConstructorNoCommaInDN() throws Exception {
+    AdEntity adEntity = new AdEntity("NoComma", "dc=com");
+    assertEquals("com", adEntity.getCommonName());
+    assertEquals("dc=com", adEntity.getDn());
+    assertTrue(adEntity.isWellKnown());
+  }
+
+  @Test
+  public void testWellKnownConstructorTrailingComma() throws Exception {
+    AdEntity adEntity = new AdEntity("NoComma", "dc=com,");
+    assertEquals("com", adEntity.getCommonName());
+    assertEquals("dc=com,", adEntity.getDn());
+    assertTrue(adEntity.isWellKnown());
+  }
+
+  @Test
+  public void testAppendGroupsOnEmptyGroup() throws Exception {
+    AdEntity adEntity = new AdEntity("parentGroup", "dc=com");
+
+    Attributes attrs = new BasicAttributes();
+    attrs.put("objectGUID;binary",
+        AdServerTest.hexStringToByteArray("000102030405060708090a0b0c"));
+    attrs.put("objectSid;binary", // S-1-0-0
+        AdServerTest.hexStringToByteArray("010100000000000000000000"));
+    attrs.put("member", null);
+    Attribute memberAttr = attrs.get("member");
+    memberAttr.clear();
+    SearchResult sr = new SearchResult("subgroup", attrs, attrs);
+    sr.setNameInNamespace("cn=subgroup,ou=Groups,dc=example,dc=com");
+    AdEntity ae = new AdEntity(sr);
+
+    HashSet<String> expectedMembers = new HashSet<String>();
+    assertEquals(expectedMembers, ae.getMembers());
+    assertEquals(0, adEntity.appendGroups(sr));
+  }
+
+  @Test
+  public void testAppendGroupsOnRealGroup() throws Exception {
+    AdEntity adEntity = new AdEntity("parentGroup", "dc=com");
+
+    Attributes attrs = new BasicAttributes();
+    attrs.put("objectGUID;binary",
+        AdServerTest.hexStringToByteArray("000102030405060708090a0b0c"));
+    attrs.put("objectSid;binary", // S-1-0-0
+        AdServerTest.hexStringToByteArray("010100000000000000000000"));
+    List<String> members = Arrays.asList("dn_for_user_1", "dn_for_user_2");
+    attrs.put("member", null);
+    Attribute memberAttr = attrs.get("member");
+    memberAttr.clear();
+    for (String member: members) {
+      memberAttr.add(member);
+    }
+
+    SearchResult sr = new SearchResult("subgroup", attrs, attrs);
+    sr.setNameInNamespace("cn=subgroup,ou=Groups,dc=example,dc=com");
+    AdEntity ae = new AdEntity(sr);
+
+    assertEquals(new HashSet<String>(members), ae.getMembers());
+    assertEquals(2, adEntity.appendGroups(sr));
+  }
+
+  @Test
+  public void testParseForeignSecurityPrincipal() throws Exception {
+    AdEntity adEntity = new AdEntity("NoComma", "dc=com");
+    assertNull(adEntity.parseForeignSecurityPrincipal(""));
+    assertNull(adEntity.parseForeignSecurityPrincipal(
+        "cn=foreignsecurityprincipals,dc=example,dc=com"));
+    String validSid = "S-1-5-21-42";
+    assertEquals(validSid, adEntity.parseForeignSecurityPrincipal(
+        "id=" + validSid + ",cn=foreignsecurityprincipals,dc=example,dc=com"));
+  }
+}