diff --git a/projects/cadcAccessControl-Server/build.xml b/projects/cadcAccessControl-Server/build.xml
index 8a87c3bca324f82a7393879f24f870c304c20d78..b5ad9f91fe28e1e3606f3b6ab652d333b722271e 100644
--- a/projects/cadcAccessControl-Server/build.xml
+++ b/projects/cadcAccessControl-Server/build.xml
@@ -148,7 +148,7 @@
         <pathelement path="${jars}:${testingJars}"/>
       </classpath>
       <sysproperty key="ca.nrc.cadc.util.PropertiesReader.dir" value="test"/>
-      <test name="ca.nrc.cadc.ac.server.web.users.GetUserActionTest" />
+      <test name="ca.nrc.cadc.ac.server.web.users.UserActionFactoryTest" />
       <formatter type="plain" usefile="false" />
     </junit>
   </target>
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupDetailSelector.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupDetailSelector.java
new file mode 100644
index 0000000000000000000000000000000000000000..6874d550ed26406ebbaae8cd9489f1a82b23f11a
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupDetailSelector.java
@@ -0,0 +1,89 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  Government of Canada                 Gouvernement du Canada
+*  National Research Council            Conseil national de recherches
+*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+*  All rights reserved                  Tous droits réservés
+*
+*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+*  expressed, implied, or               énoncée, implicite ou légale,
+*  statutory, of any kind with          de quelque nature que ce
+*  respect to the software,             soit, concernant le logiciel,
+*  including without limitation         y compris sans restriction
+*  any warranty of merchantability      toute garantie de valeur
+*  or fitness for a particular          marchande ou de pertinence
+*  purpose. NRC shall not be            pour un usage particulier.
+*  liable in any event for any          Le CNRC ne pourra en aucun cas
+*  damages, whether direct or           être tenu responsable de tout
+*  indirect, special or general,        dommage, direct ou indirect,
+*  consequential or incidental,         particulier ou général,
+*  arising from the use of the          accessoire ou fortuit, résultant
+*  software.  Neither the name          de l'utilisation du logiciel. Ni
+*  of the National Research             le nom du Conseil National de
+*  Council of Canada nor the            Recherches du Canada ni les noms
+*  names of its contributors may        de ses  participants ne peuvent
+*  be used to endorse or promote        être utilisés pour approuver ou
+*  products derived from this           promouvoir les produits dérivés
+*  software without specific prior      de ce logiciel sans autorisation
+*  written permission.                  préalable et particulière
+*                                       par écrit.
+*
+*  This file is part of the             Ce fichier fait partie du projet
+*  OpenCADC project.                    OpenCADC.
+*
+*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+*  you can redistribute it and/or       vous pouvez le redistribuer ou le
+*  modify it under the terms of         modifier suivant les termes de
+*  the GNU Affero General Public        la “GNU Affero General Public
+*  License as published by the          License” telle que publiée
+*  Free Software Foundation,            par la Free Software Foundation
+*  either version 3 of the              : soit la version 3 de cette
+*  License, or (at your option)         licence, soit (à votre gré)
+*  any later version.                   toute version ultérieure.
+*
+*  OpenCADC is distributed in the       OpenCADC est distribué
+*  hope that it will be useful,         dans l’espoir qu’il vous
+*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+*  without even the implied             GARANTIE : sans même la garantie
+*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+*  General Public License for           Générale Publique GNU Affero
+*  more details.                        pour plus de détails.
+*
+*  You should have received             Vous devriez avoir reçu une
+*  a copy of the GNU Affero             copie de la Licence Générale
+*  General Public License along         Publique GNU Affero avec
+*  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+*                                       <http://www.gnu.org/licenses/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
+package ca.nrc.cadc.ac.server;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.Role;
+
+/**
+ *
+ * @author pdowler
+ */
+public interface GroupDetailSelector 
+{
+    /** 
+     * Check if group details should be filled in for the group when
+     * querying for role.
+     * @param g
+     * @param r
+     * @return true if group details should be filled
+     */
+    boolean isDetailedSearch(Group g, Role r);
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
index f88faffeb2851833b99c606de1f63e317703960f..070080806602418dc3d0b5cacfc8849e375b0869 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
@@ -68,13 +68,15 @@
  */
 package ca.nrc.cadc.ac.server;
 
-import java.security.AccessControlException;
-import java.security.Principal;
-import java.util.Map;
-
-import ca.nrc.cadc.ac.*;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserAlreadyExistsException;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.net.TransientException;
 
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.Collection;
 
 public interface UserPersistence<T extends Principal>
 {
@@ -85,7 +87,7 @@ public interface UserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    Map<String, PersonalDetails> getUsers()
+    Collection<User<Principal>> getUsers()
             throws TransientException, AccessControlException;
     
     /**
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java
index affb8c52a77107c2bc02eb85189be23281fc0dcd..dae9d245d651331b3a0994cbbfa5d63141c60eaf 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java
@@ -94,7 +94,7 @@ import java.util.Set;
 public abstract class LdapDAO
 {
 	private static final Logger logger = Logger.getLogger(LdapDAO.class);
-	
+
     private LDAPConnection conn;
 
     LdapConfig config;
@@ -186,12 +186,12 @@ public abstract class LdapDAO
                 }
                 if (p instanceof NumericPrincipal)
                 {
-                    ldapField = "(&(objectclass=cadcaccount)(numericid=" + p.getName() + "))";
+                    ldapField = "(numericid=" + p.getName() + ")";
                     break;
                 }
                 if (p instanceof X500Principal)
                 {
-                    ldapField = "(&(objectclass=cadcaccount)(distinguishedname=" + p.getName() + "))";
+                    ldapField = "(distinguishedname=" + p.getName() + ")";
                     break;
                 }
                 if (p instanceof OpenIdPrincipal)
@@ -234,7 +234,11 @@ public abstract class LdapDAO
             throws TransientException
     {
     	logger.debug("Ldap result: " + code);
-        System.out.println("Ldap result: " + code);
+
+    	if (code == ResultCode.SUCCESS || code == ResultCode.NO_SUCH_OBJECT)
+        {
+            return;
+        }
 
         if (code == ResultCode.INSUFFICIENT_ACCESS_RIGHTS)
         {
@@ -244,10 +248,6 @@ public abstract class LdapDAO
         {
             throw new AccessControlException("Invalid credentials ");
         }
-        else if ((code == ResultCode.SUCCESS) || (code == ResultCode.NO_SUCH_OBJECT))
-        {
-            // all good. nothing to do
-        }
         else if (code == ResultCode.PARAM_ERROR)
         {
             throw new IllegalArgumentException("Error in Ldap parameters ");
@@ -256,10 +256,16 @@ public abstract class LdapDAO
         {
             throw new TransientException("Connection problems ");
         }
-        else
+        else if (code == ResultCode.TIMEOUT || code == ResultCode.TIME_LIMIT_EXCEEDED)
         {
-            throw new RuntimeException("Ldap error (" + code.getName() + ")");
+            throw new TransientException("ldap timeout");
         }
+        else if (code == ResultCode.INVALID_DN_SYNTAX)
+        {
+            throw new IllegalArgumentException("Invalid DN syntax");
+        }
+
+        throw new RuntimeException("Ldap error (" + code.getName() + ")");
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java
index 71ddf32316e6de250ab5ef5b9a9c2afdb26657b6..1cee54aed419269958d481b45a0614ffb1180692 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java
@@ -68,6 +68,18 @@
  */
 package ca.nrc.cadc.ac.server.ldap;
 
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.log4j.Logger;
+
 import ca.nrc.cadc.ac.ActivatedGroup;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
@@ -75,21 +87,44 @@ import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.Role;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.GroupDetailSelector;
 import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.StringUtil;
-import com.unboundid.ldap.sdk.*;
-import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
-import org.apache.log4j.Logger;
 
-import javax.security.auth.x500.X500Principal;
-import java.security.AccessControlException;
-import java.security.Principal;
-import java.util.*;
+import com.unboundid.ldap.sdk.AddRequest;
+import com.unboundid.ldap.sdk.Attribute;
+import com.unboundid.ldap.sdk.DN;
+import com.unboundid.ldap.sdk.Filter;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPResult;
+import com.unboundid.ldap.sdk.LDAPSearchException;
+import com.unboundid.ldap.sdk.Modification;
+import com.unboundid.ldap.sdk.ModificationType;
+import com.unboundid.ldap.sdk.ModifyRequest;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchRequest;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
 
 public class LdapGroupDAO<T extends Principal> extends LdapDAO
 {
     private static final Logger logger = Logger.getLogger(LdapGroupDAO.class);
-    
+
+    private static final String[] PUB_GROUP_ATTRS = new String[]
+    {
+        "entrydn", "cn"
+    };
+    private static final String[] GROUP_ATTRS = new String[]
+    {
+        "entrydn", "cn", "nsaccountlock", "owner", "modifytimestamp", "description"
+    };
+    private static final String[] GROUP_AND_MEMBER_ATTRS = new String[]
+    {
+        "entrydn", "cn", "nsaccountlock", "owner", "modifytimestamp", "description", "uniquemember"
+    };
+
     private LdapUserDAO<T> userPersist;
 
     public LdapGroupDAO(LdapConfig config, LdapUserDAO<T> userPersist)
@@ -105,33 +140,33 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
 
     /**
      * Persists a group.
-     * 
+     *
      * @param group The group to create
-     * 
+     *
      * @return created group
-     * 
-     * @throws GroupAlreadyExistsException If a group with the same ID already 
+     *
+     * @throws GroupAlreadyExistsException If a group with the same ID already
      *                                     exists.
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws UserNotFoundException If owner or a member not valid user.
-     * @throws GroupNotFoundException 
+     * @throws GroupNotFoundException
      */
     public Group addGroup(final Group group)
         throws GroupAlreadyExistsException, TransientException,
-               UserNotFoundException, AccessControlException, 
+               UserNotFoundException, AccessControlException,
                GroupNotFoundException
     {
         if (group.getOwner() == null)
         {
             throw new IllegalArgumentException("Group owner must be specified");
         }
-        
+
         if (!group.getProperties().isEmpty())
         {
             throw new UnsupportedOperationException(
                     "Support for groups properties not available");
         }
-        
+
         if (!isCreatorOwner(group.getOwner()))
         {
             throw new AccessControlException("Group owner must be creator");
@@ -146,22 +181,22 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             }
             else
             {
-                
+
                 DN ownerDN = userPersist.getUserDN(group.getOwner());
-                
+
                 // add group to groups tree
-                LDAPResult result = addGroup(getGroupDN(group.getID()), 
-                                             group.getID(), ownerDN, 
-                                             group.description, 
-                                             group.getUserMembers(), 
+                LDAPResult result = addGroup(getGroupDN(group.getID()),
+                                             group.getID(), ownerDN,
+                                             group.description,
+                                             group.getUserMembers(),
                                              group.getGroupMembers());
                 LdapDAO.checkLdapResult(result.getResultCode());
-                
+
                 // add group to admin groups tree
-                result = addGroup(getAdminGroupDN(group.getID()), 
-                                  group.getID(), ownerDN, 
-                                  group.description, 
-                                  group.getUserAdmins(), 
+                result = addGroup(getAdminGroupDN(group.getID()),
+                                  group.getID(), ownerDN,
+                                  group.description,
+                                  group.getUserAdmins(),
                                   group.getGroupAdmins());
                 LdapDAO.checkLdapResult(result.getResultCode());
                 // AD: Search results sometimes come incomplete if
@@ -182,24 +217,24 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         	logger.debug("addGroup Exception: " + e, e);
             LdapDAO.checkLdapResult(e.getResultCode());
             throw new RuntimeException("Unexpected LDAP exception", e);
-        } 
+        }
     }
-    
+
     private LDAPResult addGroup(final DN groupDN, final String groupID,
-                                final DN ownerDN, final String description, 
-                                final Set<User<? extends Principal>> users, 
+                                final DN ownerDN, final String description,
+                                final Set<User<? extends Principal>> users,
                                 final Set<Group> groups)
-        throws UserNotFoundException, LDAPException, TransientException, 
+        throws UserNotFoundException, LDAPException, TransientException,
         AccessControlException, GroupNotFoundException
     {
         // add new group
         List<Attribute> attributes = new ArrayList<Attribute>();
-        Attribute ownerAttribute = 
+        Attribute ownerAttribute =
                         new Attribute("owner", ownerDN.toNormalizedString());
         attributes.add(ownerAttribute);
         attributes.add(new Attribute("objectClass", "groupofuniquenames"));
         attributes.add(new Attribute("cn", groupID));
-        
+
         if (StringUtil.hasText(description))
         {
             attributes.add(new Attribute("description", description));
@@ -223,7 +258,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         if (!members.isEmpty())
         {
-            attributes.add(new Attribute("uniquemember", 
+            attributes.add(new Attribute("uniquemember",
                 (String[]) members.toArray(new String[members.size()])));
         }
 
@@ -232,10 +267,11 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
                 new ProxiedAuthorizationV2RequestControl(
                         "dn:" + getSubjectDN().toNormalizedString()));
 
+        logger.debug("addGroup: " + groupDN);
         return getConnection().add(addRequest);
     }
-    
-    
+
+
     /**
      * Checks whether group name available for the user or already in use.
      * @param group
@@ -244,7 +280,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
      * @throws UserNotFoundException
      * @throws GroupNotFoundException
      * @throws TransientException
-     * @throws GroupAlreadyExistsException 
+     * @throws GroupAlreadyExistsException
      */
     private Group reactivateGroup(final Group group)
         throws AccessControlException, UserNotFoundException,
@@ -252,22 +288,20 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
     {
         try
         {
-            // check group name exists           
+            // check group name exists
             Filter filter = Filter.createEqualityFilter("cn", group.getID());
 
-            SearchRequest searchRequest = 
-                    new SearchRequest(
-                            getGroupDN(group.getID())
-                            .toNormalizedString(), SearchScope.SUB, filter, 
+            DN groupDN = getGroupDN(group.getID());
+            SearchRequest searchRequest =
+                    new SearchRequest(groupDN.toNormalizedString(), SearchScope.BASE, filter,
                                       new String[] {"nsaccountlock"});
 
             searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
                             getSubjectDN().toNormalizedString()));
 
-            SearchResultEntry searchResult = 
-                    getConnection().searchForEntry(searchRequest);
-            
+            SearchResultEntry searchResult = getConnection().searchForEntry(searchRequest);
+
             if (searchResult == null)
             {
                 return null;
@@ -277,18 +311,18 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             {
                 throw new GroupAlreadyExistsException("Group already exists " + group.getID());
             }
-            
-            // activate group            
+
+            // activate group
             try
             {
                 return modifyGroup(null, group, true);
-            } 
+            }
             catch (GroupNotFoundException e)
             {
                 throw new RuntimeException(
                         "BUG: group to modify does not exist" + group.getID());
-            }          
-        } 
+            }
+        }
         catch (LDAPException e)
         {
         	logger.debug("reactivateGroup Exception: " + e, e);
@@ -296,67 +330,72 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             throw new RuntimeException("Unexpected LDAP exception", e);
         }
     }
-    
-    
+
+
     /**
      * Get all group names.
-     * 
+     *
      * @return A collection of strings
-     * 
+     *
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
-    public Collection<String> getGroupNames() throws TransientException
+    public Collection<String> getGroupNames()
+        throws TransientException
     {
         try
         {
-            final Filter filter = Filter.createPresenceFilter("cn");
-            final String [] attributes = new String[] {"cn", "nsaccountlock"};
-            final Collection<String> groupNames = new ArrayList<String>();
-            final long begin = System.currentTimeMillis();
+            Filter filter = Filter.createPresenceFilter("cn");
+            String [] attributes = new String[] {"cn", "nsaccountlock"};
+
+            SearchRequest searchRequest =
+                    new SearchRequest(config.getGroupsDN(),
+                                      SearchScope.SUB, filter, attributes);
 
-            final SearchResult searchResult =
-                    getConnection().search(new SearchResultListener()
+            SearchResult searchResult = null;
+            try
+            {
+                searchResult = getConnection().search(searchRequest);
+            }
+            catch (LDAPSearchException e)
             {
-                @Override
-                public void searchEntryReturned(
-                        final SearchResultEntry searchEntry)
+                logger.debug("Could not find groups root", e);
+                LdapDAO.checkLdapResult(e.getResultCode());
+                if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
                 {
-                    groupNames.add(searchEntry.getAttributeValue("cn"));
+                    throw new IllegalStateException("Could not find groups root");
                 }
 
-                @Override
-                public void searchReferenceReturned(
-                        final SearchResultReference searchReference)
-                {
-
-                }
-            }, config.getGroupsDN(), SearchScope.ONE, filter, attributes);
+                throw new IllegalStateException("unexpected failure", e);
+            }
 
             LdapDAO.checkLdapResult(searchResult.getResultCode());
-            long end = System.currentTimeMillis();
+            List<String> groupNames = new ArrayList<String>();
+            for (SearchResultEntry next : searchResult.getSearchEntries())
+            {
+                if (!next.hasAttribute("nsaccountlock"))
+                {
+                    groupNames.add(next.getAttributeValue("cn"));
+                }
+            }
 
-            logger.info("<-- groupNames in " + ((new Long(end).doubleValue()
-                                                 - new Long(begin).doubleValue())
-                                                / 1000.0) + " seconds.");
             return groupNames;
         }
         catch (LDAPException e1)
         {
-        	logger.debug("getGroupNames Exception: " + e1, e1);
+            logger.debug("getGroupNames Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
-            throw new IllegalStateException("Unexpected exception: "
-                                            + e1.getMatchedDN(), e1);
+            throw new IllegalStateException("Unexpected exception: " + e1.getMatchedDN(), e1);
         }
-        
+
     }
 
     /**
-     * Get the group with the given Group ID.
-     * 
+     * Get the group with members.
+     *
      * @param groupID The Group unique ID.
-     * 
+     *
      * @return A Group instance
-     * 
+     *
      * @throws GroupNotFoundException If the group was not found.
      * @throws TransientException  If an temporary, unexpected problem occurred.
      */
@@ -364,185 +403,96 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         throws GroupNotFoundException, TransientException,
                AccessControlException
     {
-        return getGroup(groupID, true);
-    }
-    
-    public Group getGroup(final String groupID, final boolean withMembers)
-        throws GroupNotFoundException, TransientException,
-               AccessControlException
-    {
-        Group group = getGroup(getGroupDN(groupID), groupID, true);
-        
-        Group adminGroup = getAdminGroup(getAdminGroupDN(groupID), groupID, 
-                                         true);
-        
+        Group group = getGroup(getGroupDN(groupID), groupID, GROUP_AND_MEMBER_ATTRS);
+
+        Group adminGroup = getGroup(getAdminGroupDN(groupID), null, GROUP_AND_MEMBER_ATTRS);
+
         group.getGroupAdmins().addAll(adminGroup.getGroupMembers());
         group.getUserAdmins().addAll(adminGroup.getUserMembers());
+
         return group;
     }
-    
-    private Group getGroup(final DN groupDN, final String groupID, 
-                           final boolean withMembers)
-        throws GroupNotFoundException, TransientException, 
-               AccessControlException
-    {
-        String [] attributes = new String[] {"entrydn", "cn", "description", 
-                                             "owner", "uniquemember", 
-                                             "modifytimestamp", "nsaccountlock"};
-        return getGroup(groupDN, groupID, withMembers, attributes);
-    }
-    
-    private Group getAdminGroup(final DN groupDN, final String groupID, 
-                                final boolean withMembers)
-        throws GroupNotFoundException, TransientException, 
-               AccessControlException
-    {
-        String [] attributes = new String[] {"entrydn", "cn", "owner",
-                                             "uniquemember"};
-        return getGroup(groupDN, groupID, withMembers, attributes);
-    }
 
-    private Group getGroup(final DN groupDN, final String groupID, 
-                           final boolean withMembers, final String[] attributes)
-        throws GroupNotFoundException, TransientException, 
+    // groupID is here so exceptions and loggiong have plain groupID instead of DN
+    private Group getGroup(final DN groupDN, final String xgroupID, String[] attributes)
+        throws GroupNotFoundException, TransientException,
                AccessControlException
     {
+        logger.debug("getGroup: " + groupDN + " attrs: " + attributes.length);
+        String loggableGroupID = xgroupID;
+        if (loggableGroupID == null)
+            loggableGroupID = groupDN.toString(); // member or admin group: same name, internal tree
+
         try
         {
-            Filter filter = Filter.createEqualityFilter("cn", groupID);
-            
-            SearchRequest searchRequest = 
-                    new SearchRequest(groupDN.toNormalizedString(), 
-                                      SearchScope.SUB, filter, attributes);
+            Filter filter = Filter.createNOTFilter(Filter.createPresenceFilter("nsaccountlock"));
+
+            SearchRequest searchRequest =
+                    new SearchRequest(groupDN.toNormalizedString(),
+                                      SearchScope.BASE, filter, attributes);
 
             searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
                             getSubjectDN().toNormalizedString()));
 
-            SearchResult searchResult = null;
-            try
-            {
-                searchResult = getConnection().search(searchRequest);
-            }
-            catch (LDAPSearchException e)
-            {
-                if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
-                {
-                    String msg = "Group not found " + groupID;
-                    logger.debug(msg);
-                    throw new GroupNotFoundException(groupID);
-                }
-                else
-                {
-                    LdapDAO.checkLdapResult(e.getResultCode());
-                }
-            }
-            
-            if (searchResult.getEntryCount() == 0)
-            {
-                LdapDAO.checkLdapResult(searchResult.getResultCode());
-                //access denied
-                String msg = "Not authorized to access " + groupID;
-                logger.debug(msg);
-                throw new AccessControlException(groupID);
-            }
-            
-            if (searchResult.getEntryCount() >1)
-            {
-                throw new RuntimeException("BUG: multiple results when retrieving group " + groupID);
-            }
-            
-            SearchResultEntry searchEntry = searchResult.getSearchEntries().get(0);
-            
-            if (searchEntry.getAttribute("nsaccountlock") != null)
-            {
-                // deleted group
-                String msg = "Group not found " + groupID;
-                logger.debug(msg);
-                throw new GroupNotFoundException(groupID);
-            }
-            
-            DN groupOwner = searchEntry.getAttributeValueAsDN("owner");
-            if (groupOwner == null)
-            {
-                //TODO assume user not allowed to read group
-                throw new AccessControlException(groupID);
-            }
-            
-            User<X500Principal> owner;
-            try
-            {
-                owner = userPersist.getX500User(groupOwner);
-            }
-            catch (UserNotFoundException e)
-            {
-                throw new RuntimeException("BUG: group owner not found");
-            }
-            
-            Group ldapGroup = new Group(groupID, owner);
-            if (searchEntry.hasAttribute("description"))
-            {
-                ldapGroup.description = 
-                        searchEntry.getAttributeValue("description");
-            }
-            if (searchEntry.hasAttribute("modifytimestamp"))
+
+            SearchResultEntry searchEntry = getConnection().searchForEntry(searchRequest);
+
+            if (searchEntry == null)
             {
-                ldapGroup.lastModified = 
-                        searchEntry.getAttributeValueAsDate("modifytimestamp");
+                String msg = "Group not found " + loggableGroupID;
+                logger.debug(msg + " cause: null");
+                throw new GroupNotFoundException(loggableGroupID);
             }
 
-            if (withMembers)
+            Group ldapGroup = createGroupFromEntry(searchEntry, attributes);
+
+            if (searchEntry.getAttributeValues("uniquemember") != null)
             {
-                if (searchEntry.getAttributeValues("uniquemember") != null)
+                for (String member : searchEntry.getAttributeValues("uniquemember"))
                 {
-                    for (String member : searchEntry
-                            .getAttributeValues("uniquemember"))
+                    DN memberDN = new DN(member);
+                    if (memberDN.isDescendantOf(config.getUsersDN(), false))
                     {
-                        DN memberDN = new DN(member);
-                        if (memberDN.isDescendantOf(config.getUsersDN(), false))
+                        User<X500Principal> user;
+                        try
                         {
-                            User<X500Principal> user;
-                            try
-                            {
-                                user = userPersist.getX500User(memberDN);
-                            }
-                            catch (UserNotFoundException e)
-                            {
-                                throw new RuntimeException(
-                                    "BUG: group member not found");
-                            }
+                            user = userPersist.getX500User(memberDN);
                             ldapGroup.getUserMembers().add(user);
                         }
-                        else if (memberDN.isDescendantOf(config.getGroupsDN(),
-                                                         false))
+                        catch (UserNotFoundException e)
                         {
-                            try
-                            {
-                                ldapGroup.getGroupMembers().
-                                    add(new Group(getGroupID(memberDN)));
-                            }
-                            catch(GroupNotFoundException e)
-                            {
-                                // ignore as we are not cleaning up
-                                // deleted groups from the group members
-                            }
+                            // ignore as we do not cleanup deleted users
+                            // from groups they belong to
                         }
-                        else
+                    }
+                    else if (memberDN.isDescendantOf(config.getGroupsDN(), false))
+                    {
+                        try
+                        {
+                            ldapGroup.getGroupMembers().add(getGroup(memberDN, null, PUB_GROUP_ATTRS));
+                        }
+                        catch(GroupNotFoundException e)
                         {
-                            throw new RuntimeException(
-                                "BUG: unknown member DN type: " + memberDN);
+                            // ignore as we are not cleaning up
+                            // deleted groups from the group members
                         }
                     }
+                    else
+                    {
+                        throw new RuntimeException(
+                            "BUG: unknown member DN type: " + memberDN);
+                    }
                 }
             }
-            
+
             return ldapGroup;
         }
         catch (LDAPException e1)
         {
-        	logger.debug("getGroup Exception: " + e1, e1);
+            logger.debug("getGroup Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
-            throw new GroupNotFoundException("Not found " + groupID);
+            throw new RuntimeException("BUG: checkLdapResult didn't throw an exception");
         }
     }
 
@@ -550,9 +500,9 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
      * Modify the given group.
      *
      * @param group The group to update. It must be an existing group
-     * 
+     *
      * @return The newly updated group.
-     * 
+     *
      * @throws GroupNotFoundException If the group was not found.
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
@@ -563,9 +513,9 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
                AccessControlException, UserNotFoundException
     {
         Group existing = getGroup(group.getID()); //group must exists first
-        return modifyGroup(existing, group, false); 
+        return modifyGroup(existing, group, false);
     }
-    
+
     private Group modifyGroup(final Group existing, final Group group, boolean withActivate)
         throws UserNotFoundException, TransientException,
                AccessControlException, GroupNotFoundException
@@ -575,7 +525,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             throw new UnsupportedOperationException(
                     "Support for groups properties not available");
         }
-        
+
         boolean adminChanges = false;
 
         List<Modification> mods = new ArrayList<Modification>();
@@ -595,90 +545,89 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         {
             mods.add(new Modification(ModificationType.REPLACE, "description", group.description));
         }
-        
-        Set<String> newMembers = new HashSet<String>();
-        for (User<?> member : group.getUserMembers())
-        {
-            DN memberDN = userPersist.getUserDN(member);
-            newMembers.add(memberDN.toNormalizedString());
-        }
-        for (Group gr : group.getGroupMembers())
+        try
         {
-            if (!checkGroupExists(gr.getID()))
+            Set<String> newMembers = new HashSet<String>();
+            for (User<?> member : group.getUserMembers())
             {
-                throw new GroupNotFoundException(gr.getID());
+                DN memberDN = userPersist.getUserDN(member);
+                newMembers.add(memberDN.toNormalizedString());
             }
-            DN grDN = getGroupDN(gr.getID());
-            newMembers.add(grDN.toNormalizedString());
-        }
-        
-        Set<String> newAdmins = new HashSet<String>();
-        Set<User<? extends Principal>> existingUserAdmins = new HashSet<User<? extends Principal>>(0);
-        if (existing != null)
-        {
-        	existingUserAdmins = existing.getUserAdmins();
-        }
-        for (User<?> member : group.getUserAdmins())
-        {
-        	DN memberDN = userPersist.getUserDN(member);
-        	newAdmins.add(memberDN.toNormalizedString());
-        	if (!existingUserAdmins.contains(member))
+            for (Group gr : group.getGroupMembers())
             {
-            	adminChanges = true;
+                if (!checkGroupExists(gr.getID()))
+                {
+                    throw new GroupNotFoundException(gr.getID());
+                }
+                DN grDN = getGroupDN(gr.getID());
+                newMembers.add(grDN.toNormalizedString());
             }
-        }
-        
-        Set<Group> existingGroupAdmins = new HashSet<Group>(0);
-        if (existing != null)
-        {
-        	existingGroupAdmins = existing.getGroupAdmins();
-        }
-        for (Group gr : group.getGroupAdmins())
-        {
-            if (!checkGroupExists(gr.getID()))
+
+            Set<String> newAdmins = new HashSet<String>();
+            Set<User<? extends Principal>> existingUserAdmins = new HashSet<User<? extends Principal>>(0);
+            if (existing != null)
             {
-                throw new GroupNotFoundException(gr.getID());
+                    existingUserAdmins = existing.getUserAdmins();
+            }
+            for (User<?> member : group.getUserAdmins())
+            {
+                    DN memberDN = userPersist.getUserDN(member);
+                    newAdmins.add(memberDN.toNormalizedString());
+                    if (!existingUserAdmins.contains(member))
+                {
+                    adminChanges = true;
+                }
             }
 
-        	DN grDN = getGroupDN(gr.getID());
-        	newAdmins.add(grDN.toNormalizedString());
-        	if (!existingGroupAdmins.contains(gr))
+            Set<Group> existingGroupAdmins = new HashSet<Group>(0);
+            if (existing != null)
             {
-            	adminChanges = true;
+                    existingGroupAdmins = existing.getGroupAdmins();
+            }
+            for (Group gr : group.getGroupAdmins())
+            {
+                if (!checkGroupExists(gr.getID()))
+                {
+                    throw new GroupNotFoundException(gr.getID());
+                }
+
+                    DN grDN = getGroupDN(gr.getID());
+                    newAdmins.add(grDN.toNormalizedString());
+                    if (!existingGroupAdmins.contains(gr))
+                {
+                    adminChanges = true;
+                }
+            }
+
+            mods.add(new Modification(ModificationType.REPLACE, "uniquemember",
+                    (String[]) newMembers.toArray(new String[newMembers.size()])));
+            adminMods.add(new Modification(ModificationType.REPLACE, "uniquemember",
+                    (String[]) newAdmins.toArray(new String[newAdmins.size()])));
+
+            // modify admin group first (if necessary)
+            if (adminChanges)
+            {
+            ModifyRequest modifyRequest = new ModifyRequest(getAdminGroupDN(group.getID()), adminMods);
+
+                modifyRequest.addControl(
+                        new ProxiedAuthorizationV2RequestControl(
+                                "dn:" + getSubjectDN().toNormalizedString()));
+
+                LdapDAO.checkLdapResult(getConnection().modify(modifyRequest).getResultCode());
             }
-        }
 
-        mods.add(new Modification(ModificationType.REPLACE, "uniquemember", 
-                (String[]) newMembers.toArray(new String[newMembers.size()])));
-        adminMods.add(new Modification(ModificationType.REPLACE, "uniquemember", 
-                (String[]) newAdmins.toArray(new String[newAdmins.size()])));
-        
-        try
-        {
-        	// modify admin group first (if necessary)
-        	if (adminChanges)
-        	{   
-                ModifyRequest modifyRequest = new ModifyRequest(getAdminGroupDN(group.getID()), adminMods);
-                
-	            modifyRequest.addControl(
-	                    new ProxiedAuthorizationV2RequestControl(
-	                            "dn:" + getSubjectDN().toNormalizedString()));
-	            LdapDAO.checkLdapResult(getConnection().
-	                    modify(modifyRequest).getResultCode());
-        	}
-            
             // modify the group itself now
-        	ModifyRequest modifyRequest = new ModifyRequest(getGroupDN(group.getID()), mods);
+            ModifyRequest modifyRequest = new ModifyRequest(getGroupDN(group.getID()), mods);
 
             modifyRequest.addControl(
                     new ProxiedAuthorizationV2RequestControl(
                             "dn:" + getSubjectDN().toNormalizedString()));
-            LdapDAO.checkLdapResult(getConnection().
-                    modify(modifyRequest).getResultCode());
+
+            LdapDAO.checkLdapResult(getConnection().modify(modifyRequest).getResultCode());
         }
         catch (LDAPException e1)
         {
-        	logger.debug("Modify Exception: " + e1, e1);
+            logger.debug("Modify Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
         }
         try
@@ -694,16 +643,15 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (GroupNotFoundException e)
         {
-            throw new RuntimeException(
-                    "BUG: modified group not found (" + group.getID() + ")");
+            throw new RuntimeException("BUG: modified group not found (" + group.getID() + ")");
         }
     }
 
     /**
      * Deletes the group.
-     * 
+     *
      * @param groupID The group to delete
-     * 
+     *
      * @throws GroupNotFoundException If the group was not found.
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
@@ -714,19 +662,19 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         deleteGroup(getGroupDN(groupID), groupID, false);
         deleteGroup(getAdminGroupDN(groupID), groupID, true);
     }
-    
-    private void deleteGroup(final DN groupDN, final String groupID, 
+
+    private void deleteGroup(final DN groupDN, final String groupID,
                              final boolean isAdmin)
         throws GroupNotFoundException, TransientException,
                AccessControlException
     {
-        Group group = getGroup(groupDN, groupID, true);
+        Group group = getGroup(groupDN, groupID, GROUP_AND_MEMBER_ATTRS);
         List<Modification> modifs = new ArrayList<Modification>();
         modifs.add(new Modification(ModificationType.ADD, "nsaccountlock", "true"));
-        
+
         if (isAdmin)
         {
-            if (!group.getGroupAdmins().isEmpty() || 
+            if (!group.getGroupAdmins().isEmpty() ||
                 !group.getUserAdmins().isEmpty())
             {
                 modifs.add(new Modification(ModificationType.DELETE, "uniquemember"));
@@ -734,7 +682,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         else
         {
-            if (!group.getGroupMembers().isEmpty() || 
+            if (!group.getGroupMembers().isEmpty() ||
                 !group.getUserMembers().isEmpty())
             {
                 modifs.add(new Modification(ModificationType.DELETE, "uniquemember"));
@@ -752,35 +700,32 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (LDAPException e1)
         {
-        	logger.debug("Delete Exception: " + e1, e1);
+            logger.debug("Delete Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
         }
-        
+
         try
         {
-            getGroup(group.getID());
-            throw new RuntimeException("BUG: group not deleted " + 
-                                       group.getID());
+            getGroup(getGroupDN(group.getID()), null, GROUP_ATTRS);
+            throw new RuntimeException("BUG: group not deleted " + group.getID());
         }
-        catch (GroupNotFoundException ignore) {}
+        catch (GroupNotFoundException ignore) { }
     }
-    
+
     /**
-     * Obtain a Collection of Groups that fit the given query.
-     * 
+     * Obtain a Collection of Groups that fit the given query. The returned groups
+     * will not include members.
+     *
      * @param userID The userID.
      * @param role Role of the user, either owner, member, or read/write.
      * @param groupID The Group ID.
-     * 
-     * @return Collection of Groups
-     *         matching GROUP_READ_ACI.replace(ACTUAL_GROUP_TOKEN,
-     *         readGrDN.toNormalizedString()) the query, or empty
-     *         Collection. Never null.
+     *
+     * @return possibly empty collection of Group that match the query
      * @throws TransientException  If an temporary, unexpected problem occurred.
      * @throws UserNotFoundException
      * @throws GroupNotFoundException
      */
-    public Collection<Group> getGroups(final T userID, final Role role, 
+    public Collection<Group> getGroups(final T userID, final Role role,
                                        final String groupID)
         throws TransientException, AccessControlException,
                GroupNotFoundException, UserNotFoundException
@@ -796,107 +741,162 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             // no anonymous searches
             throw new AccessControlException("Not authorized to search");
         }
-        
-        Collection<DN> groupDNs = new HashSet<DN>();
+
+        Collection<Group> ret;
         if (role == Role.OWNER)
         {
-            groupDNs.addAll(getOwnerGroups(user, userDN, groupID));
-        }
-        else if (role == Role.MEMBER)
-        {
-            groupDNs.addAll(getMemberGroups(user, userDN, groupID, false));
-        }
-        else if (role == Role.ADMIN)
-        {
-            groupDNs.addAll(getMemberGroups(user, userDN, groupID, true));
+            ret = getOwnerGroups(user, userDN, groupID);
         }
-        
-        if (logger.isDebugEnabled())
+        else
         {
-            for (DN dn : groupDNs)
+            Collection<DN> groupDNs = null;
+
+            if (role == Role.MEMBER)
             {
-                logger.debug("Search adding DN: " + dn);
+                groupDNs = getMemberGroups(user, userDN, groupID, false);
             }
-        }
-        
-        Collection<Group> groups = new HashSet<Group>();
-        try
-        {
-            for (DN groupDN : groupDNs)
+            else if (role == Role.ADMIN)
             {
-                if (role == Role.ADMIN)
-                {
-                    groupDN = new DN(groupDN.getRDNString() + "," + config.getGroupsDN());
-                }
-                try
-                {
-                    groups.add(getGroup(groupDN));
-                    logger.debug("Search adding group: " + groupDN);
-                }
-                catch (GroupNotFoundException e)
+                groupDNs = getMemberGroups(user, userDN, groupID, true);
+            }
+            else
+                throw new IllegalArgumentException("null role");
+
+            ret = new ArrayList<Group>();
+            try
+            {
+                for (DN groupDN : groupDNs)
                 {
-                    final String message = "BUG: group " + groupDN + " not found but " +
-                                           "membership exists (" + userID + ")";
-                    logger.error(message);
-                    //throw new IllegalStateException(message);
+                    if (role == Role.ADMIN)
+                    {
+                        groupDN = new DN(groupDN.getRDNString() + "," + config.getGroupsDN());
+                    }
+                    try
+                    {
+                        Group g = createGroupFromDN(groupDN);
+                        if (isDetailedSearch(g, role))
+                        {
+                            g = getGroup(groupDN, null, GROUP_ATTRS);
+                        }
+                            logger.debug("found group: " + g.getID());
+                            ret.add(g);
+                    }
+                    catch (GroupNotFoundException e)
+                    {
+                        final String message = "BUG: group " + groupDN + " not found but " +
+                                               "membership exists (" + userID + ")";
+                        logger.error(message);
+                    }
                 }
             }
+            catch (LDAPException e)
+            {
+                logger.debug("getGroups Exception: " + e, e);
+                throw new TransientException("Error getting group", e);
+            }
         }
-        catch (LDAPException e)
+
+        logger.debug("found: " + ret.size() + "groups matching " + userID + "," + role + "," + groupID);
+        return ret;
+    }
+
+    // some pretty horrible hacks to avoid querying LDAP for group details...
+    private Group createGroupFromDN(DN groupDN)
+    {
+        String cn = groupDN.getRDNString();
+        String[] parts = cn.split("=");
+        if (parts.length == 2 && parts[0].equals("cn"))
         {
-        	logger.debug("getGroups Exception: " + e, e);
-            throw new TransientException("Error getting group", e);
+            return new Group(parts[1]);
         }
-        return groups;
+        throw new RuntimeException("BUG: failed to extract group name from " + groupDN.toString());
     }
-    
-    protected Collection<DN> getOwnerGroups(final User<T> user, 
+    // this gets filled by the LdapgroupPersistence
+    GroupDetailSelector searchDetailSelector;
+
+    private boolean isDetailedSearch(Group g, Role r)
+    {
+        if (searchDetailSelector == null)
+            return true;
+        return searchDetailSelector.isDetailedSearch(g, r);
+    }
+    // end of horribleness
+
+    protected Collection<Group> getOwnerGroups(final User<T> user,
                                             final DN userDN,
                                             final String groupID)
-        throws TransientException, AccessControlException,
-               GroupNotFoundException, UserNotFoundException
+        throws TransientException, AccessControlException
     {
-        Collection<DN> groupDNs = new HashSet<DN>();
+        Collection<Group> ret = new ArrayList<Group>();
         try
-        {                           
-            Filter filter = Filter.createEqualityFilter("owner", 
-                                                        userDN.toString());
+        {
+            Filter filter = Filter.createNOTFilter(Filter.createPresenceFilter("nsaccountlock"));
+
+            filter = Filter.createANDFilter(filter,
+                Filter.createEqualityFilter("owner", userDN.toNormalizedString()));
+
             if (groupID != null)
             {
-                getGroup(groupID);
-                filter = Filter.createANDFilter(filter, 
-                                Filter.createEqualityFilter("cn", groupID));
+                DN groupDN = getGroupDN(groupID);
+                filter = Filter.createANDFilter(filter,
+                    Filter.createEqualityFilter("entrydn", groupDN.toNormalizedString()));
             }
-            
+
             SearchRequest searchRequest =  new SearchRequest(
-                    config.getGroupsDN(), SearchScope.SUB, filter, "entrydn", "nsaccountlock");
-            
+                    config.getGroupsDN(), SearchScope.SUB, filter, GROUP_ATTRS);
+
             searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
                             getSubjectDN().toNormalizedString()));
-            
+
             SearchResult results = getConnection().search(searchRequest);
             for (SearchResultEntry result : results.getSearchEntries())
             {
-                String entryDN = result.getAttributeValue("entrydn");
-                // make sure the group isn't deleted
-                if (result.getAttribute("nsaccountlock") == null)
-                {
-                    groupDNs.add(new DN(entryDN));
-                }
-                
+                ret.add(createGroupFromEntry(result, GROUP_ATTRS));
             }
         }
         catch (LDAPException e1)
         {
-        	logger.debug("getOwnerGroups Exception: " + e1, e1);
+            logger.debug("getOwnerGroups Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
         }
-        return groupDNs; 
+        return ret;
+    }
+
+    private Group createGroupFromEntry(SearchResultEntry result, String[] attributes)
+        throws LDAPException
+    {
+        if (result.getAttribute("nsaccountlock") != null)
+        {
+            throw new RuntimeException("BUG: found group with nsaccountlock set: " + result.getAttributeValue("entrydn").toString());
+        }
+
+        String entryDN = result.getAttributeValue("entrydn");
+        String groupName = result.getAttributeValue("cn");
+        if (attributes == PUB_GROUP_ATTRS)
+            return new Group(groupName);
+
+        DN ownerDN = result.getAttributeValueAsDN("owner");
+        if (ownerDN == null)
+            throw new AccessControlException(groupName);
+        try
+        {
+            User owner = userPersist.getX500User(ownerDN);
+            Group g = new Group(groupName, owner);
+            if (result.hasAttribute("description"))
+                g.description = result.getAttributeValue("description");
+            if (result.hasAttribute("modifytimestamp"))
+                g.lastModified = result.getAttributeValueAsDate("modifytimestamp");
+            return g;
+        }
+        catch(UserNotFoundException ex)
+        {
+            throw new RuntimeException("Invalid state: owner does not exist: " + ownerDN + " group: " + entryDN);
+        }
     }
-    
-    protected Collection<DN> getMemberGroups(final User<T> user, 
-                                             final DN userDN, 
+
+    protected Collection<DN> getMemberGroups(final User<T> user,
+                                             final DN userDN,
                                              final String groupID,
                                              final boolean isAdmin)
         throws TransientException, AccessControlException,
@@ -922,114 +922,17 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         else
         {
-            Collection<DN> memberGroupDNs = 
+            Collection<DN> memberGroupDNs =
                     userPersist.getUserGroups(user.getUserID(), isAdmin);
             groupDNs.addAll(memberGroupDNs);
         }
         return groupDNs;
     }
-    
-    /**
-     * Returns a group based on its LDAP DN. The returned group does not contain
-     * members or admins
-     * 
-     * @param groupDN
-     * @return
-     * @throws com.unboundid.ldap.sdk.LDAPException
-     * @throws ca.nrc.cadc.ac.GroupNotFoundException - if group does not exist,
-     * it's deleted or caller has no access to it.
-     */
-    protected Group getGroup(final DN groupDN)
-        throws LDAPException, GroupNotFoundException, UserNotFoundException
-    {
-        Filter filter = Filter.createEqualityFilter("entrydn", 
-                                                    groupDN.toNormalizedString());
-        
-        SearchRequest searchRequest =  new SearchRequest(
-                    config.getGroupsDN(), SearchScope.SUB, filter, 
-                    "cn", "description", "owner", "nsaccountlock");
-            
-        searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
-                            getSubjectDN().toNormalizedString()));
-            
-        SearchResultEntry searchResult = 
-                getConnection().searchForEntry(searchRequest);
 
-        if (searchResult == null)
-        {
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-        
-        if (searchResult.getAttribute("nsaccountlock") != null)
-        {
-            // deleted group
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-
-        Group group = new Group(searchResult.getAttributeValue("cn"),
-                                userPersist.getX500User(
-                                    new DN(searchResult.getAttributeValue(
-                                        "owner"))));
-        group.description = searchResult.getAttributeValue("description");
-        return group;
-    }
-
-    /**
-     * Returns a group ID corresponding to a DN. Although the groupID can be
-     * deduced from the group DN, this method checks if the group exists and
-     * it's active and throws an exception if any of those conditions are not
-     * met.
-     * 
-     * @param groupDN
-     * @return
-     * @throws com.unboundid.ldap.sdk.LDAPException
-     * @throws ca.nrc.cadc.ac.GroupNotFoundException - Group not found or not
-     * active
-     */
-    protected String getGroupID(final DN groupDN)
-        throws LDAPException, GroupNotFoundException
-    {
-        Filter filter = Filter.createEqualityFilter("entrydn", 
-                                                    groupDN.toNormalizedString());
-        
-        SearchRequest searchRequest =  new SearchRequest(
-                    config.getGroupsDN(), SearchScope.SUB, filter, 
-                    "cn", "nsaccountlock");
-            
-        searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
-                            getSubjectDN().toNormalizedString()));
-            
-        SearchResultEntry searchResult = 
-                getConnection().searchForEntry(searchRequest);
-
-        if (searchResult == null)
-        {
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-        
-        if (searchResult.getAttribute("nsaccountlock") != null)
-        {
-            // deleted group
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-
-        return searchResult.getAttributeValue("cn");
-    }
-    
     /**
-     * 
+     *
      * @param groupID
-     * @return 
+     * @return
      */
     protected DN getGroupDN(final String groupID) throws TransientException
     {
@@ -1039,16 +942,16 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (LDAPException e)
         {
-        	logger.debug("getGroupDN Exception: " + e, e);
+            logger.debug("getGroupDN Exception: " + e, e);
             LdapDAO.checkLdapResult(e.getResultCode());
         }
         throw new IllegalArgumentException(groupID + " not a valid group ID");
     }
-    
+
     /**
-     * 
+     *
      * @param groupID
-     * @return 
+     * @return
      */
     protected DN getAdminGroupDN(final String groupID) throws TransientException
     {
@@ -1058,24 +961,24 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (LDAPException e)
         {
-        	logger.debug("getAdminGroupDN Exception: " + e, e);
+            logger.debug("getAdminGroupDN Exception: " + e, e);
             LdapDAO.checkLdapResult(e.getResultCode());
         }
         throw new IllegalArgumentException(groupID + " not a valid group ID");
     }
-    
+
     /**
-     * 
+     *
      * @param owner
      * @return
-     * @throws UserNotFoundException 
+     * @throws UserNotFoundException
      */
     protected boolean isCreatorOwner(final User<? extends Principal> owner)
         throws UserNotFoundException
     {
         try
         {
-            User<X500Principal> subjectUser = 
+            User<X500Principal> subjectUser =
                     userPersist.getX500User(getSubjectDN());
             if (subjectUser.equals(owner))
             {
@@ -1089,18 +992,20 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             throw new RuntimeException(e);
         }
     }
-    
-    private boolean checkGroupExists(String groupID) 
-            throws TransientException
+
+    private boolean checkGroupExists(String groupID)
+            throws LDAPException, TransientException
     {
-        for (String groupName : getGroupNames())
+        try
         {
-            if (groupName.equalsIgnoreCase(groupID))
-            {
-                return true;
-            }
+            Group g = getGroup(getGroupDN(groupID), groupID, PUB_GROUP_ATTRS);
+            return true;
+        }
+        catch(GroupNotFoundException ex)
+        {
+            return false;
         }
-        return false;
+        finally { }
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java
index e66dc2e5960f289be4050c3d87429b0a3f93c38d..9f66002c0eb4cd86c8e328440456b3f54408a585 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java
@@ -79,6 +79,7 @@ import ca.nrc.cadc.ac.GroupAlreadyExistsException;
 import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.Role;
 import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.GroupDetailSelector;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.net.TransientException;
 
@@ -88,12 +89,19 @@ public class LdapGroupPersistence<T extends Principal>
     private static final Logger log = 
             Logger.getLogger(LdapGroupPersistence.class);
     private final LdapConfig config;
+    
+    private GroupDetailSelector detailSelector;
 
     public LdapGroupPersistence()
     {
         config = LdapConfig.getLdapConfig();
     }
     
+    protected void setDetailSelector(GroupDetailSelector gds)
+    {
+        this.detailSelector = gds;
+    }
+    
     public Collection<String> getGroupNames()
         throws TransientException, AccessControlException
     {
@@ -233,6 +241,7 @@ public class LdapGroupPersistence<T extends Principal>
         {
             userDAO = new LdapUserDAO<T>(config);
             groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            groupDAO.searchDetailSelector = detailSelector;
             Collection<Group> ret = groupDAO.getGroups(userID, role, groupID);
             return ret;
         }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
index 219728d8a32b0ae1eb1e05296c04bb87ae825d2c..5108b0828c2c1618262d3aa22e63eedc69057829 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
@@ -150,8 +150,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
     private String[] userAttribs = new String[]
             {
                     LDAP_FIRST_NAME, LDAP_LAST_NAME, LDAP_ADDRESS, LDAP_CITY,
-                    LDAP_COUNTRY,
-                    LDAP_EMAIL, LDAP_INSTITUTE
+                    LDAP_COUNTRY, LDAP_EMAIL, LDAP_INSTITUTE
             };
     private String[] memberAttribs = new String[]
             {
@@ -286,7 +285,16 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         DN userDN;
         try
         {
-            userDN = getUserRequestsDN(userRequest.getUser().getUserID().getName());
+            T userID = userRequest.getUser().getUserID();
+            try
+            {
+                getUser(userID, config.getUsersDN(), false);
+                throw new UserAlreadyExistsException(userID.getName() + " found in " +
+                                                     config.getUsersDN());
+            }
+            catch (UserNotFoundException ignore) {}
+
+            userDN = getUserRequestsDN(userID.getName());
             addUser(userRequest, userDN);
 
             // AD: Search results sometimes come incomplete if
@@ -294,7 +302,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
             getConnection().reconnect();
             try
             {
-                return getUser(userRequest.getUser().getUserID(), config.getUserRequestsDN());
+                return getUser(userID, config.getUserRequestsDN());
             }
             catch (UserNotFoundException e)
             {
@@ -444,7 +452,6 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         return getUser(userID, config.getUserRequestsDN());
     }
 
-
     /**
      * Get the user specified by userID.
      *
@@ -456,6 +463,24 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @throws AccessControlException If the operation is not permitted.
      */
     private User<T> getUser(final T userID, final String usersDN)
+        throws UserNotFoundException, TransientException,
+        AccessControlException
+    {
+        return getUser(userID, usersDN, true);
+    }
+
+    /**
+     * Get the user specified by userID.
+     *
+     * @param userID  The userID.
+     * @param usersDN The LDAP tree to search.
+     * @param proxy   If true proxy the request as the calling user.
+     * @return User instance.
+     * @throws UserNotFoundException  when the user is not found.
+     * @throws TransientException     If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    private User<T> getUser(final T userID, final String usersDN, boolean proxy)
             throws UserNotFoundException, TransientException,
                    AccessControlException
     {
@@ -466,8 +491,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
                     "Unsupported principal type " + userID.getClass());
         }
 
-        searchField = "(&(objectclass=inetorgperson)(objectclass=cadcaccount)(" +
-                      searchField + "=" + userID.getName() + "))";
+        searchField = "(" + searchField + "=" + userID.getName() + ")";
         logger.debug(searchField);
 
         SearchResultEntry searchResult = null;
@@ -476,7 +500,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
             SearchRequest searchRequest =
                     new SearchRequest(usersDN, SearchScope.SUB,
                                       searchField, userAttribs);
-            if (isSecure(usersDN))
+            if (proxy && isSecure(usersDN))
             {
                 searchRequest.addControl(
                         new ProxiedAuthorizationV2RequestControl(
@@ -539,11 +563,10 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @return A map of string keys to string values.
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
-    public Map<String, PersonalDetails> getUsers()
+    public Collection<User<Principal>> getUsers()
             throws TransientException
     {
-        final Map<String, PersonalDetails> users =
-                new HashMap<String, PersonalDetails>();
+        final Collection<User<Principal>> users = new ArrayList<User<Principal>>();
 
         try
         {
@@ -566,16 +589,15 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
                 {
                     if (!next.hasAttribute(LDAP_NSACCOUNTLOCK))
                     {
-                        final String trimmedFirstName =
+                        final String firstName =
                                 next.getAttributeValue(LDAP_FIRST_NAME).trim();
-                        final String trimmedLastName =
+                        final String lastName =
                                 next.getAttributeValue(LDAP_LAST_NAME).trim();
-                        final String trimmedUID =
-                                next.getAttributeValue(LDAP_UID).trim();
-
-                        users.put(trimmedUID,
-                                  new PersonalDetails(trimmedFirstName,
-                                                      trimmedLastName));
+                        final String uid =  next.getAttributeValue(LDAP_UID).trim();
+                        User<Principal> user = new User<Principal>(new HttpPrincipal(uid));
+                        PersonalDetails pd = new PersonalDetails(firstName, lastName);
+                        user.details.add(pd);
+                        users.add(user);
                     }
                 }
             }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
index 6a042cc3785193c354f28626a785e803c8190f88..fd8b41ec1abb427b629fc026f53eda34b767937a 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
@@ -75,6 +75,7 @@ import com.unboundid.ldap.sdk.DN;
 import java.security.AccessControlException;
 import java.security.Principal;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.log4j.Logger;
@@ -96,8 +97,8 @@ public class LdapUserPersistence<T extends Principal>
             logger.error("test/config/LdapConfig.properties file required.", e);
         }
     }
-    
-    public Map<String, PersonalDetails> getUsers()
+
+    public Collection<User<Principal>> getUsers()
         throws TransientException, AccessControlException
     {
         LdapUserDAO<T> userDAO = null;
@@ -125,7 +126,6 @@ public class LdapUserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    @Override
     public User<T> addUser(UserRequest<T> user)
         throws TransientException, AccessControlException,
                UserAlreadyExistsException
@@ -183,7 +183,6 @@ public class LdapUserPersistence<T extends Principal>
     * @throws TransientException     If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     */
-    @Override
     public User<T> getPendingUser(final T userID) throws UserNotFoundException,
                                                          TransientException,
                                                          AccessControlException
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/SyncOutput.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/SyncOutput.java
index 9ebd42f75c00d63f1ccc05a6cee320bb8e3b6d7f..1f67b9bf613c852c294515d673c4247b75c42dad 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/SyncOutput.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/SyncOutput.java
@@ -97,16 +97,18 @@ public class SyncOutput
 
     public void setCode(int code)
     {
+        log.debug("setting code");
         if (writer != null)
-            return;
+            throw new IllegalStateException("attempted to set code after writer has been opened");
 
         response.setStatus(code);
+        log.debug("set code " + code);
     }
 
     public void setHeader(String key, Object value)
     {
         if (writer != null)
-            return;
+            throw new IllegalStateException("attempted to set header after writer has been opened");
 
         if (value == null)
             response.setHeader(key, null);
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
index f576ec6ba47de6cf4cff2ee3e8fb394d142e3a32..80661d4573cc2e4efaac40fcbdc5e609de09053d 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
@@ -335,6 +335,23 @@ public class ACSearchRunner implements JobRunner
 //            }
         }
         */
+        catch(IllegalArgumentException ex)
+        {
+            logInfo.setSuccess(true);
+            logInfo.setMessage(ex.getMessage());
+            log.debug("FAIL", ex);
+            
+            syncOut.setResponseCode(400);
+            syncOut.setHeader("Content-Type", "text/plain");
+            try
+            {
+                syncOut.getOutputStream().write(ex.getMessage().getBytes());
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write response to output stream", e);
+            }
+        }
         catch (AccessControlException t)
         {
             logInfo.setSuccess(true);
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
index 2805eda54b4e7bebfe5f0fc719c84085651f6dfd..9a93cfecf3ead83a4ac28fe764ce58c7823470fc 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
@@ -68,19 +68,6 @@
  */
 package ca.nrc.cadc.ac.server.web.groups;
 
-import java.io.IOException;
-import java.security.AccessControlException;
-import java.security.Principal;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.List;
-
-import javax.security.auth.Subject;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.log4j.Logger;
-
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
 import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.MemberAlreadyExistsException;
@@ -91,6 +78,15 @@ import ca.nrc.cadc.ac.server.PluginFactory;
 import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.ac.server.web.SyncOutput;
 import ca.nrc.cadc.net.TransientException;
+import org.apache.log4j.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.List;
 
 public abstract class AbstractGroupAction implements PrivilegedExceptionAction<Object>
 {
@@ -208,18 +204,18 @@ public abstract class AbstractGroupAction implements PrivilegedExceptionAction<O
     private void sendError(int responseCode, String message)
     {
         syncOut.setHeader("Content-Type", "text/plain");
+        syncOut.setCode(responseCode);
         if (message != null)
         {
             try
             {
-                syncOut.getWriter() .write(message);
+                syncOut.getWriter().write(message);
             }
             catch (IOException e)
             {
                 log.warn("Could not write error message to output stream");
             }
         }
-        syncOut.setCode(responseCode);
     }
 
     <T extends Principal> GroupPersistence<T> getGroupPersistence()
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
index a4341c40aa82975b1aebf363794f30dc1a00d95c..5afabdb07782eeda62a83d49f68efb1ab6595ff6 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
@@ -99,7 +99,8 @@ public class CreateGroupAction extends AbstractGroupAction
         groupWriter.write(newGroup, syncOut.getWriter());
 
         List<String> addedMembers = null;
-        if ((newGroup.getUserMembers().size() > 0) || (newGroup.getGroupMembers().size() > 0))
+        if ((newGroup.getUserMembers().size() > 0) ||
+            (newGroup.getGroupMembers().size() > 0))
         {
             addedMembers = new ArrayList<String>();
             for (Group gr : newGroup.getGroupMembers())
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
index 096b5827a25ad73b5efcb91427879d0c3cd28943..fb9a2dbfff49f2851412f52663898931a3d5b695 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
@@ -80,7 +80,6 @@ import org.apache.log4j.Logger;
 
 import ca.nrc.cadc.ac.server.web.SyncOutput;
 import ca.nrc.cadc.auth.AuthenticationUtil;
-import ca.nrc.cadc.util.StringUtil;
 
 /**
  * Servlet for handling all requests to /groups
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionFactory.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionFactory.java
index 5cf7a054c70568d75fdddfa964a2a6334eb1c401..0009a53094c5be10b6ec77a68af06482f3959d2f 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionFactory.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionFactory.java
@@ -201,7 +201,6 @@ public abstract class GroupsActionFactory
                     }
                     sb.append(request.getContextPath());
                     sb.append(request.getServletPath());
-                    sb.append("/");
                     sb.append(path);
 
                     action = new ModifyGroupAction(groupName, sb.toString(), request.getInputStream());
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ModifyGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ModifyGroupAction.java
index f4094251a8e57c4f7a351ff40b12d357157df1ab..d42ca9b6525944989f454d2c16a17d1c6b1ca6dc 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ModifyGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ModifyGroupAction.java
@@ -133,7 +133,7 @@ public class ModifyGroupAction extends AbstractGroupAction
         }
         logGroupInfo(group.getID(), deletedMembers, addedMembers);
 
-        syncOut.setHeader("Location", "/" + group.getID());
+        syncOut.setHeader("Location", request);
         syncOut.setCode(303);
     }
 
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
index 07a83782a467cf199f1106d3a21df5481fa67561..af79d45ffa72d6f8adcbe4db50a154d006aca303 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
@@ -69,7 +69,9 @@
 package ca.nrc.cadc.ac.server.web.users;
 
 import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.ReaderException;
 import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserAlreadyExistsException;
 import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.ac.json.JsonUserListWriter;
@@ -94,7 +96,9 @@ import java.io.Writer;
 import java.security.AccessControlException;
 import java.security.Principal;
 import java.security.PrivilegedExceptionAction;
+import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -156,6 +160,13 @@ public abstract class AbstractUserAction implements PrivilegedExceptionAction<Ob
             this.logInfo.setMessage(message);
             sendError(400, message);
         }
+        catch (ReaderException e)
+        {
+            log.debug(e.getMessage(), e);
+            String message = e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(400, message);
+        }
         catch (UserNotFoundException e)
         {
             log.debug(e.getMessage(), e);
@@ -163,6 +174,13 @@ public abstract class AbstractUserAction implements PrivilegedExceptionAction<Ob
             this.logInfo.setMessage(message);
             sendError(404, message);
         }
+        catch (UserAlreadyExistsException e)
+        {
+            log.debug(e.getMessage(), e);
+            String message = "User not found: " + e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(409, message);
+        }
         catch (UnsupportedOperationException e)
         {
             log.debug(e.getMessage(), e);
@@ -196,19 +214,19 @@ public abstract class AbstractUserAction implements PrivilegedExceptionAction<Ob
 
     private void sendError(int responseCode, String message)
     {
+        syncOut.setCode(responseCode);
         syncOut.setHeader("Content-Type", "text/plain");
         if (message != null)
         {
             try
             {
-                syncOut.getWriter() .write(message);
+                syncOut.getWriter().write(message);
             }
             catch (IOException e)
             {
                 log.warn("Could not write error message to output stream");
             }
         }
-        syncOut.setCode(responseCode);
     }
 
     @SuppressWarnings("unchecked")
@@ -324,7 +342,7 @@ public abstract class AbstractUserAction implements PrivilegedExceptionAction<Ob
      *
      * @param users         The Map of user IDs to names.
      */
-    protected final void writeUsers(final Map<String, PersonalDetails> users)
+    protected final <T extends Principal> void writeUsers(final Collection<User<T>> users)
             throws IOException
     {
         syncOut.setHeader("Content-Type", acceptedContentType);
@@ -342,28 +360,4 @@ public abstract class AbstractUserAction implements PrivilegedExceptionAction<Ob
         }
     }
 
-    void redirectGet(User<?> user) throws Exception
-    {
-        final Set<Principal> httpPrincipals =  user.getIdentities();
-
-        String id = null;
-        String idType = null;
-        Iterator<Principal> i = httpPrincipals.iterator();
-        Principal next = null;
-        while (idType == null && i.hasNext())
-        {
-            next = i.next();
-            idType = AuthenticationUtil.getPrincipalType(next);
-            id = next.getName();
-        }
-
-        if (idType == null)
-        {
-            throw new IllegalStateException("No identities found.");
-        }
-
-        final String redirectURL = "/" + id + "?idType=" + idType;
-        syncOut.setHeader("Location", redirectURL);
-        syncOut.setCode(303);
-    }
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/LoginServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/LoginServlet.java
index f1daa9db4f5c8870590ca10c0698b5cbe55ce33c..eae23b582ad0798dded000843f53ea673a109eff 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/LoginServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/LoginServlet.java
@@ -71,16 +71,23 @@ package ca.nrc.cadc.ac.server.web.users;
 import java.io.IOException;
 import java.security.AccessControlException;
 
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.log4j.Logger;
 
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.ac.server.ldap.LdapGroupPersistence;
 import ca.nrc.cadc.ac.server.ldap.LdapUserPersistence;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.auth.SSOCookieManager;
 import ca.nrc.cadc.log.ServletLogInfo;
+import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.StringUtil;
 
 @SuppressWarnings("serial")
@@ -88,6 +95,30 @@ public class LoginServlet extends HttpServlet
 {
     private static final Logger log = Logger.getLogger(LoginServlet.class);
     private static final String CONTENT_TYPE = "text/plain";
+    public static final String PROXY_USER_DELIM = " as ";
+    String proxyGroup; // only users in this group can impersonate other users
+    String nonImpersonGroup; // users in this group cannot be impersonated
+    
+    
+    @Override
+    public void init(final ServletConfig config) throws ServletException
+    {
+        super.init(config);
+
+        try
+        {
+            this.proxyGroup = config.getInitParameter(
+                    LoginServlet.class.getName() + ".proxyGroup");
+            log.info("proxyGroup: " + proxyGroup);
+            this.nonImpersonGroup = config.getInitParameter(
+                    LoginServlet.class.getName() + ".nonImpersonGroup");
+            log.info("nonImpersonGroup: " + nonImpersonGroup);
+        }
+        catch(Exception ex)
+        {
+            log.error("failed to init: " + ex);
+        }
+    }
     /**
      * Attempt to login for userid/password.
      */
@@ -101,14 +132,28 @@ public class LoginServlet extends HttpServlet
         {
             log.info(logInfo.start());
             String userID = request.getParameter("username");
+            String proxyUser = null;
+            if (userID.contains(PROXY_USER_DELIM))
+            {
+                String[] fields = userID.split(PROXY_USER_DELIM);
+                proxyUser = fields[0];
+                userID = fields[1];
+                checkCanImpersonate(userID, proxyUser);
+            }
             String password = request.getParameter("password");
+            UserPersistence up = new LdapUserPersistence();
             if (StringUtil.hasText(userID))
             {
                 if (StringUtil.hasText(password))
                 {
-                	if (new LdapUserPersistence().doLogin(userID, password))
-                	{
-	            	    String token = new SSOCookieManager().generate(new HttpPrincipal(userID));
+                    if ((StringUtil.hasText(proxyUser) && 
+                            up.doLogin(proxyUser, password)) ||
+                        (!StringUtil.hasText(proxyUser) &&
+                                up.doLogin(userID, password)))   
+                    {
+	            	    String token = 
+	            	            new SSOCookieManager().generate(
+	            	                    new HttpPrincipal(userID, proxyUser));
 	            	    response.setContentType(CONTENT_TYPE);
 	            	    response.setContentLength(token.length());
 	            	    response.getWriter().write(token);
@@ -157,4 +202,29 @@ public class LoginServlet extends HttpServlet
             log.info(logInfo.end());
         }
     }
+	
+	/**
+	 * Checks if user can impersonate another user
+	 */
+    private void checkCanImpersonate(final String userID, final String proxyUser)
+            throws AccessControlException, UserNotFoundException,
+            TransientException
+    {
+        GroupPersistence<HttpPrincipal> gp = new LdapGroupPersistence<HttpPrincipal>();
+
+        if (!gp.isMember(new HttpPrincipal(proxyUser), proxyGroup))
+        {
+            throw new AccessControlException(proxyUser + " as " + userID
+                    + " failed: not in proxyGroup");
+        }
+
+        if (gp.isMember(new HttpPrincipal(userID), nonImpersonGroup))
+        {
+            if (gp.isMember(new HttpPrincipal(proxyUser), proxyGroup))
+            {
+                throw new AccessControlException(proxyUser + " as " + userID
+                        + " failed: non impersonable");
+            }
+        }
+    }
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/ModifyUserAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/ModifyUserAction.java
index 4e7ebe372ba2a3c0feb149274bf8bbfcbb38b58f..51777edcdfc1e20d793a15bc9bf74e8c510a963c 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/ModifyUserAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/ModifyUserAction.java
@@ -84,13 +84,15 @@ import java.util.Set;
 public class ModifyUserAction extends AbstractUserAction
 {
     private final InputStream inputStream;
+    private final String request;
 
 
-    ModifyUserAction(final InputStream inputStream)
+    ModifyUserAction(final InputStream inputStream, final String request)
     {
         super();
 
         this.inputStream = inputStream;
+        this.request = request;
     }
 
 
@@ -100,7 +102,8 @@ public class ModifyUserAction extends AbstractUserAction
         final User<Principal> modifiedUser = modifyUser(user);
         logUserInfo(modifiedUser.getUserID().getName());
 
-        redirectGet(modifiedUser);
+        syncOut.setHeader("Location", request);
+        syncOut.setCode(303);
     }
 
     /**
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/PasswordServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/PasswordServlet.java
index 5d07ea7147d9581fe140c98956454362652f750e..20fca463e31263da2908a76d88fff44da3cf668e 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/PasswordServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/PasswordServlet.java
@@ -70,13 +70,19 @@ package ca.nrc.cadc.ac.server.web.users;
 
 import java.io.IOException;
 import java.security.AccessControlException;
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
 import java.util.Set;
+import java.util.TreeSet;
 
 import javax.security.auth.Subject;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.ldap.LdapUserDAO;
+import ca.nrc.cadc.net.TransientException;
 import org.apache.log4j.Logger;
 
 import ca.nrc.cadc.ac.User;
@@ -85,6 +91,7 @@ import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.log.ServletLogInfo;
 import ca.nrc.cadc.util.StringUtil;
+import org.omg.CORBA.UserException;
 
 
 /**
@@ -117,37 +124,53 @@ public class PasswordServlet extends HttpServlet
         try
         {
             final Subject subject = AuthenticationUtil.getSubject(request);
-            if ((subject == null)
-                || (subject.getPrincipals(HttpPrincipal.class).isEmpty()))
+            if ((subject == null) || (subject.getPrincipals().isEmpty()))
             {
                 logInfo.setMessage("Unauthorized subject");
                 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
             }
             else
             {
-                logInfo.setSubject(subject);
-                final Set<HttpPrincipal> webPrincipals =
-                    subject.getPrincipals(HttpPrincipal.class);
-                final User<HttpPrincipal> user =
-                    new User<HttpPrincipal>(webPrincipals.iterator().next());
-                String oldPassword = request.getParameter("old_password");
-                String newPassword = request.getParameter("new_password");
-                if (StringUtil.hasText(oldPassword))
+                Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
                 {
-                    if (StringUtil.hasText(newPassword))
+                    public Object run() throws Exception
                     {
-                        (new LdapUserPersistence<HttpPrincipal>())
-                            .setPassword(user, oldPassword, newPassword);
-                    }
-                    else
-                    {
-                        throw new IllegalArgumentException("Missing new password");
+                        LdapUserPersistence<Principal> dao = new LdapUserPersistence<Principal>();
+                        User<Principal> user;
+                        try
+                        {
+                            user = dao.getUser(subject.getPrincipals().iterator().next());
+                        }
+                        catch (UserNotFoundException e)
+                        {
+                            throw new AccessControlException("User not found");
+                        }
+
+                        Subject logSubject = new Subject(false, user.getIdentities(),
+                                                         new TreeSet(), new TreeSet());
+
+                        logInfo.setSubject(logSubject);
+
+                        String oldPassword = request.getParameter("old_password");
+                        String newPassword = request.getParameter("new_password");
+                        if (StringUtil.hasText(oldPassword))
+                        {
+                            if (StringUtil.hasText(newPassword))
+                            {
+                                dao.setPassword(user, oldPassword, newPassword);
+                            }
+                            else
+                            {
+                                throw new IllegalArgumentException("Missing new password");
+                            }
+                        }
+                        else
+                        {
+                            throw new IllegalArgumentException("Missing old password");
+                        }
+                        return null;
                     }
-                }
-                else
-                {
-                    throw new IllegalArgumentException("Missing old password");
-                }
+                });
             }
         }
         catch (IllegalArgumentException e)
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserActionFactory.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserActionFactory.java
index 6c7b0c33338c075c71e3688f65d068f0bb6c2773..457dd4b1b686ef42092abd6378945affbf82ed05 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserActionFactory.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserActionFactory.java
@@ -77,6 +77,7 @@ import ca.nrc.cadc.auth.NumericPrincipal;
 import ca.nrc.cadc.auth.OpenIdPrincipal;
 
 import java.io.IOException;
+import java.net.URL;
 import java.security.Principal;
 import javax.security.auth.x500.X500Principal;
 import javax.servlet.http.HttpServletRequest;
@@ -169,7 +170,23 @@ public abstract class UserActionFactory
 
                 if (segments.length == 1)
                 {
-                    action = new ModifyUserAction(request.getInputStream());
+                    final URL requestURL = new URL(request.getRequestURL().toString());
+                    final StringBuilder sb = new StringBuilder();
+                    sb.append(requestURL.getProtocol());
+                    sb.append("://");
+                    sb.append(requestURL.getHost());
+                    if (requestURL.getPort() > 0)
+                    {
+                        sb.append(":");
+                        sb.append(requestURL.getPort());
+                    }
+                    sb.append(request.getContextPath());
+                    sb.append(request.getServletPath());
+                    sb.append(path);
+                    sb.append("?");
+                    sb.append(request.getQueryString());
+
+                    action = new ModifyUserAction(request.getInputStream(), sb.toString());
                 }
 
                 if (action != null)
@@ -242,7 +259,7 @@ public abstract class UserActionFactory
         {
             return new User<X500Principal>(new X500Principal(userName));
         }
-        else if (idType.equalsIgnoreCase(IdentityType.UID.getValue()))
+        else if (idType.equalsIgnoreCase(IdentityType.CADC.getValue()))
         {
             return new User<NumericPrincipal>(new NumericPrincipal(
                     Long.parseLong(userName)));
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserServlet.java
index 69a5c20a786b9cf364713c9555fd13a2b195cb60..6fbb19c354a93371a55156836e135a8394784f0f 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserServlet.java
@@ -192,7 +192,7 @@ public class UserServlet extends HttpServlet
     public void doPost(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(UserActionFactory.httpGetFactory(), request, response);
+        doAction(UserActionFactory.httpPostFactory(), request, response);
     }
 
     @Override
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/auth/AuthenticatorImpl.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/auth/AuthenticatorImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..024549e6ea3053593c4734ac608638b875154193
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/auth/AuthenticatorImpl.java
@@ -0,0 +1,156 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2014.                            (c) 2014.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *  $Revision: 4 $
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.auth;
+
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.ldap.LdapUserPersistence;
+import ca.nrc.cadc.profiler.Profiler;
+import org.apache.log4j.Logger;
+
+import javax.security.auth.Subject;
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+
+/**
+ * Implementation of default Authenticator for AuthenticationUtil in cadcUtil.
+ * This class augments the subject with additional identities using the
+ * access control library.
+ *
+ * @author pdowler
+ */
+public class AuthenticatorImpl implements Authenticator
+{
+    private static final Logger log = Logger.getLogger(AuthenticatorImpl.class);
+
+    public AuthenticatorImpl() { }
+
+    /**
+     * @param subject
+     * @return the possibly modified subject
+     */
+    public Subject getSubject(Subject subject)
+    {
+        AuthMethod am = AuthenticationUtil.getAuthMethod(subject);
+        if (am == null || AuthMethod.ANON.equals(am))
+            return subject;
+
+        if (subject != null && subject.getPrincipals().size() > 0)
+        {
+            Profiler prof = new Profiler(AuthenticatorImpl.class);
+            this.augmentSubject(subject);
+            prof.checkpoint("userDAO.augmentSubject()");
+
+            // if the caller had an invalid or forged CADC_SSO cookie, we could get
+            // in here and then not match any known identity: drop to anon
+            if ( subject.getPrincipals(HttpPrincipal.class).isEmpty() ) // no matching cadc account
+            {
+                log.debug("HttpPrincipal not found - dropping to anon: " + subject);
+                subject = AuthenticationUtil.getAnonSubject();
+            }
+        }
+
+        return subject;
+    }
+
+    protected void augmentSubject(final Subject subject)
+    {
+        try
+        {
+            PrivilegedExceptionAction<Object> action =
+                new PrivilegedExceptionAction<Object>()
+                {
+                    public Object run() throws Exception
+                    {
+                        try
+                        {
+                            LdapUserPersistence<Principal> dao = new LdapUserPersistence<Principal>();
+                            User<Principal> user = dao.getUser(subject.getPrincipals().iterator().next());
+                            subject.getPrincipals().addAll(user.getIdentities());
+                        }
+                        catch (UserNotFoundException e)
+                        {
+                            // ignore, could be an anonymous user
+                        }
+                        return null;
+                    }
+                };
+
+            Subject.doAs(subject, action);
+        }
+        catch (PrivilegedActionException e)
+        {
+            String msg = "Error augmenting subject " + subject;
+            throw new RuntimeException(msg, e);
+        }
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java
index b5f7b7fc706285aec7404148022f0a4ffae7f2a1..24b055459faa0ebe77d4fb669f3431118b147869 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java
@@ -494,11 +494,11 @@ public class LdapGroupDAOTest extends AbstractLdapDAOTest
                     for (Group group : groups)
                     {
                         log.debug("admin group: " + group.getID());
-                        if (group.getID().equals(testGroup1ID))
+                        if (group.getID().equalsIgnoreCase(testGroup1ID))
                         {
                             found1 = true;
                         }
-                        if (group.getID().equals(testGroup2ID))
+                        if (group.getID().equalsIgnoreCase(testGroup2ID))
                         {
                             found2 = true;
                         }
@@ -861,18 +861,10 @@ public class LdapGroupDAOTest extends AbstractLdapDAOTest
                     getGroupDAO().getGroups(unknownPrincipal, Role.OWNER, 
                                                groupID);
                     fail("searchGroups with unknown user should throw " + 
-                         "UserNotFoundException");
+                         "AccessControlException");
                 }
                 catch (AccessControlException ignore) {}
                 
-                try
-                {
-                    getGroupDAO().getGroups(daoTestPrincipal1, Role.OWNER, 
-                                               "foo");
-                    fail("searchGroups with unknown user should throw " + 
-                         "GroupNotFoundException");
-                }
-                catch (GroupNotFoundException ignore) {}
                 return null;
             }
         });
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
index 8e3a6db4ccc8259169fbbb50232317d7e0fa9e86..0624272f497c63f2e8d502f43d482d43dc67c614 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
@@ -70,6 +70,7 @@ package ca.nrc.cadc.ac.server.web.users;
 
 
 import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.json.JsonUserListWriter;
 import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.ac.server.web.SyncOutput;
@@ -85,7 +86,10 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.io.Writer;
+import java.security.Principal;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.easymock.EasyMock.*;
@@ -115,13 +119,14 @@ public class GetUserListActionTest
                 createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
-        final Map<String, PersonalDetails> userEntries =
-                new HashMap<String, PersonalDetails>();
+        List<User<Principal>> expectedUsers = new ArrayList<User<Principal>>();
 
         for (int i = 1; i <= 5; i++)
         {
-            userEntries.put("USER_" + i,
-                            new PersonalDetails("USER", Integer.toString(i)));
+            User<Principal> user = new User<Principal>(new HttpPrincipal("USER_" + i));
+            PersonalDetails pd = new PersonalDetails("USER", Integer.toString(i));
+            user.details.add(pd);
+            expectedUsers.add(user);
         }
 
         final GetUserListAction testSubject = new GetUserListAction()
@@ -138,8 +143,7 @@ public class GetUserListActionTest
         final Writer actualWriter = new StringWriter();
         final PrintWriter actualPrintWriter = new PrintWriter(actualWriter);
 
-        expect(mockUserPersistence.getUsers()).andReturn(
-                userEntries).once();
+        expect(mockUserPersistence.getUsers()).andReturn(expectedUsers).once();
         expect(mockSyncOut.getWriter()).andReturn(actualPrintWriter).once();
         mockSyncOut.setHeader("Content-Type", "application/json");
         expectLastCall().once();
@@ -153,7 +157,7 @@ public class GetUserListActionTest
         final Writer expectedWriter = new StringWriter();
         final PrintWriter expectedPrintWriter = new PrintWriter(expectedWriter);
         JsonUserListWriter userListWriter = new JsonUserListWriter();
-        userListWriter.write(userEntries, expectedPrintWriter);
+        userListWriter.write(expectedUsers, expectedPrintWriter);
         JSONAssert.assertEquals(expectedWriter.toString(), actualWriter.toString(), false);
 
         verify(mockSyncOut, mockUserPersistence);
@@ -167,13 +171,14 @@ public class GetUserListActionTest
                 createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
-        final Map<String, PersonalDetails> userEntries =
-                new HashMap<String, PersonalDetails>();
+        List<User<Principal>> expectedUsers = new ArrayList<User<Principal>>();
 
         for (int i = 1; i <= 5; i++)
         {
-            userEntries.put("USER_" + i,
-                            new PersonalDetails("USER", Integer.toString(i)));
+            User<Principal> user = new User<Principal>(new HttpPrincipal("USER_" + i));
+            PersonalDetails pd = new PersonalDetails("USER", Integer.toString(i));
+            user.details.add(pd);
+            expectedUsers.add(user);
         }
 
         final GetUserListAction testSubject = new GetUserListAction()
@@ -188,8 +193,7 @@ public class GetUserListActionTest
         final Writer actualWriter = new StringWriter();
         final PrintWriter actualPrintWriter = new PrintWriter(actualWriter);
 
-        expect(mockUserPersistence.getUsers()).andReturn(
-                userEntries).once();
+        expect(mockUserPersistence.getUsers()).andReturn(expectedUsers).once();
         expect(mockSyncOut.getWriter()).andReturn(actualPrintWriter).once();
         mockSyncOut.setHeader("Content-Type", "text/xml");
         expectLastCall().once();
@@ -203,7 +207,7 @@ public class GetUserListActionTest
         final Writer expectedWriter = new StringWriter();
         final PrintWriter expectedPrintWriter = new PrintWriter(expectedWriter);
         UserListWriter userListWriter = new UserListWriter();
-        userListWriter.write(userEntries, expectedPrintWriter);
+        userListWriter.write(expectedUsers, expectedPrintWriter);
         assertEquals("Wrong XML", expectedWriter.toString(), actualWriter.toString());
 
         verify(mockSyncOut, mockUserPersistence);
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/ModifyUserActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/ModifyUserActionTest.java
index ff4538b29c58b0c6a33548b1907f78077309a866..861403fac3f2c3d8e3808c0210fe439935e53ebc 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/ModifyUserActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/ModifyUserActionTest.java
@@ -108,6 +108,7 @@ public class ModifyUserActionTest
 
         final byte[] input = sb.toString().getBytes();
         final InputStream inputStream = new ByteArrayInputStream(input);
+        final String request = "/CADCtest?idType=http";
 
         // Should match the JSON above, without the e-mail modification.
         Principal principal = new HttpPrincipal("CADCtest");
@@ -119,8 +120,6 @@ public class ModifyUserActionTest
         personalDetail.email = "CADC.Test@nrc-cnrc.gc.ca";
         userObject.details.add(personalDetail);
 
-        final HttpServletRequest mockRequest =
-                createMock(HttpServletRequest.class);
         final SyncOutput mockSyncOut =
                 createMock(SyncOutput.class);
 
@@ -130,11 +129,9 @@ public class ModifyUserActionTest
 
         expect(mockUserPersistence.modifyUser(userObject)).andReturn(
                 userObject).once();
-//
-//        expect(mockRequest.getRemoteAddr()).andReturn(requestURL).
-//                once();
 
-        mockSyncOut.setHeader("Location", "/CADCtest?idType=http");
+
+        mockSyncOut.setHeader("Location", request);
         expectLastCall().once();
 
         mockSyncOut.setCode(303);
@@ -143,9 +140,9 @@ public class ModifyUserActionTest
         mockSyncOut.setHeader("Content-Type", "application/json");
         expectLastCall().once();
 
-        replay(mockRequest, mockSyncOut, mockUserPersistence);
+        replay(mockSyncOut, mockUserPersistence);
 
-        final ModifyUserAction testSubject = new ModifyUserAction(inputStream)
+        final ModifyUserAction testSubject = new ModifyUserAction(inputStream, request)
         {
             @Override
             @SuppressWarnings("unchecked")
@@ -161,6 +158,6 @@ public class ModifyUserActionTest
         testSubject.setLogInfo(logInfo);
         testSubject.doAction();
 
-        verify(mockRequest, mockSyncOut, mockUserPersistence);
+        verify(mockSyncOut, mockUserPersistence);
     }
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UserActionFactoryTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UserActionFactoryTest.java
index 93aa2a541fdc96f9b2ba2f2057ab37becf347c8c..54db0b92985648da2304dadf29e42e63fcbaaa62 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UserActionFactoryTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UserActionFactoryTest.java
@@ -175,11 +175,12 @@ public class UserActionFactoryTest
 
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("userName");
-            //EasyMock.expect(request.getRequestURL()).andReturn(sb);
-            //EasyMock.expect(request.getContextPath()).andReturn("");
-            //EasyMock.expect(request.getServletPath()).andReturn("");
+            EasyMock.expect(request.getRequestURL()).andReturn(sb);
+            EasyMock.expect(request.getContextPath()).andReturn("");
+            EasyMock.expect(request.getServletPath()).andReturn("");
+            EasyMock.expect(request.getQueryString()).andReturn("");
             EasyMock.expect(request.getInputStream()).andReturn(null);
-            //EasyMock.expect(request.getParameter("idType")).andReturn("sessionID");
+//            EasyMock.expect(request.getParameter("idType")).andReturn("sessionID");
             EasyMock.replay(request);
             AbstractUserAction action = UserActionFactory.httpPostFactory().createAction(request);
             EasyMock.verify(request);
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
index 6a91450bc9acc1e27da28ae2f227edfa2fe8fd16..8dac1009e59bf92e492678dfb3837e7088aa0658 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
@@ -155,6 +155,11 @@ public class Group
         return owner;
     }
 
+    public void setOwner(User<? extends Principal> owner)
+    {
+        this.owner = owner;
+    }
+
     /**
      * 
      * @return a set of properties associated with a group
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
index 97a63b6bb2827991999266e3e547cdf4bd31f42d..70ec423820e5bac0c3878dda8cb7a7469279e151 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
@@ -152,6 +152,18 @@ public class User<T extends Principal>
         return getClass().getSimpleName() + "[" + userID.getName() + "]";
     }
 
+    public <S extends UserDetails>S getUserDetail(final Class<S> userDetailsClass)
+    {
+        for (final UserDetails ud : details)
+        {
+            if (ud.getClass() == userDetailsClass)
+            {
+                return (S) ud;
+            }
+        }
+        return null;
+    }
+
     public <S extends UserDetails> Set<S> getDetails(
             final Class<S> userDetailsClass)
     {
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
index bad17d1f5f80cee819082265ca7f344940ac14b1..3f0a4f954a61d5ec56ca98e4773e3ab842f2c401 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
@@ -68,28 +68,6 @@
  */
 package ca.nrc.cadc.ac.client;
 
-import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.GroupAlreadyExistsException;
-import ca.nrc.cadc.ac.GroupNotFoundException;
-import ca.nrc.cadc.ac.Role;
-import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserNotFoundException;
-import ca.nrc.cadc.ac.xml.GroupListReader;
-import ca.nrc.cadc.ac.xml.GroupReader;
-import ca.nrc.cadc.ac.xml.GroupWriter;
-import ca.nrc.cadc.auth.AuthenticationUtil;
-import ca.nrc.cadc.auth.HttpPrincipal;
-import ca.nrc.cadc.auth.SSLUtil;
-import ca.nrc.cadc.net.HttpDownload;
-import ca.nrc.cadc.net.HttpPost;
-import ca.nrc.cadc.net.HttpUpload;
-import ca.nrc.cadc.net.InputStreamWrapper;
-import ca.nrc.cadc.net.NetUtil;
-import org.apache.log4j.Logger;
-
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLSocketFactory;
-import javax.security.auth.Subject;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -106,17 +84,37 @@ import java.security.AccessController;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+
+import ca.nrc.cadc.ac.*;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.ac.xml.GroupListReader;
+import ca.nrc.cadc.ac.xml.GroupReader;
+import ca.nrc.cadc.ac.xml.GroupWriter;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.auth.SSLUtil;
+import ca.nrc.cadc.net.HttpDownload;
+import ca.nrc.cadc.net.HttpPost;
+import ca.nrc.cadc.net.HttpUpload;
+import ca.nrc.cadc.net.InputStreamWrapper;
+import ca.nrc.cadc.net.NetUtil;
+import ca.nrc.cadc.net.event.TransferEvent;
+import ca.nrc.cadc.net.event.TransferListener;
+
 
 /**
  * Client class for performing group searching and group actions
  * with the access control web service.
  */
-public class GMSClient
+public class GMSClient implements TransferListener
 {
     private static final Logger log = Logger.getLogger(GMSClient.class);
 
@@ -130,10 +128,10 @@ public class GMSClient
      * Constructor.
      *
      * @param baseURL The URL of the supporting access control web service
-     *                obtained from the registry.
+     * obtained from the registry.
      */
-    public GMSClient(final String baseURL)
-            throws IllegalArgumentException
+    public GMSClient(String baseURL)
+        throws IllegalArgumentException
     {
         if (baseURL == null)
         {
@@ -159,6 +157,18 @@ public class GMSClient
         }
     }
 
+    public void transferEvent(TransferEvent te)
+    {
+        if ( TransferEvent.RETRYING == te.getState() )
+            log.debug("retry after request failed, reason: "  + te.getError());
+    }
+
+    public String getEventHeader()
+    {
+        return null; // no custom eventID header
+    }
+
+
     /**
      * Get a list of groups.
      *
@@ -169,8 +179,6 @@ public class GMSClient
         throw new UnsupportedOperationException("Not yet implemented");
     }
 
-
-
     /**
      * Obtain all of the users as userID - name in JSON format.
      *
@@ -181,9 +189,8 @@ public class GMSClient
     {
         final List<User<HttpPrincipal>> webUsers =
                 new ArrayList<User<HttpPrincipal>>();
-
         final HttpDownload httpDownload =
-                createDisplayUsersHTTPDownload(webUsers);
+                    createDisplayUsersHTTPDownload(webUsers);
 
         httpDownload.setRequestProperty("Accept", "application/json");
         httpDownload.run();
@@ -194,12 +201,10 @@ public class GMSClient
         {
             final String errMessage = error.getMessage();
             final int responseCode = httpDownload.getResponseCode();
-
-            log.debug("getDisplayUsers response " + responseCode + ": " +
-                      errMessage);
-
-            if ((responseCode == 401) || (responseCode == 403) ||
-                (responseCode == -1))
+            log.debug("getDisplayUsers response " + responseCode + ": "
+                      + errMessage);
+            if ((responseCode == 401) || (responseCode == 403)
+                || (responseCode == -1))
             {
                 throw new AccessControlException(errMessage);
             }
@@ -207,9 +212,11 @@ public class GMSClient
             {
                 throw new IllegalArgumentException(errMessage);
             }
-
-            throw new IOException("HttpResponse (" + responseCode + ") - "
-                                  + errMessage);
+            else
+            {
+                throw new IOException("HttpResponse (" + responseCode + ") - "
+                                      + errMessage);
+            }
         }
 
         log.debug("Content-Length: " + httpDownload.getContentLength());
@@ -218,6 +225,14 @@ public class GMSClient
         return webUsers;
     }
 
+
+    /**
+     * Create a new HTTPDownload instance.  Testers can override as needed.
+     *
+     * @param webUsers          The User objects.
+     * @return                  HttpDownload instance.  Never null.
+     * @throws IOException      Any writing/reading errors.
+     */
     HttpDownload createDisplayUsersHTTPDownload(
             final List<User<HttpPrincipal>> webUsers) throws IOException
     {
@@ -233,13 +248,13 @@ public class GMSClient
      * @return The newly created group will all the information.
      * @throws GroupAlreadyExistsException If a group with the same name already
      *                                     exists.
-     * @throws AccessControlException      If unauthorized to perform this operation.
+     * @throws AccessControlException If unauthorized to perform this operation.
      * @throws UserNotFoundException
      * @throws IOException
      */
     public Group createGroup(Group group)
-            throws GroupAlreadyExistsException, AccessControlException,
-                   UserNotFoundException, IOException
+        throws GroupAlreadyExistsException, AccessControlException,
+               UserNotFoundException, IOException
     {
         URL createGroupURL = new URL(this.baseURL + "/groups");
         log.debug("createGroupURL request to " + createGroupURL.toString());
@@ -310,7 +325,7 @@ public class GMSClient
      * @throws java.io.IOException
      */
     public Group getGroup(String groupName)
-            throws GroupNotFoundException, AccessControlException, IOException
+        throws GroupNotFoundException, AccessControlException, IOException
     {
         URL getGroupURL = new URL(this.baseURL + "/groups/" + groupName);
         log.debug("getGroup request to " + getGroupURL.toString());
@@ -323,8 +338,7 @@ public class GMSClient
         Throwable error = transfer.getThrowable();
         if (error != null)
         {
-            log.debug("getGroup throwable (" + transfer
-                    .getResponseCode() + ")", error);
+            log.debug("getGroup throwable (" + transfer.getResponseCode() + ")", error);
             // transfer returns a -1 code for anonymous access.
             if ((transfer.getResponseCode() == -1) ||
                 (transfer.getResponseCode() == 401) ||
@@ -365,7 +379,7 @@ public class GMSClient
      * @throws java.io.IOException
      */
     public List<String> getGroupNames()
-            throws AccessControlException, IOException
+        throws AccessControlException, IOException
     {
         final URL getGroupNamesURL = new URL(this.baseURL + "/groups");
         log.debug("getGroupNames request to " + getGroupNamesURL.toString());
@@ -373,28 +387,26 @@ public class GMSClient
         final List<String> groupNames = new ArrayList<String>();
         final HttpDownload httpDownload =
                 new HttpDownload(getGroupNamesURL, new InputStreamWrapper()
+        {
+            @Override
+            public void read(final InputStream inputStream) throws IOException
+            {
+                try
                 {
-                    @Override
-                    public void read(final InputStream inputStream) throws
-                                                                    IOException
-                    {
-                        try
-                        {
-                            InputStreamReader inReader = new InputStreamReader(inputStream);
-                            BufferedReader reader = new BufferedReader(inReader);
-                            String line;
-                            while ((line = reader.readLine()) != null)
-                            {
-                                groupNames.add(line);
-                            }
-                        }
-                        catch (Exception bug)
-                        {
-                            log.error("Unexpected exception", bug);
-                            throw new RuntimeException(bug);
-                        }
+                    InputStreamReader inReader = new InputStreamReader(inputStream);
+                    BufferedReader reader = new BufferedReader(inReader);
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        groupNames.add(line);
                     }
-                });
+                }
+                catch (Exception bug)
+                {
+                    log.error("Unexpected exception", bug);
+                    throw new RuntimeException(bug);
+                }
+            }
+        });
 
         httpDownload.setSSLSocketFactory(getSSLSocketFactory());
         httpDownload.run();
@@ -410,7 +422,7 @@ public class GMSClient
                       errMessage);
 
             if ((responseCode == 401) || (responseCode == 403) ||
-                (responseCode == -1))
+                    (responseCode == -1))
             {
                 throw new AccessControlException(errMessage);
             }
@@ -433,15 +445,14 @@ public class GMSClient
      * @param group The update group object.
      * @return The group after update.
      * @throws IllegalArgumentException If cyclical membership is detected.
-     * @throws GroupNotFoundException   If the group was not found.
-     * @throws UserNotFoundException    If a member was not found.
-     * @throws AccessControlException   If unauthorized to perform this operation.
+     * @throws GroupNotFoundException If the group was not found.
+     * @throws UserNotFoundException If a member was not found.
+     * @throws AccessControlException If unauthorized to perform this operation.
      * @throws java.io.IOException
      */
     public Group updateGroup(Group group)
-            throws IllegalArgumentException, GroupNotFoundException,
-                   UserNotFoundException,
-                   AccessControlException, IOException
+        throws IllegalArgumentException, GroupNotFoundException, UserNotFoundException,
+               AccessControlException, IOException
     {
         URL updateGroupURL = new URL(this.baseURL + "/groups/" + group.getID());
         log.debug("updateGroup request to " + updateGroupURL.toString());
@@ -457,8 +468,10 @@ public class GMSClient
         HttpPost transfer = new HttpPost(updateGroupURL, groupXML.toString(),
                                          "application/xml", true);
         transfer.setSSLSocketFactory(getSSLSocketFactory());
+        transfer.setTransferListener(this);
         transfer.run();
 
+
         Throwable error = transfer.getThrowable();
         if (error != null)
         {
@@ -475,15 +488,10 @@ public class GMSClient
             }
             if (transfer.getResponseCode() == 404)
             {
-                if (error.getMessage() != null && error.getMessage()
-                        .toLowerCase().contains("user"))
-                {
+                if (error.getMessage() != null && error.getMessage().toLowerCase().contains("user"))
                     throw new UserNotFoundException(error.getMessage());
-                }
                 else
-                {
                     throw new GroupNotFoundException(error.getMessage());
-                }
             }
             throw new IOException(error);
         }
@@ -511,7 +519,7 @@ public class GMSClient
      * @throws java.io.IOException
      */
     public void deleteGroup(String groupName)
-            throws GroupNotFoundException, AccessControlException, IOException
+        throws GroupNotFoundException, AccessControlException, IOException
     {
         URL deleteGroupURL = new URL(this.baseURL + "/groups/" + groupName);
         log.debug("deleteGroup request to " + deleteGroupURL.toString());
@@ -536,7 +544,7 @@ public class GMSClient
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception e)
+        catch(Exception e)
         {
             throw new AccessControlException(e.getMessage());
         }
@@ -548,7 +556,7 @@ public class GMSClient
                       errMessage);
 
             if ((responseCode == 401) || (responseCode == 403) ||
-                (responseCode == -1))
+                    (responseCode == -1))
             {
                 throw new AccessControlException(errMessage);
             }
@@ -570,13 +578,13 @@ public class GMSClient
      * @param targetGroupName The group in which to add the group member.
      * @param groupMemberName The group member to add.
      * @throws IllegalArgumentException If cyclical membership is detected.
-     * @throws GroupNotFoundException   If the group was not found.
-     * @throws AccessControlException   If unauthorized to perform this operation.
+     * @throws GroupNotFoundException If the group was not found.
+     * @throws AccessControlException If unauthorized to perform this operation.
      * @throws java.io.IOException
      */
     public void addGroupMember(String targetGroupName, String groupMemberName)
-            throws IllegalArgumentException, GroupNotFoundException,
-                   AccessControlException, IOException
+        throws IllegalArgumentException, GroupNotFoundException,
+               AccessControlException, IOException
     {
         URL addGroupMemberURL = new URL(this.baseURL + "/groups/" +
                                         targetGroupName + "/groupMembers/" +
@@ -620,15 +628,14 @@ public class GMSClient
      * Add a user as a member of a group.
      *
      * @param targetGroupName The group in which to add the group member.
-     * @param userID          The user to add.
+     * @param userID The user to add.
      * @throws GroupNotFoundException If the group was not found.
-     * @throws UserNotFoundException  If the member was not found.
+     * @throws UserNotFoundException If the member was not found.
      * @throws java.io.IOException
      * @throws AccessControlException If unauthorized to perform this operation.
      */
     public void addUserMember(String targetGroupName, Principal userID)
-            throws GroupNotFoundException, UserNotFoundException,
-                   AccessControlException, IOException
+        throws GroupNotFoundException, UserNotFoundException, AccessControlException, IOException
     {
         String userIDType = AuthenticationUtil.getPrincipalType(userID);
         String encodedUserID = URLEncoder.encode(userID.getName(), "UTF-8");
@@ -665,15 +672,10 @@ public class GMSClient
             }
             if (responseCode == 404)
             {
-                if (errMessage != null && errMessage.toLowerCase()
-                        .contains("user"))
-                {
+                if (errMessage != null && errMessage.toLowerCase().contains("user"))
                     throw new UserNotFoundException(errMessage);
-                }
                 else
-                {
                     throw new GroupNotFoundException(errMessage);
-                }
             }
             throw new IOException(errMessage);
         }
@@ -690,7 +692,7 @@ public class GMSClient
      */
     public void removeGroupMember(String targetGroupName,
                                   String groupMemberName)
-            throws GroupNotFoundException, AccessControlException, IOException
+        throws GroupNotFoundException, AccessControlException, IOException
     {
         URL removeGroupMemberURL = new URL(this.baseURL + "/groups/" +
                                            targetGroupName + "/groupMembers/" +
@@ -712,15 +714,13 @@ public class GMSClient
                     .setSSLSocketFactory(getSSLSocketFactory());
         }
 
-        // Try to handle anonymous access and throw AccessControlException 
+        // Try to handle anonymous access and throw AccessControlException
         int responseCode = -1;
         try
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception ignore)
-        {
-        }
+        catch (Exception ignore) {}
 
         if (responseCode != 200)
         {
@@ -750,15 +750,14 @@ public class GMSClient
      * Remove a user as a member of a group.
      *
      * @param targetGroupName The group from which to remove the group member.
-     * @param userID          The user to remove.
+     * @param userID The user to remove.
      * @throws GroupNotFoundException If the group was not found.
-     * @throws UserNotFoundException  If the member was not found.
+     * @throws UserNotFoundException If the member was not found.
      * @throws java.io.IOException
      * @throws AccessControlException If unauthorized to perform this operation.
      */
     public void removeUserMember(String targetGroupName, Principal userID)
-            throws GroupNotFoundException, UserNotFoundException,
-                   AccessControlException, IOException
+        throws GroupNotFoundException, UserNotFoundException, AccessControlException, IOException
     {
         String userIDType = AuthenticationUtil.getPrincipalType(userID);
         String encodedUserID = URLEncoder.encode(userID.toString(), "UTF-8");
@@ -784,15 +783,13 @@ public class GMSClient
                     .setSSLSocketFactory(getSSLSocketFactory());
         }
 
-        // Try to handle anonymous access and throw AccessControlException 
+        // Try to handle anonymous access and throw AccessControlException
         int responseCode = -1;
         try
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception ignore)
-        {
-        }
+        catch (Exception ignore) {}
 
         if (responseCode != 200)
         {
@@ -812,15 +809,10 @@ public class GMSClient
             }
             if (responseCode == 404)
             {
-                if (errMessage != null && errMessage.toLowerCase()
-                        .contains("user"))
-                {
+                if (errMessage != null && errMessage.toLowerCase().contains("user"))
                     throw new UserNotFoundException(errMessage);
-                }
                 else
-                {
                     throw new GroupNotFoundException(errMessage);
-                }
             }
             throw new IOException(errMessage);
         }
@@ -830,22 +822,22 @@ public class GMSClient
      * Get all the memberships of the user of a certain role.
      *
      * @param userID Identifies the user.
-     * @param role   The role to look up.
+     * @param role The role to look up.
      * @return A list of groups for which the user has the role.
-     * @throws UserNotFoundException    If the user does not exist.
-     * @throws AccessControlException   If not allowed to peform the search.
+     * @throws UserNotFoundException If the user does not exist.
+     * @throws AccessControlException If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException              If an unknown error occured.
+     * @throws IOException If an unknown error occured.
      */
     public List<Group> getMemberships(Principal userID, Role role)
-            throws UserNotFoundException, AccessControlException, IOException
+        throws UserNotFoundException, AccessControlException, IOException
     {
         if (userID == null || role == null)
         {
             throw new IllegalArgumentException("userID and role are required.");
         }
 
-        List<Group> cachedGroups = getCachedGroups(userID, role);
+        List<Group> cachedGroups = getCachedGroups(userID, role, true);
         if (cachedGroups != null)
         {
             return cachedGroups;
@@ -914,19 +906,19 @@ public class GMSClient
      * Return the group, specified by paramter groupName, if the user,
      * identified by userID, is a member of that group.  Return null
      * otherwise.
-     * <p/>
+     *
      * This call is identical to getMemberShip(userID, groupName, Role.MEMBER)
      *
-     * @param userID    Identifies the user.
+     * @param userID Identifies the user.
      * @param groupName Identifies the group.
      * @return The group or null of the user is not a member.
-     * @throws UserNotFoundException    If the user does not exist.
-     * @throws AccessControlException   If not allowed to peform the search.
+     * @throws UserNotFoundException If the user does not exist.
+     * @throws AccessControlException If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException              If an unknown error occured.
+     * @throws IOException If an unknown error occured.
      */
     public Group getMembership(Principal userID, String groupName)
-            throws UserNotFoundException, AccessControlException, IOException
+        throws UserNotFoundException, AccessControlException, IOException
     {
         return getMembership(userID, groupName, Role.MEMBER);
     }
@@ -936,35 +928,27 @@ public class GMSClient
      * identified by userID, is a member (of type role) of that group.
      * Return null otherwise.
      *
-     * @param userID    Identifies the user.
+     * @param userID Identifies the user.
      * @param groupName Identifies the group.
-     * @param role      The membership role to search.
+     * @param role The membership role to search.
      * @return The group or null of the user is not a member.
-     * @throws UserNotFoundException    If the user does not exist.
-     * @throws AccessControlException   If not allowed to peform the search.
+     * @throws UserNotFoundException If the user does not exist.
+     * @throws AccessControlException If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException              If an unknown error occured.
+     * @throws IOException If an unknown error occured.
      */
     public Group getMembership(Principal userID, String groupName, Role role)
-            throws UserNotFoundException, AccessControlException, IOException
+        throws UserNotFoundException, AccessControlException, IOException
     {
         if (userID == null || groupName == null || role == null)
         {
             throw new IllegalArgumentException("userID and role are required.");
         }
 
-        List<Group> cachedGroups = getCachedGroups(userID, role);
-        if (cachedGroups != null)
+        Group cachedGroup = getCachedGroup(userID, groupName, role);
+        if (cachedGroup != null)
         {
-            int index = cachedGroups.indexOf(new Group(groupName));
-            if (index != -1)
-            {
-                return cachedGroups.get(index);
-            }
-            else
-            {
-                return null;
-            }
+            return cachedGroup;
         }
 
         String idType = AuthenticationUtil.getPrincipalType(userID);
@@ -1024,9 +1008,9 @@ public class GMSClient
             }
             if (groups.size() == 1)
             {
-                // don't cache these results as it is not a complete
-                // list of memberships--it only applies to one group.
-                return groups.get(0);
+                Group ret = groups.get(0);
+                addCachedGroup(userID, ret, role);
+                return ret;
             }
             throw new IllegalStateException(
                     "Duplicate membership for " + id + " in group " + groupName);
@@ -1040,19 +1024,19 @@ public class GMSClient
 
     /**
      * Check if userID is a member of groupName.
-     * <p/>
+     *
      * This is equivalent to isMember(userID, groupName, Role.MEMBER)
      *
-     * @param userID    Identifies the user.
+     * @param userID Identifies the user.
      * @param groupName Identifies the group.
      * @return True if the user is a member of the group
-     * @throws UserNotFoundException    If the user does not exist.
-     * @throws AccessControlException   If not allowed to peform the search.
+     * @throws UserNotFoundException If the user does not exist.
+     * @throws AccessControlException If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException              If an unknown error occured.
+     * @throws IOException If an unknown error occured.
      */
     public boolean isMember(Principal userID, String groupName)
-            throws UserNotFoundException, AccessControlException, IOException
+        throws UserNotFoundException, AccessControlException, IOException
     {
         return isMember(userID, groupName, Role.MEMBER);
     }
@@ -1060,17 +1044,17 @@ public class GMSClient
     /**
      * Check if userID is a member (of type role) of groupName.
      *
-     * @param userID    Identifies the user.
+     * @param userID Identifies the user.
      * @param groupName Identifies the group.
-     * @param role      The type of membership.
+     * @param role The type of membership.
      * @return True if the user is a member of the group
-     * @throws UserNotFoundException    If the user does not exist.
-     * @throws AccessControlException   If not allowed to peform the search.
+     * @throws UserNotFoundException If the user does not exist.
+     * @throws AccessControlException If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException              If an unknown error occured.
+     * @throws IOException If an unknown error occured.
      */
     public boolean isMember(Principal userID, String groupName, Role role)
-            throws UserNotFoundException, AccessControlException, IOException
+        throws UserNotFoundException, AccessControlException, IOException
     {
         Group group = getMembership(userID, groupName, role);
         return group != null;
@@ -1082,16 +1066,13 @@ public class GMSClient
     public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory)
     {
         if (mySocketFactory != null)
-        {
             throw new IllegalStateException("Illegal use of GMSClient: "
-                                            + "cannot set SSLSocketFactory after using one created from Subject");
-        }
+                    + "cannot set SSLSocketFactory after using one created from Subject");
         this.sslSocketFactory = sslSocketFactory;
         clearCache();
     }
 
     private int subjectHashCode = 0;
-
     private SSLSocketFactory getSSLSocketFactory()
     {
         AccessControlContext ac = AccessController.getContext();
@@ -1114,12 +1095,9 @@ public class GMSClient
         {
             int c = s.hashCode();
             if (c != subjectHashCode)
-            {
                 throw new IllegalStateException("Illegal use of "
-                                                + this.getClass()
-                                                        .getSimpleName()
-                                                + ": subject change not supported for internal SSLSocketFactory");
-            }
+                        + this.getClass().getSimpleName()
+                        + ": subject change not supported for internal SSLSocketFactory");
         }
         return this.mySocketFactory;
     }
@@ -1128,15 +1106,13 @@ public class GMSClient
     {
         AccessControlContext acContext = AccessController.getContext();
         Subject subject = Subject.getSubject(acContext);
-
         if (subject != null)
         {
-            log.debug("Clearing cache");
-            subject.getPrivateCredentials().clear();
+            subject.getPrivateCredentials().remove(new GroupMemberships());
         }
     }
 
-    protected List<Group> getCachedGroups(Principal userID, Role role)
+    protected GroupMemberships getGroupCache(Principal userID)
     {
         AccessControlContext acContext = AccessController.getContext();
         Subject subject = Subject.getSubject(acContext);
@@ -1144,46 +1120,81 @@ public class GMSClient
         // only consult cache if the userID is of the calling subject
         if (userIsSubject(userID, subject))
         {
-            Set groupCredentialSet = subject
-                    .getPrivateCredentials(GroupMemberships.class);
-            if ((groupCredentialSet != null) &&
-                (groupCredentialSet.size() == 1))
+            Set<GroupMemberships> gset = subject.getPrivateCredentials(GroupMemberships.class);
+            if (gset == null || gset.isEmpty())
             {
-                Iterator i = groupCredentialSet.iterator();
-                GroupMemberships groupMemberships = ((GroupMemberships) i
-                        .next());
-                return groupMemberships.memberships.get(role);
+                GroupMemberships mems = new GroupMemberships();
+                subject.getPrivateCredentials().add(mems);
+                return mems;
             }
+            GroupMemberships mems = gset.iterator().next();
+            return mems;
+        }
+        return null; // no cache
+    }
+
+    protected Group getCachedGroup(Principal userID, String groupID, Role role)
+    {
+        List<Group> groups = getCachedGroups(userID, role, false);
+        if (groups == null)
+            return null; // no cache
+        for (Group g : groups)
+        {
+            if (g.getID().equals(groupID))
+                return g;
         }
         return null;
     }
+    protected List<Group> getCachedGroups(Principal userID, Role role, boolean complete)
+    {
+        GroupMemberships mems = getGroupCache(userID);
+        if (mems == null)
+            return null; // no cache
 
-    protected void setCachedGroups(Principal userID, List<Group> groups, Role role)
+        Boolean cacheState = mems.complete.get(role);
+        if (!complete || Boolean.TRUE.equals(cacheState))
+            return mems.memberships.get(role);
+
+        // caller wanted complete and we don't have that
+        return null;
+    }
+
+    protected void addCachedGroup(Principal userID, Group group, Role role)
     {
-        AccessControlContext acContext = AccessController.getContext();
-        Subject subject = Subject.getSubject(acContext);
+        GroupMemberships mems = getGroupCache(userID);
+        if (mems == null)
+            return; // no cache
 
-        // only save to cache if the userID is of the calling subject
-        if (userIsSubject(userID, subject))
+        List<Group> groups = mems.memberships.get(role);
+        if (groups == null)
         {
-            log.debug("Caching groups for " + userID + ", role " + role);
+            groups = new ArrayList<Group>();
+            mems.complete.put(role, Boolean.FALSE);
+            mems.memberships.put(role, groups);
+        }
+        if (!groups.contains(group))
+            groups.add(group);
+    }
 
-            final GroupMemberships groupCredentials;
-            Set groupCredentialSet = subject
-                    .getPrivateCredentials(GroupMemberships.class);
-            if ((groupCredentialSet != null) &&
-                (groupCredentialSet.size() == 1))
-            {
-                Iterator i = groupCredentialSet.iterator();
-                groupCredentials = ((GroupMemberships) i.next());
-            }
-            else
-            {
-                groupCredentials = new GroupMemberships();
-                subject.getPrivateCredentials().add(groupCredentials);
-            }
+    protected void setCachedGroups(Principal userID, List<Group> groups, Role role)
+    {
+        GroupMemberships mems = getGroupCache(userID);
+        if (mems == null)
+            return; // no cache
 
-            groupCredentials.memberships.put(role, groups);
+        log.debug("Caching groups for " + userID + ", role " + role);
+        List<Group> cur = mems.memberships.get(role);
+        if (cur == null)
+        {
+            cur = new ArrayList<Group>();
+            mems.complete.put(role, Boolean.FALSE);
+            mems.memberships.put(role, cur);
+        }
+        for (Group group : groups)
+        {
+            if (!cur.contains(group))
+                cur.add(group);
+            mems.complete.put(role, Boolean.TRUE);
         }
     }
 
@@ -1196,7 +1207,7 @@ public class GMSClient
 
         for (Principal subjectPrincipal : subject.getPrincipals())
         {
-            if (subjectPrincipal.equals(userID))
+            if (AuthenticationUtil.equals(subjectPrincipal, userID))
             {
                 return true;
             }
@@ -1205,17 +1216,31 @@ public class GMSClient
     }
 
     /**
-     * Class used to hold list of groups in which
-     * a user is a member.
+     * Class used to hold list of groups in which a user is known to be a member.
      */
-    protected class GroupMemberships
+    protected class GroupMemberships implements Comparable
     {
         Map<Role, List<Group>> memberships = new HashMap<Role, List<Group>>();
+        Map<Role, Boolean> complete = new HashMap<Role, Boolean>();
 
         protected GroupMemberships()
         {
         }
 
+        // only allow one in a set - makes clearCache simple too
+        public boolean equals(Object rhs)
+        {
+            if (rhs != null && rhs instanceof GroupMemberships)
+                return true;
+            return false;
+        }
+
+        public int compareTo(Object t)
+        {
+            if (this.equals(t))
+                return 0;
+            return -1; // wonder if this is sketchy
+        }
     }
 
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClientMain.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClientMain.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4a1fa6d6561729b0e67bfcffe8b46d894c9d4a2
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClientMain.java
@@ -0,0 +1,276 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2014.                            (c) 2014.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *  $Revision: 4 $
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.client;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.User;
+import java.net.URI;
+import java.net.URL;
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.auth.CertCmdArgUtil;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.reg.client.RegistryClient;
+import ca.nrc.cadc.util.ArgumentMap;
+import ca.nrc.cadc.util.Log4jInit;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.util.Set;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * Prototype main class for the GMSClient.  Currently
+ * only used for testing.  Should not be used for production
+ * work.
+ */
+public class GMSClientMain implements PrivilegedAction<Object>
+{
+
+    private static Logger log = Logger.getLogger(GMSClientMain.class);
+
+    public static final String ARG_ADD_MEMBER = "add-member";
+    public static final String ARG_CREATE_GROUP = "create";
+    public static final String ARG_GET_GROUP = "get";
+    public static final String ARG_DELETE_GROUP = "delete";
+
+    public static final String ARG_USERID = "userid";
+    public static final String ARG_GROUP = "group";
+
+    public static final String ARG_HELP = "help";
+    public static final String ARG_VERBOSE = "verbose";
+    public static final String ARG_DEBUG = "debug";
+    public static final String ARG_H = "h";
+    public static final String ARG_V = "v";
+    public static final String ARG_D = "d";
+
+    private GMSClient client;
+    private ArgumentMap argMap;
+
+    private GMSClientMain()
+    {
+        RegistryClient regClient = new RegistryClient();
+        URL acURL = null;
+        try
+        {
+            acURL = regClient.getServiceURL(new URI("ivo://cadc.nrc.ca/canfargms"), "https");
+        }
+        catch (Exception e)
+        {
+            log.error("FAIL", e);
+        }
+        log.info("GMS service URL: " + acURL);
+        client = new GMSClient(acURL.toString());
+    }
+
+    public static void main(String[] args)
+    {
+        ArgumentMap argMap = new ArgumentMap(args);
+
+        if (argMap.isSet(ARG_HELP) || argMap.isSet(ARG_H))
+        {
+            usage();
+            System.exit(0);
+        }
+
+        // Set debug mode
+        if (argMap.isSet(ARG_DEBUG) || argMap.isSet(ARG_D))
+        {
+            Log4jInit.setLevel("ca.nrc.cadc.ac.client", Level.DEBUG);
+            Log4jInit.setLevel("ca.nrc.cadc.net", Level.DEBUG);
+        }
+        else if (argMap.isSet(ARG_VERBOSE) || argMap.isSet(ARG_V))
+        {
+            Log4jInit.setLevel("ca.nrc.cadc.ac.client", Level.INFO);
+        }
+        else
+            Log4jInit.setLevel("ca", Level.WARN);
+
+        GMSClientMain main = new GMSClientMain();
+        main.argMap = argMap;
+
+        Subject subject = CertCmdArgUtil.initSubject(argMap, true);
+
+        Object response = null;
+
+        if (subject != null)
+            response = Subject.doAs(subject, main);
+        else
+            response = main.run();
+
+        log.debug("Response: " + response);
+    }
+
+    private String getCommand()
+    {
+        if (argMap.isSet(ARG_ADD_MEMBER))
+            return ARG_ADD_MEMBER;
+        
+        if (argMap.isSet(ARG_CREATE_GROUP))
+            return ARG_CREATE_GROUP;
+        
+        if (argMap.isSet(ARG_GET_GROUP))
+            return ARG_GET_GROUP;
+        
+        if (argMap.isSet(ARG_DELETE_GROUP))
+            return ARG_DELETE_GROUP;
+
+        throw new IllegalArgumentException("No valid commands");
+    }
+
+    private static void usage()
+    {
+        System.out.println("--add-member --group=<g> --userid=<u>");
+        System.out.println("--create --group=<g>");
+        System.out.println("--get --group=<g>");
+        System.out.println("--delete --group=<g>");
+    }
+
+    @Override
+    public Object run()
+    {
+        try
+        {
+            String command = getCommand();
+
+            if (command.equals(ARG_ADD_MEMBER))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                String userID = argMap.getValue(ARG_USERID);
+
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+
+                if (userID == null)
+                    throw new IllegalArgumentException("No userid specified");
+
+                client.addUserMember(group, new HttpPrincipal(userID));
+            }
+            else if (command.equals(ARG_CREATE_GROUP))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+                
+                AccessControlContext accessControlContext = AccessController.getContext();
+                Subject subject = Subject.getSubject(accessControlContext);
+                Set<X500Principal> principals = subject.getPrincipals(X500Principal.class);
+                X500Principal p = principals.iterator().next();
+                
+                Group g = new Group(group, new User(p));
+                g.getUserMembers().add(g.getOwner());
+                client.createGroup(g);
+            }
+            else if (command.equals(ARG_GET_GROUP))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+             
+                Group g = client.getGroup(group);
+                System.out.println("found: " + g.getID());
+                System.out.println("\t" + g.description);
+                System.out.println("owner: " + g.getOwner());
+                
+                for (User u : g.getUserAdmins())
+                    System.out.println("admin: " + u);
+                
+                for (Group ga : g.getGroupAdmins())
+                    System.out.println("admin: " + ga);
+                
+                for (User u : g.getUserMembers())
+                    System.out.println("member: " + u);
+                
+                for (Group gm : g.getGroupMembers())
+                    System.out.println("member: " + gm);
+                
+            }
+            else if (command.equals(ARG_DELETE_GROUP))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+             
+                client.deleteGroup(group);
+            }
+
+            return null;
+        }
+        catch (Throwable t)
+        {
+            log.error("ERROR", t);
+            return t;
+        }
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/UserClient.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/UserClient.java
index c108f030c4fb867b52b98649648e60c99f5d2243..a22a7519f360b2598a9728110b0163e6dfab2f3c 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/UserClient.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/UserClient.java
@@ -264,7 +264,7 @@ public class UserClient
             }
             else if (principal instanceof NumericPrincipal)
             {
-            	idTypeStr = IdentityType.UID.getValue();
+            	idTypeStr = IdentityType.CADC.getValue();
             }
             else if (principal instanceof CookiePrincipal)
             {
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
index a240184be0794193897d8924ce1002747b5ecb4c..e5997162863ce13b0f560bb579d4b7e492d8b851 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
@@ -76,37 +76,15 @@ import org.jdom2.Document;
 import org.json.JSONException;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.Reader;
 import java.util.Scanner;
 
+/**
+ * Class to read an JSON representation of a list of Groups
+ * into a list of Group objects.
+ */
 public class JsonGroupReader extends GroupReader
 {
-    /**
-     * Construct a Group from a InputStream.
-     *
-     * @param in InputStream.
-     * @return Group Group.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public Group read(InputStream in)
-        throws ReaderException, IOException
-    {
-        if (in == null)
-        {
-            throw new IOException("stream closed");
-        }
-        InputStreamReader reader;
-
-        Scanner s = new Scanner(in).useDelimiter("\\A");
-        String json = s.hasNext() ? s.next() : "";
-
-        return read(json);
-    }
-
     /**
      * Construct a Group from a Reader.
      *
@@ -127,27 +105,6 @@ public class JsonGroupReader extends GroupReader
         Scanner s = new Scanner(reader).useDelimiter("\\A");
         String json = s.hasNext() ? s.next() : "";
 
-        return read(json);
-    }
-
-    /**
-     * Construct a Group from an JSON String source.
-     *
-     * @param json String of JSON.
-     * @return Group Group.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public Group read(String json)
-        throws ReaderException, IOException
-    {
-        if (json == null)
-        {
-            throw new IllegalArgumentException("JSON must not be null");
-        }
-
-        // Create a JSONObject from the JSON
         try
         {
             JsonInputter jsonInputter = new JsonInputter();
@@ -160,12 +117,12 @@ public class JsonGroupReader extends GroupReader
             jsonInputter.getListElementMap().put("userAdmins", "user");
 
             Document document = jsonInputter.input(json);
-            return GroupReader.parseGroup(document.getRootElement());
+            return getGroup(document.getRootElement());
         }
         catch (JSONException e)
         {
             String error = "Unable to parse JSON to Group because " +
-                           e.getMessage();
+                e.getMessage();
             throw new ReaderException(error, e);
         }
     }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java
index 2db229327ce52306744846b95f0a4ffb19f310ce..864e14df31befef998f29f0cfaafaca5ecc4216b 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java
@@ -71,57 +71,18 @@ package ca.nrc.cadc.ac.json;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.ac.xml.GroupWriter;
-import ca.nrc.cadc.util.StringBuilderWriter;
 import ca.nrc.cadc.xml.JsonOutputter;
 import org.jdom2.Document;
 import org.jdom2.Element;
 
-import java.io.BufferedWriter;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 
+/**
+ * Class to write a JSON representation of a Group object.
+ */
 public class JsonGroupWriter extends GroupWriter
 {
-    /**
-     * Write a Group to a StringBuilder.
-     * @param group
-     * @param builder
-     * @throws IOException
-     * @throws WriterException
-     */
-    public void write(Group group, StringBuilder builder)
-        throws IOException, WriterException
-    {
-        write(group, new StringBuilderWriter(builder));
-    }
-
-    /**
-     * Write a Group to an OutputStream.
-     *
-     * @param group Group to write.
-     * @param out OutputStream to write to.
-     * @throws IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    @Override
-    public void write(Group group, OutputStream out)
-        throws IOException, WriterException
-    {
-        OutputStreamWriter outWriter;
-        try
-        {
-            outWriter = new OutputStreamWriter(out, "UTF-8");
-        }
-        catch (UnsupportedEncodingException e)
-        {
-            throw new RuntimeException("UTF-8 encoding not supported", e);
-        }
-        write(group, new BufferedWriter(outWriter));
-    }
-
     /**
      * Write a Group to a Writer.
      *
@@ -139,7 +100,7 @@ public class JsonGroupWriter extends GroupWriter
             throw new WriterException("null group");
         }
 
-        Element children = GroupWriter.getGroupElement(group);
+        Element children = getElement(group);
         Element groupElement = new Element("group");
         groupElement.addContent(children);
         Document document = new Document();
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e63adedabde2c2c9a704f5b05dc5c2c1f791a2f
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListReader.java
@@ -0,0 +1,127 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2015.                            (c) 2015.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.json;
+
+import ca.nrc.cadc.ac.ReaderException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.xml.UserListReader;
+import ca.nrc.cadc.xml.JsonInputter;
+import org.jdom2.Document;
+import org.json.JSONException;
+
+import java.io.Reader;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * Class to read an JSON representation of a Collection of Users
+ * into a list of User objects.
+ */
+public class JsonUserListReader extends UserListReader
+{
+    /**
+     * Construct a list of Users from a Reader.
+     *
+     * @param reader Reader.
+     * @return users List of Users.
+     * @throws ReaderException
+     * @throws URISyntaxException
+     */
+    @Override
+    public List<User<Principal>> read(Reader reader)
+        throws URISyntaxException, ReaderException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        Scanner s = new Scanner(reader).useDelimiter("\\A");
+        String json = s.hasNext() ? s.next() : "";
+
+        try
+        {
+            JsonInputter jsonInputter = new JsonInputter();
+            jsonInputter.getListElementMap().put("identities", "identity");
+            jsonInputter.getListElementMap().put("details", "userDetails");
+
+            Document document = jsonInputter.input(json);
+            return getUserList(document.getRootElement());
+        }
+        catch (JSONException e)
+        {
+            String error = "Unable to parse JSON to list of Users because " +
+                e.getMessage();
+            throw new ReaderException(error, e);
+        }
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
index 5bf9317a44b6c30c61b50f569471f652dc875daa..9a8acad66057c4c6c16e8ab52c5baf29de49f175 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
@@ -68,58 +68,54 @@
 
 package ca.nrc.cadc.ac.json;
 
-import ca.nrc.cadc.ac.PersonalDetails;
-import org.json.JSONException;
-import org.json.JSONWriter;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.ac.xml.UserListWriter;
+import ca.nrc.cadc.xml.JsonOutputter;
+import org.jdom2.Document;
+import org.jdom2.Element;
 
 import java.io.IOException;
 import java.io.Writer;
-import java.util.Map;
-
+import java.security.Principal;
+import java.util.Collection;
 
 /**
  * Class to write out, as JSON, a list of user entries.
  */
-public class JsonUserListWriter
+public class JsonUserListWriter extends UserListWriter
 {
-    public static void write(final Map<String, PersonalDetails> users,
-                             final Writer writer) throws IOException
+    /**
+     * Write a Collection of Users to a Writer.
+     *
+     * @param users  Users to write.
+     * @param writer Writer to write to.
+     * @throws IOException     if the writer fails to write.
+     * @throws WriterException
+     */
+    @Override
+    public <T extends Principal> void write(Collection<User<T>> users, Writer writer)
+        throws IOException, WriterException
     {
-        final JSONWriter jsonWriter = new JSONWriter(writer);
-
-        try
+        if (users == null)
         {
-            jsonWriter.array();
-
-            for (final Map.Entry<String, PersonalDetails> entry
-                    : users.entrySet())
-            {
-                jsonWriter.object();
-
-                jsonWriter.key("id").value(entry.getKey());
-                jsonWriter.key("firstName").value(entry.getValue().
-                        getFirstName());
-                jsonWriter.key("lastName").value(entry.getValue().
-                        getLastName());
-
-                jsonWriter.endObject();
-                writer.write("\n");
-            }
+            throw new WriterException("null users");
         }
-        catch (JSONException e)
-        {
-            throw new IOException(e);
-        }
-        finally
+
+        Element usersElement = new Element("users");
+        for (User<? extends Principal> user : users)
         {
-            try
-            {
-                jsonWriter.endArray();
-            }
-            catch (JSONException e)
-            {
-                // Do nothing.
-            }
+            Element userElement = new Element(("user"));
+            userElement.addContent(getElement(user));
         }
+        Document document = new Document();
+        document.setRootElement(usersElement);
+
+        JsonOutputter jsonOutputter = new JsonOutputter();
+        jsonOutputter.getListElementNames().add("identities");
+        jsonOutputter.getListElementNames().add("details");
+
+        jsonOutputter.output(document, writer);
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
index 2c4a8e376ec338c5fa2157d08767ca79f8999d5d..4ef9ee9afa59ff7ea34631b2f569fcc5c718644f 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
@@ -76,36 +76,15 @@ import org.jdom2.Document;
 import org.json.JSONException;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.Reader;
 import java.security.Principal;
 import java.util.Scanner;
 
+/**
+ * Class to read a JSON representation of a User to a User object.
+ */
 public class JsonUserReader extends UserReader
 {
-    /**
-     * Construct a User from a InputStream.
-     *
-     * @param in InputStream.
-     * @return User User.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public User<Principal> read(InputStream in)
-        throws IOException
-    {
-        if (in == null)
-        {
-            throw new IOException("stream closed");
-        }
-
-        Scanner s = new Scanner(in).useDelimiter("\\A");
-        String json = s.hasNext() ? s.next() : "";
-
-        return read(json);
-    }
-
     /**
      * Construct a User from a Reader.
      *
@@ -126,27 +105,6 @@ public class JsonUserReader extends UserReader
         Scanner s = new Scanner(reader).useDelimiter("\\A");
         String json = s.hasNext() ? s.next() : "";
 
-        return read(json);
-    }
-
-    /**
-     * Construct a User from an JSON String source.
-     *
-     * @param json String of JSON.
-     * @return User User.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public User<Principal> read(String json)
-        throws IOException
-    {
-        if (json == null || json.isEmpty())
-        {
-            throw new IllegalArgumentException("JSON must not be null or empty");
-        }
-
-        // Create a JSONObject from the JSON
         try
         {
             JsonInputter jsonInputter = new JsonInputter();
@@ -154,7 +112,7 @@ public class JsonUserReader extends UserReader
             jsonInputter.getListElementMap().put("details", "userDetails");
 
             Document document = jsonInputter.input(json);
-            return UserReader.parseUser(document.getRootElement());
+            return getUser(document.getRootElement());
         }
         catch (JSONException e)
         {
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
index b8d61e8cea9567a2caac1beb562766a5e05b1bfa..eb03dbb01e2655358798886e44be0d896b867d9e 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
@@ -76,36 +76,15 @@ import org.jdom2.Document;
 import org.json.JSONException;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.Reader;
 import java.security.Principal;
 import java.util.Scanner;
 
+/**
+ * Class to read a JSON representation of a UserRequest to a UserRequest object.
+ */
 public class JsonUserRequestReader extends UserRequestReader
 {
-    /**
-     * Construct a User from a InputStream.
-     *
-     * @param in InputStream.
-     * @return User User.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public UserRequest<Principal> read(InputStream in)
-            throws IOException
-    {
-        if (in == null)
-        {
-            throw new IOException("stream closed");
-        }
-
-        Scanner s = new Scanner(in).useDelimiter("\\A");
-        String json = s.hasNext() ? s.next() : "";
-
-        return read(json);
-    }
-
     /**
      * Construct a User from a Reader.
      *
@@ -126,41 +105,20 @@ public class JsonUserRequestReader extends UserRequestReader
         Scanner s = new Scanner(reader).useDelimiter("\\A");
         String json = s.hasNext() ? s.next() : "";
 
-        return read(json);
-    }
-
-    /**
-     * Construct a UserRequest from an JSON String source.
-     *
-     * @param json String of the JSON.
-     * @return UserRequest UserRequest.
-     * @throws IOException
-     */
-    @Override
-    public UserRequest<Principal> read(String json)
-        throws IOException
-    {
-        if (json == null)
+        try
         {
-            throw new IllegalArgumentException("JSON must not be null");
+            JsonInputter jsonInputter = new JsonInputter();
+            jsonInputter.getListElementMap().put("identities", "identity");
+            jsonInputter.getListElementMap().put("details", "userDetails");
+
+            Document document = jsonInputter.input(json);
+            return getUserRequest(document.getRootElement());
         }
-        else
+        catch (JSONException e)
         {
-            try
-            {
-                JsonInputter jsonInputter = new JsonInputter();
-                jsonInputter.getListElementMap().put("identities", "identity");
-                jsonInputter.getListElementMap().put("details", "userDetails");
-
-                Document document = jsonInputter.input(json);
-                return UserRequestReader.parseUserRequest(document.getRootElement());
-            }
-            catch (JSONException e)
-            {
-                String error = "Unable to parse JSON to User because " +
-                    e.getMessage();
-                throw new ReaderException(error, e);
-            }
+            String error = "Unable to parse JSON to User because " +
+                e.getMessage();
+            throw new ReaderException(error, e);
         }
     }
 
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
index 7ebb07a652a9b972252d528e869ac86938d33c12..6cf6e6f742b009f319f25371126cbe777de768da 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
@@ -1,9 +1,77 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  Government of Canada                 Gouvernement du Canada
+*  National Research Council            Conseil national de recherches
+*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+*  All rights reserved                  Tous droits réservés
+*
+*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+*  expressed, implied, or               énoncée, implicite ou légale,
+*  statutory, of any kind with          de quelque nature que ce
+*  respect to the software,             soit, concernant le logiciel,
+*  including without limitation         y compris sans restriction
+*  any warranty of merchantability      toute garantie de valeur
+*  or fitness for a particular          marchande ou de pertinence
+*  purpose. NRC shall not be            pour un usage particulier.
+*  liable in any event for any          Le CNRC ne pourra en aucun cas
+*  damages, whether direct or           être tenu responsable de tout
+*  indirect, special or general,        dommage, direct ou indirect,
+*  consequential or incidental,         particulier ou général,
+*  arising from the use of the          accessoire ou fortuit, résultant
+*  software.  Neither the name          de l'utilisation du logiciel. Ni
+*  of the National Research             le nom du Conseil National de
+*  Council of Canada nor the            Recherches du Canada ni les noms
+*  names of its contributors may        de ses  participants ne peuvent
+*  be used to endorse or promote        être utilisés pour approuver ou
+*  products derived from this           promouvoir les produits dérivés
+*  software without specific prior      de ce logiciel sans autorisation
+*  written permission.                  préalable et particulière
+*                                       par écrit.
+*
+*  This file is part of the             Ce fichier fait partie du projet
+*  OpenCADC project.                    OpenCADC.
+*
+*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+*  you can redistribute it and/or       vous pouvez le redistribuer ou le
+*  modify it under the terms of         modifier suivant les termes de
+*  the GNU Affero General Public        la “GNU Affero General Public
+*  License as published by the          License” telle que publiée
+*  Free Software Foundation,            par la Free Software Foundation
+*  either version 3 of the              : soit la version 3 de cette
+*  License, or (at your option)         licence, soit (à votre gré)
+*  any later version.                   toute version ultérieure.
+*
+*  OpenCADC is distributed in the       OpenCADC est distribué
+*  hope that it will be useful,         dans l’espoir qu’il vous
+*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+*  without even the implied             GARANTIE : sans même la garantie
+*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+*  General Public License for           Générale Publique GNU Affero
+*  more details.                        pour plus de détails.
+*
+*  You should have received             Vous devriez avoir reçu une
+*  a copy of the GNU Affero             copie de la Licence Générale
+*  General Public License along         Publique GNU Affero avec
+*  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+*                                       <http://www.gnu.org/licenses/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
 package ca.nrc.cadc.ac.json;
 
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.ac.xml.UserRequestWriter;
-import ca.nrc.cadc.util.StringBuilderWriter;
 import ca.nrc.cadc.xml.JsonOutputter;
 import org.jdom2.Document;
 import org.jdom2.Element;
@@ -17,20 +85,6 @@ import java.security.Principal;
  */
 public class JsonUserRequestWriter extends UserRequestWriter
 {
-    /**
-     * Write a UserRequest to a StringBuilder.
-     *
-     * @param userRequest UserRequest to write.
-     * @param builder StringBuilder to write to.
-     * @throws java.io.IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    public void write(UserRequest<? extends Principal> userRequest, StringBuilder builder)
-        throws IOException, WriterException
-    {
-        write(userRequest, new StringBuilderWriter(builder));
-    }
-
     /**
      * Write a UserRequest to a Writer.
      *
@@ -39,7 +93,8 @@ public class JsonUserRequestWriter extends UserRequestWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(UserRequest<? extends Principal> userRequest, Writer writer)
+    @Override
+    public <T extends Principal> void write(UserRequest<T> userRequest, Writer writer)
         throws IOException, WriterException
     {
         if (userRequest == null)
@@ -47,7 +102,7 @@ public class JsonUserRequestWriter extends UserRequestWriter
             throw new WriterException("null UserRequest");
         }
 
-        Element children = UserRequestWriter.getUserRequestElement(userRequest);
+        Element children = getElement(userRequest);
         Element userRequestElement = new Element("userRequest");
         userRequestElement.addContent(children);
         Document document = new Document();
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
index 444a670a4453e0bd2e6b7f481fb9247e73df0525..3b9004ff24ae028ccd7b6027e2ce18b77394c902 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
@@ -69,68 +69,21 @@
 package ca.nrc.cadc.ac.json;
 
 import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserDetails;
 import ca.nrc.cadc.ac.WriterException;
-import ca.nrc.cadc.ac.xml.GroupWriter;
 import ca.nrc.cadc.ac.xml.UserWriter;
-import ca.nrc.cadc.util.StringBuilderWriter;
 import ca.nrc.cadc.xml.JsonOutputter;
 import org.jdom2.Document;
 import org.jdom2.Element;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
 
-import java.io.BufferedWriter;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.security.Principal;
-import java.util.Set;
 
+/**
+ * Class to write a JSON representation of a User object.
+ */
 public class JsonUserWriter extends UserWriter
 {
-    /**
-     * Write a User as a JSON string to a StringBuilder.
-     * 
-     * @param user User to write.
-     * @param builder StringBuilder to write to.
-     * @throws IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    @Override
-    public void write(User<? extends Principal> user, StringBuilder builder)
-        throws IOException, WriterException
-    {
-        write(user, new StringBuilderWriter(builder));
-    }
-
-    /**
-     * Write a User as a JSON string to an OutputStream.
-     *
-     * @param user User to write.
-     * @param out OutputStream to write to.
-     * @throws IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    @Override
-    public void write(User<? extends Principal> user, OutputStream out)
-        throws IOException, WriterException
-    {                
-        OutputStreamWriter outWriter;
-        try
-        {
-            outWriter = new OutputStreamWriter(out, "UTF-8");
-        }
-        catch (UnsupportedEncodingException e)
-        {
-            throw new RuntimeException("UTF-8 encoding not supported", e);
-        }
-        write(user, new BufferedWriter(outWriter));
-    }
-
     /**
      * Write a User as a JSON string to a Writer.
      *
@@ -140,7 +93,7 @@ public class JsonUserWriter extends UserWriter
      * @throws WriterException
      */
     @Override
-    public void write(User<? extends Principal> user, Writer writer)
+    public<T extends Principal> void write(User<T> user, Writer writer)
         throws IOException, WriterException
     {
         if (user == null)
@@ -148,7 +101,7 @@ public class JsonUserWriter extends UserWriter
             throw new WriterException("null User");
         }
 
-        Element children = UserWriter.getUserElement(user);
+        Element children = getElement(user);
         Element userElement = new Element("user");
         userElement.addContent(children);
         Document document = new Document();
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/AbstractReaderWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/AbstractReaderWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7401185b0487eafa718057a3fa74732f74f0e6bc
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/AbstractReaderWriter.java
@@ -0,0 +1,1017 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  Government of Canada                 Gouvernement du Canada
+*  National Research Council            Conseil national de recherches
+*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+*  All rights reserved                  Tous droits réservés
+*
+*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+*  expressed, implied, or               énoncée, implicite ou légale,
+*  statutory, of any kind with          de quelque nature que ce
+*  respect to the software,             soit, concernant le logiciel,
+*  including without limitation         y compris sans restriction
+*  any warranty of merchantability      toute garantie de valeur
+*  or fitness for a particular          marchande ou de pertinence
+*  purpose. NRC shall not be            pour un usage particulier.
+*  liable in any event for any          Le CNRC ne pourra en aucun cas
+*  damages, whether direct or           être tenu responsable de tout
+*  indirect, special or general,        dommage, direct ou indirect,
+*  consequential or incidental,         particulier ou général,
+*  arising from the use of the          accessoire ou fortuit, résultant
+*  software.  Neither the name          de l'utilisation du logiciel. Ni
+*  of the National Research             le nom du Conseil National de
+*  Council of Canada nor the            Recherches du Canada ni les noms
+*  names of its contributors may        de ses  participants ne peuvent
+*  be used to endorse or promote        être utilisés pour approuver ou
+*  products derived from this           promouvoir les produits dérivés
+*  software without specific prior      de ce logiciel sans autorisation
+*  written permission.                  préalable et particulière
+*                                       par écrit.
+*
+*  This file is part of the             Ce fichier fait partie du projet
+*  OpenCADC project.                    OpenCADC.
+*
+*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+*  you can redistribute it and/or       vous pouvez le redistribuer ou le
+*  modify it under the terms of         modifier suivant les termes de
+*  the GNU Affero General Public        la “GNU Affero General Public
+*  License as published by the          License” telle que publiée
+*  Free Software Foundation,            par la Free Software Foundation
+*  either version 3 of the              : soit la version 3 de cette
+*  License, or (at your option)         licence, soit (à votre gré)
+*  any later version.                   toute version ultérieure.
+*
+*  OpenCADC is distributed in the       OpenCADC est distribué
+*  hope that it will be useful,         dans l’espoir qu’il vous
+*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+*  without even the implied             GARANTIE : sans même la garantie
+*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+*  General Public License for           Générale Publique GNU Affero
+*  more details.                        pour plus de détails.
+*
+*  You should have received             Vous devriez avoir reçu une
+*  a copy of the GNU Affero             copie de la Licence Générale
+*  General Public License along         Publique GNU Affero avec
+*  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+*                                       <http://www.gnu.org/licenses/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
+package ca.nrc.cadc.ac.xml;
+
+import ca.nrc.cadc.ac.AC;
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupProperty;
+import ca.nrc.cadc.ac.IdentityType;
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.PosixDetails;
+import ca.nrc.cadc.ac.ReaderException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserDetails;
+import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+import ca.nrc.cadc.auth.OpenIdPrincipal;
+import ca.nrc.cadc.date.DateUtil;
+import org.jdom2.Attribute;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.Writer;
+import java.security.Principal;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * AbstractReaderWriter TODO describe class
+ */
+public abstract class AbstractReaderWriter
+{
+    /**
+     * Write to root Element to a writer.
+     *
+     * @param root Root Element to write.
+     * @param writer Writer to write to.
+     * @throws IOException if the writer fails to write.
+     */
+    protected void write(Element root, Writer writer)
+        throws IOException
+    {
+        XMLOutputter outputter = new XMLOutputter();
+        outputter.setFormat(Format.getPrettyFormat());
+        outputter.output(new Document(root), writer);
+    }
+
+    /**
+     * Get a User object from a JDOM element.
+     *
+     * @param element The User JDOM element.
+     * @return A User object.
+     * @throws ReaderException
+     */
+    protected final User<Principal> getUser(Element element)
+        throws ReaderException
+    {
+        // userID element of the User element
+        Element userIDElement = element.getChild("userID");
+        if (userIDElement == null)
+        {
+            String error = "userID element not found in user element";
+            throw new ReaderException(error);
+        }
+
+        // identity element of the userID element
+        Element userIDIdentityElement = userIDElement.getChild("identity");
+        if (userIDIdentityElement == null)
+        {
+            String error = "identity element not found in userID element";
+            throw new ReaderException(error);
+        }
+
+        Principal userID = getPrincipal(userIDIdentityElement);
+        User<Principal> user = new User<Principal>(userID);
+
+        // identities
+        Element identitiesElement = element.getChild("identities");
+        if (identitiesElement != null)
+        {
+            List<Element> identityElements = identitiesElement.getChildren("identity");
+            for (Element identityElement : identityElements)
+            {
+                user.getIdentities().add(getPrincipal(identityElement));
+            }
+
+        }
+
+        // details
+        Element detailsElement = element.getChild("details");
+        if (detailsElement != null)
+        {
+            List<Element> userDetailsElements = detailsElement.getChildren("userDetails");
+            for (Element userDetailsElement : userDetailsElements)
+            {
+                user.details.add(getUserDetails(userDetailsElement));
+            }
+        }
+
+        return user;
+    }
+
+    /**
+     * Get a UserRequest object from a JDOM element.
+     *
+     * @param element The UserRequest JDOM element.
+     * @return A UserRequest object.
+     * @throws ReaderException
+     */
+    protected final UserRequest<Principal> getUserRequest(Element element)
+        throws ReaderException
+    {
+        // user element of the UserRequest element
+        Element userElement = element.getChild("user");
+        if (userElement == null)
+        {
+            String error = "user element not found in userRequest element";
+            throw new ReaderException(error);
+        }
+        User<Principal> user = getUser(userElement);
+
+        // password element of the userRequest element
+        Element passwordElement = element.getChild("password");
+        if (passwordElement == null)
+        {
+            String error = "password element not found in userRequest element";
+            throw new ReaderException(error);
+        }
+        String password = passwordElement.getText();
+
+        return new UserRequest<Principal>(user, password.toCharArray());
+    }
+
+    /**
+     * Get a Principal object from a JDOM element.
+     *
+     * @param element The Principal JDOM element.
+     * @return A Principal object.
+     * @throws ReaderException
+     */
+    protected final Principal getPrincipal(Element element)
+        throws ReaderException
+    {
+        if (element == null)
+        {
+            String error = "null identity element";
+            throw new ReaderException(error);
+        }
+
+        if (!element.getName().equals("identity"))
+        {
+            String error = "expected identity element name, found " +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String type = element.getAttributeValue("type");
+        if (type == null)
+        {
+            String error = "type attribute not found in identity element" +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String identity = element.getText();
+        Principal principal;
+        if (type.equals(IdentityType.OPENID.getValue()))
+        {
+            principal = new OpenIdPrincipal(identity);
+        }
+        else if (type.equals(IdentityType.CADC.getValue()))
+        {
+            Integer cadcID;
+            try
+            {
+                cadcID = Integer.valueOf(identity);
+            }
+            catch (NumberFormatException e)
+            {
+                String error = "Non-integer cadcID: " + identity;
+                throw new ReaderException(error);
+            }
+            principal = new NumericPrincipal(cadcID);
+        }
+        else if (type.equals(IdentityType.USERNAME.getValue()))
+        {
+            principal = new HttpPrincipal(identity);
+        }
+        else if (type.equals(IdentityType.X500.getValue()))
+        {
+            principal = new X500Principal(identity);
+        }
+        else
+        {
+            String error = "Unknown type attribute: " + type;
+            throw new ReaderException(error);
+        }
+
+        return principal;
+    }
+
+    /**
+     * Get a UserDetails object from a JDOM element.
+     *
+     * @param element The UserDetails JDOM element.
+     * @return A UserDetails object.
+     * @throws ReaderException
+     */
+    protected final UserDetails getUserDetails(Element element)
+        throws ReaderException
+    {
+        if (element == null)
+        {
+            throw new ReaderException("null UserDetails");
+        }
+
+        if (!element.getName().equals(UserDetails.NAME))
+        {
+            String error = "expected element name userDetails, found " +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String type = element.getAttributeValue(UserDetails.TYPE_ATTRIBUTE);
+        if (type == null)
+        {
+            String error = "userDetails missing required attribute type";
+            throw new ReaderException(error);
+        }
+
+        if (type.equals(PosixDetails.NAME))
+        {
+            return getPosixDetails(element);
+        }
+        if (type.equals(PersonalDetails.NAME))
+        {
+            return getPersonalDetails(element);
+        }
+
+        String error = "Unknown UserDetails attribute type " + type;
+        throw new ReaderException(error);
+    }
+
+    /**
+     * Get a PosixDetails object from a JDOM element.
+     *
+     * @param element The PosixDetails JDOM element.
+     * @return A PosixDetails object.
+     * @throws ReaderException
+     */
+    protected final PosixDetails getPosixDetails(Element element)
+        throws ReaderException
+    {
+        // uid
+        Element uidElement = element.getChild(PosixDetails.UID);
+        if (uidElement == null)
+        {
+            String error = "posixDetails missing required element uid";
+            throw new ReaderException(error);
+        }
+        long uid;
+        try
+        {
+            uid = Long.valueOf(uidElement.getText());
+        }
+        catch (NumberFormatException e)
+        {
+            String error = "Cannot parse posixDetails uid to a long";
+            throw new ReaderException(error);
+        }
+
+        // gid
+        Element gidElement = element.getChild(PosixDetails.GID);
+        if (gidElement == null)
+        {
+            String error = "posixDetails missing required element gid";
+            throw new ReaderException(error);
+        }
+        long gid;
+        try
+        {
+            gid = Long.valueOf(gidElement.getText());
+        }
+        catch (NumberFormatException e)
+        {
+            String error = "Cannot parse posixDetails gid to a long";
+            throw new ReaderException(error);
+        }
+
+        // homeDirectory
+        Element homeDirElement = element.getChild(PosixDetails.HOME_DIRECTORY);
+        if (homeDirElement == null)
+        {
+            String error = "posixDetails missing required element homeDirectory";
+            throw new ReaderException(error);
+        }
+        String homeDirectory = homeDirElement.getText();
+
+        return new PosixDetails(uid, gid, homeDirectory);
+    }
+
+    /**
+     * Get a PersonalDetails object from a JDOM element.
+     *
+     * @param element The PersonalDetails JDOM element.
+     * @return A PersonalDetails object.
+     * @throws ReaderException
+     */
+    protected final PersonalDetails getPersonalDetails(Element element)
+        throws ReaderException
+    {
+        // firstName
+        Element firstNameElement = element.getChild(PersonalDetails.FIRSTNAME);
+        if (firstNameElement == null)
+        {
+            String error = "personalDetails missing required element firstName";
+            throw new ReaderException(error);
+        }
+        String firstName = firstNameElement.getText();
+
+        // lastName
+        Element lastNameElement = element.getChild(PersonalDetails.LASTNAME);
+        if (lastNameElement == null)
+        {
+            String error = "personalDetails missing required element lastName";
+            throw new ReaderException(error);
+        }
+        String lastName = lastNameElement.getText();
+
+        PersonalDetails details = new PersonalDetails(firstName, lastName);
+
+        // email
+        Element emailElement = element.getChild(PersonalDetails.EMAIL);
+        if (emailElement != null)
+        {
+            details.email = emailElement.getText();
+        }
+
+        // address
+        Element addressElement = element.getChild(PersonalDetails.ADDRESS);
+        if (addressElement != null)
+        {
+            details.address = addressElement.getText();
+        }
+
+        // institute
+        Element instituteElement = element.getChild(PersonalDetails.INSTITUTE);
+        if (instituteElement != null)
+        {
+            details.institute = instituteElement.getText();
+        }
+
+        // city
+        Element cityElement = element.getChild(PersonalDetails.CITY);
+        if (cityElement != null)
+        {
+            details.city = cityElement.getText();
+        }
+
+        // country
+        Element countryElement = element.getChild(PersonalDetails.COUNTRY);
+        if (countryElement != null)
+        {
+            details.country = countryElement.getText();
+        }
+
+        return details;
+    }
+
+    /**
+     * Get a UserRequest object from a JDOM element.
+     *
+     * @param element The UserRequest JDOM element.
+     * @return A UserRequest object.
+     * @throws ReaderException
+     */
+    protected final Group getGroup(Element element)
+        throws ReaderException
+    {
+        String uri = element.getAttributeValue("uri");
+        if (uri == null)
+        {
+            String error = "group missing required uri attribute";
+            throw new ReaderException(error);
+        }
+
+        // Group groupID
+        int index = uri.indexOf(AC.GROUP_URI);
+        if (index == -1)
+        {
+            String error = "group uri attribute malformed: " + uri;
+            throw new ReaderException(error);
+        }
+        String groupID = uri.substring(AC.GROUP_URI.length());
+
+        // Group owner
+        User<? extends Principal> user = null;
+        Element ownerElement = element.getChild("owner");
+        if (ownerElement != null)
+        {
+            // Owner user
+            Element userElement = ownerElement.getChild("user");
+            if (userElement == null)
+            {
+                String error = "owner missing required user element";
+                throw new ReaderException(error);
+            }
+            user = getUser(userElement);
+        }
+
+        Group group = new Group(groupID, user);
+
+        // description
+        Element descriptionElement = element.getChild("description");
+        if (descriptionElement != null)
+        {
+            group.description = descriptionElement.getText();
+        }
+
+        // lastModified
+        Element lastModifiedElement = element.getChild("lastModified");
+        if (lastModifiedElement != null)
+        {
+            try
+            {
+                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
+                group.lastModified = df.parse(lastModifiedElement.getText());
+            }
+            catch (ParseException e)
+            {
+                String error = "Unable to parse group lastModified because " + e.getMessage();
+
+                throw new ReaderException(error);
+            }
+        }
+
+        // properties
+        Element propertiesElement = element.getChild("properties");
+        if (propertiesElement != null)
+        {
+            List<Element> propertyElements = propertiesElement.getChildren("property");
+            for (Element propertyElement : propertyElements)
+            {
+                group.getProperties().add(getGroupProperty(propertyElement));
+            }
+        }
+
+        // groupMembers
+        Element groupMembersElement = element.getChild("groupMembers");
+        if (groupMembersElement != null)
+        {
+            List<Element> groupElements = groupMembersElement.getChildren("group");
+            for (Element groupMember : groupElements)
+            {
+                group.getGroupMembers().add(getGroup(groupMember));
+            }
+        }
+
+        // userMembers
+        Element userMembersElement = element.getChild("userMembers");
+        if (userMembersElement != null)
+        {
+            List<Element> userElements = userMembersElement.getChildren("user");
+            for (Element userMember : userElements)
+            {
+                group.getUserMembers().add(getUser(userMember));
+            }
+        }
+
+        // groupAdmins
+        Element groupAdminsElement = element.getChild("groupAdmins");
+        if (groupAdminsElement != null)
+        {
+            List<Element> groupElements = groupAdminsElement.getChildren("group");
+            for (Element groupMember : groupElements)
+            {
+                group.getGroupAdmins().add(getGroup(groupMember));
+            }
+        }
+
+        // userAdmins
+        Element userAdminsElement = element.getChild("userAdmins");
+        if (userAdminsElement != null)
+        {
+            List<Element> userElements = userAdminsElement.getChildren("user");
+            for (Element userMember : userElements)
+            {
+                group.getUserAdmins().add(getUser(userMember));
+            }
+        }
+
+        return group;
+    }
+
+    /**
+     * Get a GroupProperty object from a JDOM element.
+     *
+     * @param element The GroupProperty JDOM element.
+     * @return A GroupProperty object.
+     * @throws ReaderException
+     */
+    protected final GroupProperty getGroupProperty(Element element)
+        throws ReaderException
+    {
+        if (element == null)
+        {
+            String error = "null property element";
+            throw new ReaderException(error);
+        }
+
+        if (!element.getName().equals(GroupProperty.NAME))
+        {
+            String error = "expected property element name, found " +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String key = element.getAttributeValue(GroupProperty.KEY_ATTRIBUTE);
+        if (key == null)
+        {
+            String error = "required key attribute not found";
+            throw new ReaderException(error);
+        }
+
+        String type = element.getAttributeValue(GroupProperty.TYPE_ATTRIBUTE);
+        if (type == null)
+        {
+            String error = "required type attribute not found";
+            throw new ReaderException(error);
+        }
+        Object value;
+        if (type.equals(GroupProperty.STRING_TYPE))
+        {
+            value = String.valueOf(element.getText());
+        }
+        else
+        {
+            if (type.equals(GroupProperty.INTEGER_TYPE))
+            {
+                value = Integer.valueOf(element.getText());
+            }
+            else
+            {
+                String error = "Unsupported GroupProperty type: " + type;
+                throw new ReaderException(error);
+            }
+        }
+        Boolean readOnly = Boolean.valueOf(element.getAttributeValue(GroupProperty.READONLY_ATTRIBUTE));
+
+        return new GroupProperty(key, value, readOnly);
+    }
+
+    /**
+     * Get a JDOM element from a User object.
+     *
+     * @param user The User.
+     * @return A JDOM User representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(User<? extends Principal> user)
+        throws WriterException
+    {
+        // Create the user Element.
+        Element userElement = new Element("user");
+
+        // userID element
+        Element userIDElement = new Element("userID");
+        userIDElement.addContent(getElement(user.getUserID()));
+        userElement.addContent(userIDElement);
+
+        // identities
+        Set<Principal> identities = user.getIdentities();
+        if (!identities.isEmpty())
+        {
+            Element identitiesElement = new Element("identities");
+            for (Principal identity : identities)
+            {
+                identitiesElement.addContent(getElement(identity));
+            }
+            userElement.addContent(identitiesElement);
+        }
+
+        // details
+        if (!user.details.isEmpty())
+        {
+            Element detailsElement = new Element("details");
+            Set<UserDetails> userDetails = user.details;
+            for (UserDetails userDetail : userDetails)
+            {
+                detailsElement.addContent(getElement(userDetail));
+            }
+            userElement.addContent(detailsElement);
+        }
+
+        return userElement;
+    }
+
+    /**
+     * Get a JDOM element from a UserRequest object.
+     *
+     * @param userRequest The UserRequest.
+     * @return A JDOM UserRequest representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(UserRequest<? extends Principal> userRequest)
+        throws WriterException
+    {
+        // Create the userRequest Element.
+        Element userRequestElement = new Element("userRequest");
+
+        // user element
+        Element userElement = getElement(userRequest.getUser());
+        userRequestElement.addContent(userElement);
+
+        // password element
+        Element passwordElement = new Element("password");
+        passwordElement.setText(String.valueOf(userRequest.getPassword()));
+        userRequestElement.addContent(passwordElement);
+
+        return userRequestElement;
+    }
+
+    /**
+     * Get a JDOM element from a Principal object.
+     *
+     * @param identity The Principal.
+     * @return A JDOM UserDetails representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(Principal identity)
+        throws WriterException
+    {
+        if (identity == null)
+        {
+            String error = "null identity";
+            throw new WriterException(error);
+        }
+
+        Element identityElement = new Element("identity");
+        if ((identity instanceof HttpPrincipal))
+        {
+            identityElement.setAttribute("type", IdentityType.USERNAME.getValue());
+        }
+        else if ((identity instanceof NumericPrincipal))
+        {
+            identityElement.setAttribute("type", IdentityType.CADC.getValue());
+        }
+        else if ((identity instanceof OpenIdPrincipal))
+        {
+            identityElement.setAttribute("type", IdentityType.OPENID.getValue());
+        }
+        else if ((identity instanceof X500Principal))
+        {
+            identityElement.setAttribute("type", IdentityType.X500.getValue());
+        }
+        else
+        {
+            String error = "Unsupported Principal type " +
+                identity.getClass().getSimpleName();
+            throw new IllegalArgumentException(error);
+        }
+        identityElement.setText(identity.getName());
+
+        return identityElement;
+    }
+
+    /**
+     * Get a JDOM element from a UserDetails object.
+     *
+     * @param details The UserDetails.
+     * @return A JDOM UserDetails representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(UserDetails details)
+        throws WriterException
+    {
+        if (details == null)
+        {
+            throw new WriterException("null UserDetails");
+        }
+
+        if ((details instanceof PosixDetails))
+        {
+            return getElement((PosixDetails) details);
+        }
+        if ((details instanceof PersonalDetails))
+        {
+            return getElement((PersonalDetails) details);
+        }
+
+        String error = "Unknown UserDetails implementation: " +
+            details.getClass().getName();
+        throw new WriterException(error);
+    }
+
+    /**
+     * Get a JDOM element from a PosixDetails object.
+     *
+     * @param details The PosixDetails.
+     * @return A JDOM PosixDetails representation.
+     */
+    protected final Element getElement(PosixDetails details)
+    {
+        Element detailsElement = new Element(UserDetails.NAME);
+        detailsElement.setAttribute(UserDetails.TYPE_ATTRIBUTE,
+            PosixDetails.NAME);
+
+        Element uidElement = new Element(PosixDetails.UID);
+        uidElement.setText(String.valueOf(details.getUid()));
+        detailsElement.addContent(uidElement);
+
+        Element gidElement = new Element(PosixDetails.GID);
+        gidElement.setText(String.valueOf(details.getGid()));
+        detailsElement.addContent(gidElement);
+
+        Element homeDirElement = new Element(PosixDetails.HOME_DIRECTORY);
+        homeDirElement.setText(details.getHomeDirectory());
+        detailsElement.addContent(homeDirElement);
+
+        return detailsElement;
+    }
+
+    /**
+     * Get a JDOM element from a PersonalDetails object.
+     *
+     * @param details The PersonalDetails.
+     * @return JDOM PersonalDetails representation.
+     */
+    protected final Element getElement(PersonalDetails details)
+    {
+        Element detailsElement = new Element(UserDetails.NAME);
+        detailsElement.setAttribute(UserDetails.TYPE_ATTRIBUTE,
+            PersonalDetails.NAME);
+
+        Element firstNameElement = new Element(PersonalDetails.FIRSTNAME);
+        firstNameElement.setText(details.getFirstName());
+        detailsElement.addContent(firstNameElement);
+
+        Element lastNameElement = new Element(PersonalDetails.LASTNAME);
+        lastNameElement.setText(details.getLastName());
+        detailsElement.addContent(lastNameElement);
+
+        if (details.email != null)
+        {
+            Element emailElement = new Element(PersonalDetails.EMAIL);
+            emailElement.setText(details.email);
+            detailsElement.addContent(emailElement);
+        }
+
+        if (details.address != null)
+        {
+            Element addressElement = new Element(PersonalDetails.ADDRESS);
+            addressElement.setText(details.address);
+            detailsElement.addContent(addressElement);
+        }
+
+        if (details.institute != null)
+        {
+            Element instituteElement = new Element(PersonalDetails.INSTITUTE);
+            instituteElement.setText(details.institute);
+            detailsElement.addContent(instituteElement);
+        }
+
+        if (details.city != null)
+        {
+            Element cityElement = new Element(PersonalDetails.CITY);
+            cityElement.setText(details.city);
+            detailsElement.addContent(cityElement);
+        }
+
+        if (details.country != null)
+        {
+            Element countryElement = new Element(PersonalDetails.COUNTRY);
+            countryElement.setText(details.country);
+            detailsElement.addContent(countryElement);
+        }
+
+        return detailsElement;
+    }
+
+    /**
+     * Get a JDOM element from a Group object.
+     *
+     * @param group The UserRequest.
+     * @return A JDOM Group representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(Group group)
+        throws WriterException
+    {
+        return getElement(group, true);
+    }
+
+    /**
+     * Get a JDOM element from a Group object.
+     *
+     * @param group The UserRequest.
+     * @param deepCopy Return all Group elements.
+     * @return A JDOM Group representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(Group group, boolean deepCopy)
+        throws WriterException
+    {
+        // Create the root group element.
+        Element groupElement = new Element("group");
+        String groupURI = AC.GROUP_URI + group.getID();
+        groupElement.setAttribute(new Attribute("uri", groupURI));
+
+        // Group owner
+        if (group.getOwner() != null)
+        {
+            Element ownerElement = new Element("owner");
+            Element userElement = getElement(group.getOwner());
+            ownerElement.addContent(userElement);
+            groupElement.addContent(ownerElement);
+        }
+
+        if (deepCopy)
+        {
+            // Group description
+            if (group.description != null)
+            {
+                Element descriptionElement = new Element("description");
+                descriptionElement.setText(group.description);
+                groupElement.addContent(descriptionElement);
+            }
+
+            // lastModified
+            if (group.lastModified != null)
+            {
+                Element lastModifiedElement = new Element("lastModified");
+                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
+                lastModifiedElement.setText(df.format(group.lastModified));
+                groupElement.addContent(lastModifiedElement);
+            }
+
+            // Group properties
+            if (!group.getProperties().isEmpty())
+            {
+                Element propertiesElement = new Element("properties");
+                for (GroupProperty property : group.getProperties())
+                {
+                    propertiesElement.addContent(getElement(property));
+                }
+                groupElement.addContent(propertiesElement);
+            }
+
+            // Group groupMembers.
+            if ((group.getGroupMembers() != null) && (!group.getGroupMembers().isEmpty()))
+            {
+                Element groupMembersElement = new Element("groupMembers");
+                for (Group groupMember : group.getGroupMembers())
+                {
+                    groupMembersElement.addContent(getElement(groupMember, false));
+                }
+                groupElement.addContent(groupMembersElement);
+            }
+
+            // Group userMembers
+            if ((group.getUserMembers() != null) && (!group.getUserMembers().isEmpty()))
+            {
+                Element userMembersElement = new Element("userMembers");
+                for (User<? extends Principal> userMember : group.getUserMembers())
+                {
+                    userMembersElement.addContent(getElement(userMember));
+                }
+                groupElement.addContent(userMembersElement);
+            }
+
+            // Group groupAdmins.
+            if ((group.getGroupAdmins() != null) && (!group.getGroupAdmins().isEmpty()))
+            {
+                Element groupAdminsElement = new Element("groupAdmins");
+                for (Group groupMember : group.getGroupAdmins())
+                {
+                    groupAdminsElement.addContent(getElement(groupMember, false));
+                }
+                groupElement.addContent(groupAdminsElement);
+            }
+
+            // Group userAdmins
+            if ((group.getUserAdmins() != null) && (!group.getUserAdmins().isEmpty()))
+            {
+                Element userAdminsElement = new Element("userAdmins");
+                for (User<? extends Principal> userMember : group.getUserAdmins())
+                {
+                    userAdminsElement.addContent(getElement(userMember));
+                }
+                groupElement.addContent(userAdminsElement);
+            }
+        }
+
+        return groupElement;
+    }
+
+    /**
+     * Get a JDOM element from a GroupProperty object.
+     *
+     * @param property The GroupProperty.
+     * @return A JDOM GroupProperty representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(GroupProperty property)
+        throws WriterException
+    {
+        if (property == null)
+        {
+            throw new WriterException("null GroupProperty");
+        }
+
+        Element propertyElement = new Element(GroupProperty.NAME);
+        propertyElement.setAttribute(GroupProperty.KEY_ATTRIBUTE,
+            property.getKey());
+        if (property.isReadOnly())
+        {
+            propertyElement.setAttribute(GroupProperty.READONLY_ATTRIBUTE,
+                "true");
+        }
+
+        Object value = property.getValue();
+        if ((value instanceof String))
+        {
+            propertyElement.setAttribute(GroupProperty.TYPE_ATTRIBUTE,
+                GroupProperty.STRING_TYPE);
+        }
+        else if ((value instanceof Integer))
+        {
+            propertyElement.setAttribute(GroupProperty.TYPE_ATTRIBUTE,
+                GroupProperty.INTEGER_TYPE);
+        }
+        else
+        {
+            String error = "Unsupported value type: " +
+                value.getClass().getSimpleName();
+            throw new IllegalArgumentException(error);
+        }
+        propertyElement.setText(String.valueOf(property.getValue()));
+
+        return propertyElement;
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
index cd962918e443f8d3db33cd7eead08d475285543d..da9f16c848b0c53e57051d62d4a241012b1392db 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
@@ -71,6 +71,10 @@ package ca.nrc.cadc.ac.xml;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.ReaderException;
 import ca.nrc.cadc.xml.XmlUtil;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -80,15 +84,12 @@ import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.JDOMException;
 
 /**
- * Class to read an XML representation of a list of Groups
- * into a List of Group objects.
+ * Class to read an XML representation of a List of Groups
+ * into a Collection of Group objects.
  */
-public class GroupListReader
+public class GroupListReader extends AbstractReaderWriter
 {
     /**
      * Construct a list of Group's from an XML String source.
@@ -110,7 +111,7 @@ public class GroupListReader
     }
 
     /**
-     * Construct a list of Group's from a InputStream.
+     * Construct a List of Group's from a InputStream.
      * 
      * @param in InputStream.
      * @return Groups List of Group.
@@ -175,20 +176,29 @@ public class GroupListReader
             throw new ReaderException(error);
         }
 
-        return parseGroups(root);
+        return getGroupList(root);
     }
 
-    protected static List<Group> parseGroups(Element groupsElement)
-            throws URISyntaxException, ReaderException
-    {
+    /**
+     * Get a List of Groups from a JDOM element.
+     *
+     * @param element The Group's JDOM element.
+     * @return A List of Group objects.
+     * @throws URISyntaxException
+     * @throws ReaderException
+     */
+    protected final List<Group> getGroupList(Element element)
+        throws URISyntaxException, ReaderException
+    {;
         List<Group> groups = new ArrayList<Group>();
 
-        List<Element> groupElements = groupsElement.getChildren("group");
+        List<Element> groupElements = element.getChildren("group");
         for (Element groupElement : groupElements)
         {
-            groups.add(GroupReader.parseGroup(groupElement));
+            groups.add(getGroup(groupElement));
         }
 
         return groups;
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
index b3707fb3d7ce6bb3e2f3c16a6e9f59c01606b786..a42cff8bef88bf2965dd59888ea8cd81e8838abb 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
@@ -1,8 +1,79 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  Government of Canada                 Gouvernement du Canada
+*  National Research Council            Conseil national de recherches
+*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+*  All rights reserved                  Tous droits réservés
+*
+*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+*  expressed, implied, or               énoncée, implicite ou légale,
+*  statutory, of any kind with          de quelque nature que ce
+*  respect to the software,             soit, concernant le logiciel,
+*  including without limitation         y compris sans restriction
+*  any warranty of merchantability      toute garantie de valeur
+*  or fitness for a particular          marchande ou de pertinence
+*  purpose. NRC shall not be            pour un usage particulier.
+*  liable in any event for any          Le CNRC ne pourra en aucun cas
+*  damages, whether direct or           être tenu responsable de tout
+*  indirect, special or general,        dommage, direct ou indirect,
+*  consequential or incidental,         particulier ou général,
+*  arising from the use of the          accessoire ou fortuit, résultant
+*  software.  Neither the name          de l'utilisation du logiciel. Ni
+*  of the National Research             le nom du Conseil National de
+*  Council of Canada nor the            Recherches du Canada ni les noms
+*  names of its contributors may        de ses  participants ne peuvent
+*  be used to endorse or promote        être utilisés pour approuver ou
+*  products derived from this           promouvoir les produits dérivés
+*  software without specific prior      de ce logiciel sans autorisation
+*  written permission.                  préalable et particulière
+*                                       par écrit.
+*
+*  This file is part of the             Ce fichier fait partie du projet
+*  OpenCADC project.                    OpenCADC.
+*
+*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+*  you can redistribute it and/or       vous pouvez le redistribuer ou le
+*  modify it under the terms of         modifier suivant les termes de
+*  the GNU Affero General Public        la “GNU Affero General Public
+*  License as published by the          License” telle que publiée
+*  Free Software Foundation,            par la Free Software Foundation
+*  either version 3 of the              : soit la version 3 de cette
+*  License, or (at your option)         licence, soit (à votre gré)
+*  any later version.                   toute version ultérieure.
+*
+*  OpenCADC is distributed in the       OpenCADC est distribué
+*  hope that it will be useful,         dans l’espoir qu’il vous
+*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+*  without even the implied             GARANTIE : sans même la garantie
+*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+*  General Public License for           Générale Publique GNU Affero
+*  more details.                        pour plus de détails.
+*
+*  You should have received             Vous devriez avoir reçu une
+*  a copy of the GNU Affero             copie de la Licence Générale
+*  General Public License along         Publique GNU Affero avec
+*  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+*                                       <http://www.gnu.org/licenses/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
 package ca.nrc.cadc.ac.xml;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.util.StringBuilderWriter;
+import org.jdom2.Element;
+
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -10,19 +81,15 @@ import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.util.Collection;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 /**
  * Class to write a XML representation from a Collection of Groups objects.
  */
-public class GroupListWriter
+public class GroupListWriter extends AbstractReaderWriter
 {
     /**
-     * Write a List of Group's to a StringBuilder.
-     * @param groups List of Group's to write.
+     * Write a Collection of Group's to a StringBuilder.
+     * @param groups Collection of Group's to write.
      * @param builder
      * @throws java.io.IOException
      * @throws WriterException
@@ -34,9 +101,9 @@ public class GroupListWriter
     }
 
     /**
-     * Write a List of Group's to an OutputStream.
+     * Write a Collection of Group's to an OutputStream.
      * 
-     * @param groups List of Group's to write.
+     * @param groups Collection of Group's to write.
      * @param out OutputStream to write to.
      * @throws IOException if the writer fails to write.
      * @throws WriterException
@@ -57,9 +124,9 @@ public class GroupListWriter
     }
 
     /**
-     * Write a List of Group's to a Writer.
+     * Write a Collection of Group's to a Writer.
      * 
-     * @param groups List of Group's to write.
+     * @param groups Collection of Group's to write.
      * @param writer  Writer to write to.
      * @throws IOException if the writer fails to write.
      * @throws WriterException
@@ -72,41 +139,27 @@ public class GroupListWriter
         throw new WriterException("null groups");
         }
 
-        write(getGroupsElement(groups), writer);
+        write(getElement(groups), writer);
     }
 
     /**
-     * 
-     * @param groups List of Group's to write.
-     * @return Element of list of Group's.
+     * Get a JDOM element from a Collection of Group objects.
+     *
+     * @param groups Collection of Group's to write.
+     * @return A JDOM Group list representation.
      * @throws WriterException
      */
-    public static Element getGroupsElement(Collection<Group> groups)
+    protected final Element getElement(Collection<Group> groups)
         throws WriterException
     {
         Element groupsElement = new Element("groups");
 
         for (Group group : groups)
         {
-            groupsElement.addContent(GroupWriter.getGroupElement(group));
+            groupsElement.addContent(getElement(group));
         }
 
         return groupsElement;
     }
 
-    /**
-     * Write to root Element to a writer.
-     * 
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
-    }
-    
 }
\ No newline at end of file
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
index 0f0d1dca1233d54a01c9cbb0da2ba9685fffc18c..b9c64d1bcd3a75ed9855739db1df0f999fe0fe7d 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
@@ -68,11 +68,8 @@
  */
 package ca.nrc.cadc.ac.xml;
 
-import ca.nrc.cadc.ac.AC;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.ReaderException;
-import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.date.DateUtil;
 import ca.nrc.cadc.xml.XmlUtil;
 import org.jdom2.Document;
 import org.jdom2.Element;
@@ -85,15 +82,11 @@ import java.io.Reader;
 import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
-import java.security.Principal;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.util.List;
 
 /**
  * Class to read a XML representation of a Group to a Group object.
  */
-public class GroupReader
+public class GroupReader extends AbstractReaderWriter
 {
 
     /**
@@ -181,128 +174,7 @@ public class GroupReader
             throw new ReaderException(error);
         }
 
-        return parseGroup(root);
+        return getGroup(root);
     }
 
-    public static Group parseGroup(Element groupElement)
-        throws ReaderException
-    {
-        String uri = groupElement.getAttributeValue("uri");
-        if (uri == null)
-        {
-            String error = "group missing required uri attribute";
-            throw new ReaderException(error);
-        }
-
-        // Group groupID
-        int index = uri.indexOf(AC.GROUP_URI);
-        if (index == -1)
-        {
-            String error = "group uri attribute malformed: " + uri;
-            throw new ReaderException(error);
-        }
-        String groupID = uri.substring(AC.GROUP_URI.length());
-
-        // Group owner
-        User<? extends Principal> user = null;
-        Element ownerElement = groupElement.getChild("owner");
-        if (ownerElement != null)
-        {
-            // Owner user
-            Element userElement = ownerElement.getChild("user");
-            if (userElement == null)
-            {
-                String error = "owner missing required user element";
-                throw new ReaderException(error);
-            }
-            user = UserReader.parseUser(userElement);
-        }
-
-        Group group = new Group(groupID, user);
-
-        // description
-        Element descriptionElement = groupElement.getChild("description");
-        if (descriptionElement != null)
-        {
-            group.description = descriptionElement.getText();
-        }
-
-        // lastModified
-        Element lastModifiedElement = groupElement.getChild("lastModified");
-        if (lastModifiedElement != null)
-        {
-            try
-            {
-                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
-                group.lastModified = df.parse(lastModifiedElement.getText());
-            }
-            catch (ParseException e)
-            {
-                String error = "Unable to parse group lastModified because " + e.getMessage();
-
-                throw new ReaderException(error);
-            }
-
-        }
-        
-        // properties
-        Element propertiesElement = groupElement.getChild("properties");
-        if (propertiesElement != null)
-        {
-            List<Element> propertyElements = propertiesElement.getChildren("property");
-            for (Element propertyElement : propertyElements)
-            {
-                group.getProperties().add(ca.nrc.cadc.ac.xml.GroupPropertyReader.read(propertyElement));
-            }
-
-        }
-
-        // groupMembers
-        Element groupMembersElement = groupElement.getChild("groupMembers");
-        if (groupMembersElement != null)
-        {
-            List<Element> groupElements = groupMembersElement.getChildren("group");
-            for (Element groupMember : groupElements)
-            {
-                group.getGroupMembers().add(parseGroup(groupMember));
-            }
-
-        }
-
-        // userMembers
-        Element userMembersElement = groupElement.getChild("userMembers");
-        if (userMembersElement != null)
-        {
-            List<Element> userElements = userMembersElement.getChildren("user");
-            for (Element userMember : userElements)
-            {
-                group.getUserMembers().add(UserReader.parseUser(userMember));
-            }
-        }
-        
-        // groupAdmins
-        Element groupAdminsElement = groupElement.getChild("groupAdmins");
-        if (groupAdminsElement != null)
-        {
-            List<Element> groupElements = groupAdminsElement.getChildren("group");
-            for (Element groupMember : groupElements)
-            {
-                group.getGroupAdmins().add(parseGroup(groupMember));
-            }
-
-        }
-
-        // userAdmins
-        Element userAdminsElement = groupElement.getChild("userAdmins");
-        if (userAdminsElement != null)
-        {
-            List<Element> userElements = userAdminsElement.getChildren("user");
-            for (Element userMember : userElements)
-            {
-                group.getUserAdmins().add(UserReader.parseUser(userMember));
-            }
-        }
-
-        return group;
-    }
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
index 380ba79a8d7ec2fa8e203f8e7f6d8bafbb51ff0e..67687c5374fee4350aedaf80eb67d277e869de1e 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
@@ -68,18 +68,9 @@
  */
 package ca.nrc.cadc.ac.xml;
 
-import ca.nrc.cadc.ac.AC;
 import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.GroupProperty;
-import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.WriterException;
-import ca.nrc.cadc.date.DateUtil;
 import ca.nrc.cadc.util.StringBuilderWriter;
-import org.jdom2.Attribute;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -87,13 +78,11 @@ import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
-import java.security.Principal;
-import java.text.DateFormat;
 
 /**
  * Class to write a XML representation of a Group object.
  */
-public class GroupWriter
+public class GroupWriter extends AbstractReaderWriter
 {
     /**
      * Write a Group to a StringBuilder.
@@ -147,129 +136,7 @@ public class GroupWriter
             throw new WriterException("null group");
         }
 
-        write(getGroupElement(group), writer);
+        write(getElement(group), writer);
     }
 
-    /**
-     * 
-     * @param group
-     * @return 
-     * @throws WriterException
-     */
-    public static Element getGroupElement(Group group)
-        throws WriterException
-    {
-        return getGroupElement(group, true);
-    }
-
-    public static Element getGroupElement(Group group, boolean deepCopy)
-        throws WriterException
-    {
-        // Create the root group element.
-        Element groupElement = new Element("group");
-        String groupURI = AC.GROUP_URI + group.getID();
-        groupElement.setAttribute(new Attribute("uri", groupURI));
-
-        // Group owner
-        if (group.getOwner() != null)
-        {
-            Element ownerElement = new Element("owner");
-            Element userElement = UserWriter.getUserElement(group.getOwner());
-            ownerElement.addContent(userElement);
-            groupElement.addContent(ownerElement);
-        }
-
-        if (deepCopy)
-        {
-            // Group description
-            if (group.description != null)
-            {
-                Element descriptionElement = new Element("description");
-                descriptionElement.setText(group.description);
-                groupElement.addContent(descriptionElement);
-            }
-
-            // lastModified
-            if (group.lastModified != null)
-            {
-                Element lastModifiedElement = new Element("lastModified");
-                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
-                lastModifiedElement.setText(df.format(group.lastModified));
-                groupElement.addContent(lastModifiedElement);
-            }
-
-            // Group properties
-            if (!group.getProperties().isEmpty())
-            {
-                Element propertiesElement = new Element("properties");
-                for (GroupProperty property : group.getProperties())
-                {
-                    propertiesElement.addContent(ca.nrc.cadc.ac.xml.GroupPropertyWriter.write(property));
-                }
-                groupElement.addContent(propertiesElement);
-            }
-
-            // Group groupMembers.
-            if ((group.getGroupMembers() != null) && (!group.getGroupMembers().isEmpty()))
-            {
-                Element groupMembersElement = new Element("groupMembers");
-                for (Group groupMember : group.getGroupMembers())
-                {
-                    groupMembersElement.addContent(getGroupElement(groupMember, false));
-                }
-                groupElement.addContent(groupMembersElement);
-            }
-
-            // Group userMembers
-            if ((group.getUserMembers() != null) && (!group.getUserMembers().isEmpty()))
-            {
-                Element userMembersElement = new Element("userMembers");
-                for (User<? extends Principal> userMember : group.getUserMembers())
-                {
-                    userMembersElement.addContent(UserWriter.getUserElement(userMember));
-                }
-                groupElement.addContent(userMembersElement);
-            }
-            
-            // Group groupAdmins.
-            if ((group.getGroupAdmins() != null) && (!group.getGroupAdmins().isEmpty()))
-            {
-                Element groupAdminsElement = new Element("groupAdmins");
-                for (Group groupMember : group.getGroupAdmins())
-                {
-                    groupAdminsElement.addContent(getGroupElement(groupMember, false));
-                }
-                groupElement.addContent(groupAdminsElement);
-            }
-
-            // Group userAdmins
-            if ((group.getUserAdmins() != null) && (!group.getUserAdmins().isEmpty()))
-            {
-                Element userAdminsElement = new Element("userAdmins");
-                for (User<? extends Principal> userMember : group.getUserAdmins())
-                {
-                    userAdminsElement.addContent(UserWriter.getUserElement(userMember));
-                }
-                groupElement.addContent(userAdminsElement);
-            }
-        }
-
-        return groupElement;
-    }
-
-    /**
-     * Write to root Element to a writer.
-     * 
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
-    }
-    
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..ae454fcb8ebe8aeb4c66537d711cb4b73a00276a
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListReader.java
@@ -0,0 +1,204 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2015.                            (c) 2015.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.xml;
+
+import ca.nrc.cadc.ac.ReaderException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.xml.XmlUtil;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class to read an XML representation of a List of Users
+ * into a List of User objects.
+ */
+public class UserListReader extends AbstractReaderWriter
+{
+    /**
+     * Construct a List of Users from an XML String source.
+     *
+     * @param xml String of the XML.
+     * @return List of users.
+     * @throws ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public List<User<Principal>> read(String xml)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (xml == null)
+        {
+            throw new IllegalArgumentException("XML must not be null");
+        }
+        return read(new StringReader(xml));
+    }
+
+    /**
+     * Construct a List of Users from a InputStream.
+     *
+     * @param in InputStream.
+     * @return List of Users.
+     * @throws ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public List<User<Principal>> read(InputStream in)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (in == null)
+        {
+            throw new IOException("stream closed");
+        }
+        InputStreamReader reader;
+        try
+        {
+            reader = new InputStreamReader(in, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported");
+        }
+        return read(reader);
+    }
+
+    /**
+     * Construct a List of Users from a Reader.
+     *
+     * @param reader Reader.
+     * @return List of Users.
+     * @throws ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public List<User<Principal>> read(Reader reader)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        Document document;
+        try
+        {
+            document = XmlUtil.buildDocument(reader);
+        }
+        catch (JDOMException jde)
+        {
+            String error = "XML failed validation: " + jde.getMessage();
+            throw new ReaderException(error, jde);
+        }
+
+        Element root = document.getRootElement();
+
+        String userElemName = root.getName();
+
+        if (!userElemName.equalsIgnoreCase("users"))
+        {
+            String error = "Expected users element, found " + userElemName;
+            throw new ReaderException(error);
+        }
+
+        return getUserList(root);
+    }
+
+    /**
+     * Get a List of Users from a JDOM element.
+     *
+     * @param element The Users JDOM element.
+     * @return A List of User objects.
+     * @throws URISyntaxException
+     * @throws ReaderException
+     */
+    protected final List<User<Principal>> getUserList(Element element)
+        throws URISyntaxException, ReaderException
+    {
+        List<User<Principal>> users = new ArrayList<User<Principal>>();
+
+        List<Element> userElements = element.getChildren("user");
+        for (Element userElement : userElements)
+        {
+            users.add(getUser(userElement));
+        }
+
+        return users;
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
index f08d4de4c4ba371da692b858c25e51198cb6abe0..1674324dffa067f6fc06bd9bc3629c7c7bf27d15 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
@@ -68,54 +68,100 @@
 
 package ca.nrc.cadc.ac.xml;
 
-import ca.nrc.cadc.ac.PersonalDetails;
-import org.jdom2.Document;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.util.StringBuilderWriter;
 import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
+import java.io.BufferedWriter;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
 import java.io.Writer;
-import java.util.Map;
+import java.security.Principal;
+import java.util.Collection;
 
 /**
- * Class to write a XML representation of a Collection of User's.
+ * Class to write a XML representation of a List of User's.
  */
-public class UserListWriter
+public class UserListWriter extends AbstractReaderWriter
 {
     /**
-     * Write the Map of User entries as XML.
+     * Write a Collection of User's to a StringBuilder.
      *
-     * @param users             The Map of User IDs to Names.
-     * @param writer            The Writer to output to.
-     * @throws IOException      Any writing errors.
+     * @param users   Collection of User's to write.
+     * @param builder The StringBuilder.
+     * @throws java.io.IOException
+     * @throws WriterException
      */
-    public void write(final Map<String, PersonalDetails> users,
-                      final Writer writer) throws IOException
+    public <T extends Principal> void write(Collection<User<T>> users, StringBuilder builder)
+        throws IOException, WriterException
     {
-        // Create the root users Element.
-        final Element usersElement = new Element("users");
+        write(users, new StringBuilderWriter(builder));
+    }
+
+    /**
+     * Write a Collection of User's to an OutputStream.
+     *
+     * @param users Collection of User's to write.
+     * @param out   OutputStream to write to.
+     * @throws IOException     if the writer fails to write.
+     * @throws WriterException
+     */
+    public <T extends Principal> void write(Collection<User<T>> users, OutputStream out)
+        throws IOException, WriterException
+    {
+        OutputStreamWriter outWriter;
+        try
+        {
+            outWriter = new OutputStreamWriter(out, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported", e);
+        }
+        write(users, new BufferedWriter(outWriter));
+    }
 
-        for (final Map.Entry<String, PersonalDetails> entry : users.entrySet())
+    /**
+     * Write a Collection of Users to a Writer.
+     *
+     * @param users  Users to write.
+     * @param writer Writer to write to.
+     * @throws IOException     if the writer fails to write.
+     * @throws WriterException
+     */
+    public <T extends Principal> void write(Collection<User<T>> users, Writer writer)
+        throws IOException, WriterException
+    {
+        if (users == null)
         {
-            final Element userEntryElement = new Element("user");
-            final Element firstNameElement = new Element("firstName");
-            final Element lastNameElement = new Element("lastName");
+            throw new WriterException("null users");
+        }
 
-            userEntryElement.setAttribute("id", entry.getKey());
+        write(getElement(users), writer);
+    }
 
-            firstNameElement.setText(entry.getValue().getFirstName());
-            userEntryElement.addContent(firstNameElement);
 
-            lastNameElement.setText(entry.getValue().getLastName());
-            userEntryElement.addContent(lastNameElement);
+    /**
+     * Get a JDOM element from a Collection of User objects.
+     *
+     * @param users Collection of User's to write.
+     * @return A JDOM Group list representation.
+     * @throws WriterException
+     */
+    protected final <T extends Principal> Element getElement(Collection<User<T>> users)
+        throws WriterException
+    {
+        Element usersElement = new Element("users");
 
-            usersElement.addContent(userEntryElement);
+        for (User<T> user : users)
+        {
+            usersElement.addContent(getElement(user));
         }
 
-        final XMLOutputter output = new XMLOutputter();
-
-        output.setFormat(Format.getPrettyFormat());
-        output.output(new Document(usersElement), writer);
+        return usersElement;
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
index 0accd9da28358bd855b9b73c0d10ce8265044872..c3cbae20aaa3f64e875560f521b1776366f98aca 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
@@ -83,16 +83,15 @@ import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.security.Principal;
-import java.util.List;
 
 /**
  * Class to read a XML representation of a User to a User object.
  */
-public class UserReader
+public class UserReader extends AbstractReaderWriter
 {
     /**
      * Construct a User from an XML String source.
-     * 
+     *
      * @param xml String of the XML.
      * @return User User.
      * @throws ReaderException
@@ -111,7 +110,7 @@ public class UserReader
 
     /**
      * Construct a User from a InputStream.
-     * 
+     *
      * @param in InputStream.
      * @return User User.
      * @throws java.io.IOException
@@ -137,7 +136,7 @@ public class UserReader
 
     /**
      * Construct a User from a Reader.
-     * 
+     *
      * @param reader Reader.
      * @return User User.
      * @throws ReaderException
@@ -166,58 +165,7 @@ public class UserReader
         // Root element and namespace of the Document
         Element root = document.getRootElement();
 
-        return parseUser(root);
-    }
-
-    public static User<Principal> parseUser(Element userElement)
-        throws ReaderException
-    {
-        // userID element of the User element
-        Element userIDElement = userElement.getChild("userID");
-        if (userIDElement == null)
-        {
-            String error = "userID element not found in user element";
-            throw new ReaderException(error);
-        }
-
-        // identity element of the userID element
-        Element userIDIdentityElement = userIDElement.getChild("identity");
-        if (userIDIdentityElement == null)
-        {
-            String error = "identity element not found in userID element";
-            throw new ReaderException(error);
-        }
-
-        IdentityReader identityReader = new IdentityReader();
-        Principal userID = identityReader.read(userIDIdentityElement);
-
-        User<Principal> user = new User<Principal>(userID);
-
-        // identities
-        Element identitiesElement = userElement.getChild("identities");
-        if (identitiesElement != null)
-        {
-            List<Element> identityElements = identitiesElement.getChildren("identity");
-            for (Element identityElement : identityElements)
-            {
-                user.getIdentities().add(identityReader.read(identityElement));
-            }
-
-        }
-
-        // details
-        Element detailsElement = userElement.getChild("details");
-        if (detailsElement != null)
-        {
-            UserDetailsReader userDetailsReader = new UserDetailsReader();
-            List<Element> userDetailsElements = detailsElement.getChildren("userDetails");
-            for (Element userDetailsElement : userDetailsElements)
-            {
-                user.details.add(userDetailsReader.read(userDetailsElement));
-            }
-        }
-
-        return user;
+        return getUser(root);
     }
 
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
index e44eba1e4dc37fe8f9fa81b0f7b9629bf271e65e..dffef3e5e7f65d9fc3217f18f19f3a641189a095 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
@@ -69,9 +69,12 @@
 package ca.nrc.cadc.ac.xml;
 
 import ca.nrc.cadc.ac.ReaderException;
-import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.xml.XmlUtil;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -80,14 +83,10 @@ import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.security.Principal;
 
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.JDOMException;
-
 /**
  * Class to read a XML representation of a UserRequest to a UserRequest object.
  */
-public class UserRequestReader
+public class UserRequestReader extends AbstractReaderWriter
 {
     /**
      * Construct a UserRequest from an XML String source.
@@ -164,31 +163,7 @@ public class UserRequestReader
         // Root element and namespace of the Document
         Element root = document.getRootElement();
 
-        return parseUserRequest(root);
+        return getUserRequest(root);
     }
 
-    protected static UserRequest<Principal> parseUserRequest(
-            Element userRequestElement)
-        throws ReaderException
-    {
-        // user element of the UserRequest element
-        Element userElement = userRequestElement.getChild("user");
-        if (userElement == null)
-        {
-            String error = "user element not found in userRequest element";
-            throw new ReaderException(error);
-        }
-        User<Principal> user = ca.nrc.cadc.ac.xml.UserReader.parseUser(userElement);
-
-        // password element of the userRequest element
-        Element passwordElement = userRequestElement.getChild("password");
-        if (passwordElement == null)
-        {
-            String error = "password element not found in userRequest element";
-            throw new ReaderException(error);
-        }
-        String password = passwordElement.getText();
-
-        return new UserRequest<Principal>(user, password.toCharArray());
-    }
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
index fe0ffee96a6fbbd1a9e9a70f77d575dfbdf0a629..e87840d129da63713cd78b9b5107413dec04404e 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
@@ -72,10 +72,6 @@ package ca.nrc.cadc.ac.xml;
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.util.StringBuilderWriter;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -84,7 +80,7 @@ import java.security.Principal;
 /**
  * Class to write a XML representation of a UserRequest object.
  */
-public class UserRequestWriter
+public class UserRequestWriter extends AbstractReaderWriter
 {
     /**
      * Write a UserRequest to a StringBuilder.
@@ -94,7 +90,7 @@ public class UserRequestWriter
      * @throws java.io.IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(UserRequest<? extends Principal> userRequest, StringBuilder builder)
+    public <T extends Principal> void write(UserRequest<T> userRequest, StringBuilder builder)
         throws IOException, WriterException
     {
         write(userRequest, new StringBuilderWriter(builder));
@@ -108,7 +104,7 @@ public class UserRequestWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(UserRequest<? extends Principal> userRequest, Writer writer)
+    public <T extends Principal> void write(UserRequest<T> userRequest, Writer writer)
         throws IOException, WriterException
     {
         if (userRequest == null)
@@ -116,46 +112,7 @@ public class UserRequestWriter
             throw new WriterException("null UserRequest");
         }
 
-        write(getUserRequestElement(userRequest), writer);
+        write(getElement(userRequest), writer);
     }
 
-    /**
-     * Build the UserRequest element.
-     *
-     * @param userRequest UserRequest.
-     * @return member Element.
-     * @throws WriterException
-     */
-    public static Element getUserRequestElement(UserRequest<? extends Principal> userRequest)
-        throws WriterException
-    {
-        // Create the userRequest Element.
-        Element userRequestElement = new Element("userRequest");
-
-        // user element
-        Element userElement = UserWriter.getUserElement(userRequest.getUser());
-        userRequestElement.addContent(userElement);
-
-        // password element
-        Element passwordElement = new Element("password");
-        passwordElement.setText(String.valueOf(userRequest.getPassword()));
-        userRequestElement.addContent(passwordElement);
-
-        return userRequestElement;
-    }
-
-    /**
-     * Write to root Element to a writer.
-     *
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
-    }
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
index 86327ad8f27e7e77bfaa4b17291fe4d8830e2fbf..7df99cbb6e0d284eb1c8f5df5fde0995f5398fc2 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
@@ -69,13 +69,8 @@
 package ca.nrc.cadc.ac.xml;
 
 import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserDetails;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.util.StringBuilderWriter;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -84,12 +79,11 @@ import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.security.Principal;
-import java.util.Set;
 
 /**
  * Class to write a XML representation of a User object.
  */
-public class UserWriter
+public class UserWriter extends AbstractReaderWriter
 {
     /**
      * Write a User to a StringBuilder.
@@ -99,7 +93,7 @@ public class UserWriter
      * @throws java.io.IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(User<? extends Principal> user, StringBuilder builder)
+    public <T extends Principal> void write(User<T> user, StringBuilder builder)
         throws IOException, WriterException
     {
         write(user, new StringBuilderWriter(builder));
@@ -113,7 +107,7 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(User<? extends Principal> user, OutputStream out)
+    public <T extends Principal> void write(User<T> user, OutputStream out)
         throws IOException, WriterException
     {                
         OutputStreamWriter outWriter;
@@ -136,7 +130,7 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(User<? extends Principal> user, Writer writer)
+    public <T extends Principal> void write(User<T> user, Writer writer)
         throws IOException, WriterException
     {
         if (user == null)
@@ -144,69 +138,7 @@ public class UserWriter
             throw new WriterException("null User");
         }
         
-        write(getUserElement(user), writer);
-    }
-
-    /**
-     * Build the member Element of a User.
-     *
-     * @param user User.
-     * @return member Element.
-     * @throws WriterException
-     */
-    public static Element getUserElement(User<? extends Principal> user)
-        throws WriterException
-    {
-        // Create the user Element.
-        Element userElement = new Element("user");
-
-        // userID element
-        IdentityWriter identityWriter = new IdentityWriter();
-        Element userIDElement = new Element("userID");
-        userIDElement.addContent(identityWriter.write(user.getUserID()));
-        userElement.addContent(userIDElement);
-
-        // identities
-        Set<Principal> identities = user.getIdentities();
-        if (!identities.isEmpty())
-        {
-            Element identitiesElement = new Element("identities");
-            for (Principal identity : identities)
-            {
-                identitiesElement.addContent(identityWriter.write(identity));
-            }
-            userElement.addContent(identitiesElement);
-        }
-
-        // details
-        if (!user.details.isEmpty())
-        {
-            UserDetailsWriter userDetailsWriter = new UserDetailsWriter();
-            Element detailsElement = new Element("details");
-            Set<UserDetails> userDetails = user.details;
-            for (UserDetails userDetail : userDetails)
-            {
-                detailsElement.addContent(userDetailsWriter.write(userDetail));
-            }
-            userElement.addContent(detailsElement);
-        }
-
-        return userElement;
-    }
-
-    /**
-     * Write to root Element to a writer.
-     *
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
+        write(getElement(user), writer);
     }
 
 }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java
index 2da9b1e8948109ca172b198bf0e96965b54b6a7e..d3eade86bb389c2368c7c6c16082f2f0a197b18c 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java
@@ -92,20 +92,20 @@ import ca.nrc.cadc.util.Log4jInit;
 
 import org.junit.Assert;
 import org.junit.Test;
+
 import static org.easymock.EasyMock.*;
 
 
 public class GMSClientTest
 {
-    
+
     private static final Logger log = Logger.getLogger(GMSClientTest.class);
-    
+
     public GMSClientTest()
     {
         Log4jInit.setLevel("ca.nrc.cadc.ac", Level.DEBUG);
     }
 
-
     @Test
     public void testGetDisplayUsers() throws Exception
     {
@@ -127,18 +127,16 @@ public class GMSClientTest
         expectLastCall().once();
 
         expect(mockHTTPDownload.getThrowable()).andReturn(null).once();
-
         expect(mockHTTPDownload.getContentLength()).andReturn(88l).once();
         expect(mockHTTPDownload.getContentType()).andReturn(
                 "application/json").once();
 
         replay(mockHTTPDownload);
-
         testSubject.getDisplayUsers();
-
         verify(mockHTTPDownload);
     }
 
+
     @Test
     public void testUserIsSubject()
     {
@@ -148,7 +146,7 @@ public class GMSClientTest
             HttpPrincipal userID = new HttpPrincipal("test");
             HttpPrincipal userID2 = new HttpPrincipal("test2");
             subject.getPrincipals().add(userID);
-            
+
             RegistryClient regClient = new RegistryClient();
             URL baseURL = regClient.getServiceURL(new URI(AC.GMS_SERVICE_URI),
                                                   "https");
@@ -159,10 +157,10 @@ public class GMSClientTest
             Assert.assertFalse(client.userIsSubject(null, subject));
             Assert.assertFalse(client.userIsSubject(userID2, subject));
             Assert.assertTrue(client.userIsSubject(userID, subject));
-            
+
             HttpPrincipal userID3 = new HttpPrincipal("test3");
             subject.getPrincipals().add(userID3);
-            
+
             Assert.assertTrue(client.userIsSubject(userID, subject));
             Assert.assertFalse(client.userIsSubject(userID2, subject));
             Assert.assertTrue(client.userIsSubject(userID3, subject));
@@ -173,7 +171,7 @@ public class GMSClientTest
             Assert.fail("Unexpected exception: " + t.getMessage());
         }
     }
-    
+
     @Test
     public void testGroupCaching()
     {
@@ -182,86 +180,108 @@ public class GMSClientTest
             Subject subject = new Subject();
             final HttpPrincipal test1UserID = new HttpPrincipal("test");
             subject.getPrincipals().add(test1UserID);
-            
+
             RegistryClient regClient = new RegistryClient();
             URL baseURL = regClient.getServiceURL(new URI(AC.GMS_SERVICE_URI),
-                    "https");
+                                                  "https");
             final GMSClient client = new GMSClient(baseURL.toString());
 
             Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+            {
+                @Override
+                public Object run() throws Exception
                 {
-                    @Override
-                    public Object run() throws Exception
-                    {
-
-                        List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER);
-                        Assert.assertNull("Cache should be null", initial);
-
-                        List<Group> expected = new ArrayList<Group>();
-                        Group group1 = new Group("1");
-                        Group group2 = new Group("2");
-                        expected.add(group1);
-                        expected.add(group2);
-
-                        client.setCachedGroups(test1UserID, expected, Role.MEMBER);
-
-                        List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER);
-                        Assert.assertEquals("Wrong cached groups", expected, actual);
-                        
-                        // check against another role
-                        actual = client.getCachedGroups(test1UserID, Role.OWNER);
-                        Assert.assertNull("Cache should be null", actual);
-                        
-                        // check against another userid
-                        final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
-                        actual = client.getCachedGroups(anotherUserID, Role.MEMBER);
-                        Assert.assertNull("Cache should be null", actual);
-
-                        return null;
-                    }
-                });
-            
+
+                    List<Group> initial = client
+                            .getCachedGroups(test1UserID, Role.MEMBER, true);
+                    Assert.assertNull("Cache should be null", initial);
+
+                    // add single group as isMember might do
+                    Group group0 = new Group("0");
+                    client.addCachedGroup(test1UserID, group0, Role.MEMBER);
+                    List<Group> actual = client
+                            .getCachedGroups(test1UserID, Role.MEMBER, true);
+                    Assert.assertNull("Cache should be null", actual);
+
+                    Group g = client
+                            .getCachedGroup(test1UserID, "0", Role.MEMBER);
+                    Assert.assertNotNull("cached group from incomplete cache", g);
+
+                    // add all groups like getMemberships might do
+                    List<Group> expected = new ArrayList<Group>();
+                    Group group1 = new Group("1");
+                    Group group2 = new Group("2");
+                    expected.add(group0);
+                    expected.add(group1);
+                    expected.add(group2);
+
+                    client.setCachedGroups(test1UserID, expected, Role.MEMBER);
+
+                    actual = client
+                            .getCachedGroups(test1UserID, Role.MEMBER, true);
+                    Assert.assertEquals("Wrong cached groups", expected, actual);
+
+                    // check against another role
+                    actual = client
+                            .getCachedGroups(test1UserID, Role.OWNER, true);
+                    Assert.assertNull("Cache should be null", actual);
+
+                    // check against another userid
+                    final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
+                    actual = client
+                            .getCachedGroups(anotherUserID, Role.MEMBER, true);
+                    Assert.assertNull("Cache should be null", actual);
+
+                    return null;
+                }
+            });
+
             subject = new Subject();
             final HttpPrincipal test2UserID = new HttpPrincipal("test2");
             subject.getPrincipals().add(test2UserID);
-            
+
             // do the same but as a different user
             Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
-                    {
-                        @Override
-                        public Object run() throws Exception
-                        {
-
-                            List<Group> initial = client.getCachedGroups(test2UserID, Role.MEMBER);
-                            Assert.assertNull("Cache should be null", initial);
-
-                            List<Group> expected = new ArrayList<Group>();
-                            Group group1 = new Group("1");
-                            Group group2 = new Group("2");
-                            expected.add(group1);
-                            expected.add(group2);
-
-                            client.setCachedGroups(test2UserID, expected, Role.MEMBER);
-
-                            List<Group> actual = client.getCachedGroups(test2UserID, Role.MEMBER);
-                            Assert.assertEquals("Wrong cached groups", expected, actual);
-                            
-                            // check against another role
-                            actual = client.getCachedGroups(test2UserID, Role.OWNER);
-                            Assert.assertNull("Cache should be null", actual);
-                            
-                            // check against another userid
-                            final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
-                            actual = client.getCachedGroups(anotherUserID, Role.MEMBER);
-                            Assert.assertNull("Cache should be null", actual);
-
-                            return null;
-                        }
-                    });
+            {
+                @Override
+                public Object run() throws Exception
+                {
+
+                    List<Group> initial = client
+                            .getCachedGroups(test2UserID, Role.MEMBER, true);
+                    Assert.assertNull("Cache should be null", initial);
+
+                    List<Group> expected = new ArrayList<Group>();
+                    Group group1 = new Group("1");
+                    Group group2 = new Group("2");
+                    expected.add(group1);
+                    expected.add(group2);
+
+                    client.setCachedGroups(test2UserID, expected, Role.MEMBER);
+
+                    List<Group> actual = client
+                            .getCachedGroups(test2UserID, Role.MEMBER, true);
+                    Assert.assertEquals("Wrong cached groups", expected, actual);
+
+                    // check against another role
+                    actual = client
+                            .getCachedGroups(test2UserID, Role.OWNER, true);
+                    Assert.assertNull("Cache should be null", actual);
+
+                    // check against another userid
+                    final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
+                    actual = client
+                            .getCachedGroups(anotherUserID, Role.MEMBER, true);
+                    Assert.assertNull("Cache should be null", actual);
+
+                    return null;
+                }
+            });
 
             // do the same without a subject
 
-            List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER);
+            List<Group> initial = client
+                    .getCachedGroups(test1UserID, Role.MEMBER, true);
             Assert.assertNull("Cache should be null", initial);
 
             List<Group> newgroups = new ArrayList<Group>();
@@ -272,7 +292,8 @@ public class GMSClientTest
 
             client.setCachedGroups(test1UserID, newgroups, Role.MEMBER);
 
-            List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER);
+            List<Group> actual = client
+                    .getCachedGroups(test1UserID, Role.MEMBER, true);
             Assert.assertNull("Cache should still be null", actual);
         }
         catch (Throwable t)
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserListReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserListReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..44b3922a64b8ba20c6270cece1d1717832e52f8c
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserListReaderWriterTest.java
@@ -0,0 +1,95 @@
+package ca.nrc.cadc.ac.json;
+
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.PosixDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+import org.apache.log4j.Logger;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+/**
+ * JsonUserListReaderWriterTest TODO describe class
+ */
+public class JsonUserListReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(JsonUserListReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            JsonUserListReader reader = new JsonUserListReader();
+            List<User<Principal>> u = reader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+
+        try
+        {
+            InputStream in = null;
+            JsonUserListReader reader = new JsonUserListReader();
+            List<User<Principal>> u = reader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+
+        try
+        {
+            Reader r = null;
+            JsonUserListReader reader = new JsonUserListReader();
+            List<User<Principal>> u = reader.read(r);
+            fail("null Reader should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            JsonUserWriter writer = new JsonUserWriter();
+            writer.write(null, new StringBuilder());
+            fail("null User should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+
+    @Test
+    public void testReadWrite()
+        throws Exception
+    {
+        User<Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
+        expected.getIdentities().add(new NumericPrincipal(123l));
+        expected.details.add(new PersonalDetails("firstname", "lastname"));
+        expected.details.add(new PosixDetails(123l, 456l, "foo"));
+
+        StringBuilder json = new StringBuilder();
+        JsonUserWriter writer = new JsonUserWriter();
+        writer.write(expected, json);
+        assertFalse(json.toString().isEmpty());
+
+        JsonUserReader reader = new JsonUserReader();
+        User<Principal> actual = reader.read(json.toString());
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+    }
+
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
index dccefe894c2a5b2a3addf25e226cee910061ea74..7de85967189e9397195f80ab03f012b5c8c03d4c 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
@@ -103,7 +103,7 @@ public class JsonUserReaderWriterTest
         {
             String s = null;
             JsonUserReader reader = new JsonUserReader();
-            User<? extends Principal> u = reader.read(s);
+            User<Principal> u = reader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -112,7 +112,7 @@ public class JsonUserReaderWriterTest
         {
             InputStream in = null;
             JsonUserReader reader = new JsonUserReader();
-            User<? extends Principal> u = reader.read(in);
+            User<Principal> u = reader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -121,7 +121,7 @@ public class JsonUserReaderWriterTest
         {
             Reader r = null;
             JsonUserReader reader = new JsonUserReader();
-            User<? extends Principal> u = reader.read(r);
+            User<Principal> u = reader.read(r);
             fail("null Reader should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -144,7 +144,7 @@ public class JsonUserReaderWriterTest
     public void testReadWrite()
         throws Exception
     {
-        User<Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
+        User<? extends Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
         expected.getIdentities().add(new NumericPrincipal(123l));
         expected.details.add(new PersonalDetails("firstname", "lastname"));
         expected.details.add(new PosixDetails(123l, 456l, "foo"));
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupListReaderWriterTest.java
similarity index 98%
rename from projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java
rename to projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupListReaderWriterTest.java
index 0281eaf87cf1456c0d5781fc187012d3d5c7caa2..a18c13b3c093922e9da73774004296729eb0cf70 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupListReaderWriterTest.java
@@ -88,9 +88,9 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class GroupsReaderWriterTest
+public class GroupListReaderWriterTest
 {
-    private static Logger log = Logger.getLogger(GroupsReaderWriterTest.class);
+    private static Logger log = Logger.getLogger(GroupListReaderWriterTest.class);
 
     @Test
     public void testReaderExceptions()
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java
index 7075c9e1f649ca01beb98529bfba564f441582b1..0a2598471ee0c64f290d0bee13e48852d1a74f0e 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java
@@ -83,7 +83,7 @@ import static org.junit.Assert.*;
  *
  * @author jburke
  */
-public class GroupPropertyReaderWriterTest
+public class GroupPropertyReaderWriterTest extends AbstractReaderWriter
 {
     private static Logger log = Logger.getLogger(GroupPropertyReaderWriterTest.class);
 
@@ -100,7 +100,7 @@ public class GroupPropertyReaderWriterTest
         Element element = null;
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("null element should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -108,7 +108,7 @@ public class GroupPropertyReaderWriterTest
         element = new Element("foo");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("element not named 'property' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -116,7 +116,7 @@ public class GroupPropertyReaderWriterTest
         element = new Element("property");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("element without 'key' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -124,7 +124,7 @@ public class GroupPropertyReaderWriterTest
         element.setAttribute("key", "foo");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("element without 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -132,7 +132,7 @@ public class GroupPropertyReaderWriterTest
         element.setAttribute("type", "Double");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("Unsupported 'type' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -144,15 +144,16 @@ public class GroupPropertyReaderWriterTest
     {
         try
         {
-            Element element = GroupPropertyWriter.write(null);
+            GroupProperty groupProperty = null;
+            Element element = getElement(groupProperty);
             fail("null GroupProperty should throw WriterException");
         }
         catch (WriterException e) {}
          
-        GroupProperty gp = new GroupProperty("key", new Double(1.0), true);
+        GroupProperty groupProperty = new GroupProperty("key", new Double(1.0), true);
         try
         {
-            Element element = GroupPropertyWriter.write(gp);
+            Element element = getElement(groupProperty);
             fail("Unsupported GroupProperty type should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -164,20 +165,20 @@ public class GroupPropertyReaderWriterTest
     {
         // String type
         GroupProperty expected = new GroupProperty("key", "value", true);
-        Element element = GroupPropertyWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
          
-        GroupProperty actual = GroupPropertyReader.read(element);
+        GroupProperty actual = getGroupProperty(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
          
         // Integer tuype
         expected = new GroupProperty("key", new Integer(1), false);
-        element = GroupPropertyWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = GroupPropertyReader.read(element);
+        actual = getGroupProperty(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java
index 58918c8d17fc6d9e056305e9246f4e3ce9e0c121..8bb1d674b19e633333e8e331c9add568e585a3b5 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java
@@ -89,7 +89,7 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class IdentityReaderWriterTest
+public class IdentityReaderWriterTest extends AbstractReaderWriter
 {
     private static Logger log = Logger.getLogger(IdentityReaderWriterTest.class);
 
@@ -100,7 +100,7 @@ public class IdentityReaderWriterTest
         Element element = null;
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("null element should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -108,7 +108,7 @@ public class IdentityReaderWriterTest
         element = new Element("foo");
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("element not named 'identity' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -116,7 +116,7 @@ public class IdentityReaderWriterTest
         element = new Element("identity");
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("element without 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -124,7 +124,7 @@ public class IdentityReaderWriterTest
         element.setAttribute("type", "foo");
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("element with unknown 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -134,17 +134,18 @@ public class IdentityReaderWriterTest
     public void testWriterExceptions()
         throws Exception
     {
+        Principal p = null;
         try
         {
-            Element element = IdentityWriter.write(null);
+            Element element = getElement(p);
             fail("null Identity should throw WriterException");
         }
         catch (WriterException e) {}
          
-        Principal p = new JMXPrincipal("foo");
+        p = new JMXPrincipal("foo");
         try
         {
-            Element element = IdentityWriter.write(p);
+            Element element = getElement(p);
             fail("Unsupported Principal type should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -156,40 +157,40 @@ public class IdentityReaderWriterTest
     {
         // X500
         Principal expected = new X500Principal("cn=foo,o=bar");
-        Element element = IdentityWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
          
-        Principal actual = IdentityReader.read(element);
+        Principal actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
-         
-        // UID
+
+        // CADC
         expected = new NumericPrincipal(123l);
-        element = IdentityWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = IdentityReader.read(element);
+        actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
         
         // OpenID
         expected = new OpenIdPrincipal("bar");
-        element = IdentityWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = IdentityReader.read(element);
+        actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
         
         // HTTP
         expected = new HttpPrincipal("baz");
-        element = IdentityWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = IdentityReader.read(element);
+        actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java
index a7badabbaa4ce6e745946e879b6d4636b5141d65..4a6304dd4e31fa88ccb2fd34f53c46164972ce27 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java
@@ -85,7 +85,7 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class UserDetailsReaderWriterTest
+public class UserDetailsReaderWriterTest extends AbstractReaderWriter
 {
     private static Logger log = Logger.getLogger(UserDetailsReaderWriterTest.class);
 
@@ -96,7 +96,7 @@ public class UserDetailsReaderWriterTest
         Element element = null;
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("null element should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -104,7 +104,7 @@ public class UserDetailsReaderWriterTest
         element = new Element("foo");
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("element not named 'userDetails' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -112,7 +112,7 @@ public class UserDetailsReaderWriterTest
         element = new Element(UserDetails.NAME);
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("element without 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -120,7 +120,7 @@ public class UserDetailsReaderWriterTest
         element.setAttribute("type", "foo");
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("element with unknown 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -132,7 +132,8 @@ public class UserDetailsReaderWriterTest
     {
         try
         {
-            Element element = UserDetailsWriter.write(null);
+            UserDetails ud = null;
+            Element element = getElement(ud);
             fail("null UserDetails should throw WriterException");
         }
         catch (WriterException e) {}
@@ -148,10 +149,10 @@ public class UserDetailsReaderWriterTest
         expected.country = "country";
         expected.email = "email";
         expected.institute = "institute";
-        Element element = UserDetailsWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
         
-        PersonalDetails actual = (PersonalDetails) UserDetailsReader.read(element);
+        PersonalDetails actual = (PersonalDetails) getUserDetails(element);
         assertNotNull(actual);
         assertEquals(expected, actual);
         assertEquals(expected.address, actual.address);
@@ -166,10 +167,10 @@ public class UserDetailsReaderWriterTest
         throws Exception
     {
         UserDetails expected = new PosixDetails(123l, 456, "/dev/null");
-        Element element = UserDetailsWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
         
-        UserDetails actual = UserDetailsReader.read(element);
+        UserDetails actual = getUserDetails(element);
         assertNotNull(actual);
         assertEquals(expected, actual);
     }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserListReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserListReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ddca58a35b80651b347fb533e575b9f825a19b40
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserListReaderWriterTest.java
@@ -0,0 +1,92 @@
+package ca.nrc.cadc.ac.xml;
+
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import org.apache.log4j.Logger;
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class UserListReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(UserListReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            UserListReader UserListReader = new UserListReader();
+            List<User<Principal>> u = UserListReader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+
+        try
+        {
+            InputStream in = null;
+            UserListReader userListReader = new UserListReader();
+            List<User<Principal>> u = userListReader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+
+        try
+        {
+            Reader r = null;
+            UserListReader userListReader = new UserListReader();
+            List<User<Principal>> u = userListReader.read(r);
+            fail("null element should throw ReaderException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            UserListWriter userListWriter = new UserListWriter();
+            userListWriter.write(null, new StringBuilder());
+            fail("null User should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+
+    @Test
+    public void testMinimalReadWrite()
+        throws Exception
+    {
+        List<User<Principal>> expected = new ArrayList<User<Principal>>();
+        expected.add(new User<Principal>(new HttpPrincipal("foo")));
+        expected.add(new User<Principal>(new X500Principal("cn=foo,o=bar")));
+
+        StringBuilder xml = new StringBuilder();
+        UserListWriter userListWriter = new UserListWriter();
+        userListWriter.write(expected, xml);
+        assertFalse(xml.toString().isEmpty());
+
+        UserListReader userListReader = new UserListReader();
+        List<User<Principal>> actual = userListReader.read(xml.toString());
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+        assertEquals(expected.get(0), actual.get(0));
+        assertEquals(expected.get(1), actual.get(1));
+    }
+
+}
\ No newline at end of file
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java
index 4e8d9f2b066ef053fb815854ee2a3ce03c66e30c..c420e7ca34c5bf47b8290238d9011d4f73c5a6d5 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java
@@ -102,7 +102,7 @@ public class UserReaderWriterTest
         {
             String s = null;
             UserReader userReader = new UserReader();
-            User<? extends Principal> u = userReader.read(s);
+            User<Principal> u = userReader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -111,7 +111,7 @@ public class UserReaderWriterTest
         {
             InputStream in = null;
             UserReader userReader = new UserReader();
-            User<? extends Principal> u = userReader.read(in);
+            User<Principal> u = userReader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -120,7 +120,7 @@ public class UserReaderWriterTest
         {
             Reader r = null;
             UserReader userReader = new UserReader();
-            User<? extends Principal> u = userReader.read(r);
+            User<Principal> u = userReader.read(r);
             fail("null Reader should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -143,7 +143,7 @@ public class UserReaderWriterTest
     public void testReadWrite()
         throws Exception
     {
-        User<? extends Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
+        User<Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
         expected.getIdentities().add(new NumericPrincipal(123l));
         expected.details.add(new PersonalDetails("firstname", "lastname"));