diff --git a/projects/cadcAccessControl-Server/build.xml b/projects/cadcAccessControl-Server/build.xml
index 12e688469aee1444d7027bef89f048af9de33f88..39cce90c8e2f2e8c6a52777adad23ec353c5d8f0 100644
--- a/projects/cadcAccessControl-Server/build.xml
+++ b/projects/cadcAccessControl-Server/build.xml
@@ -139,7 +139,7 @@
   <property name="testingJars"
             value="${lib.commons-logging}:${dev.junit}:${dev.jsonassert}:${dev.httpunit}:${dev.easyMock}:${dev.selenium.server}:${dev.objenesis}:${lib.js}:${lib.nekoHTML}:${lib.xerces}"/>
 
-  <target name="test" depends="compile,compile-test">
+  <target name="single-test" depends="compile,compile-test">
     <echo message="Running test suite..." />
     <junit printsummary="yes" haltonfailure="yes" fork="yes">
       <classpath>
@@ -148,8 +148,7 @@
         <pathelement path="${jars}:${testingJars}"/>
       </classpath>
       <sysproperty key="ca.nrc.cadc.util.PropertiesReader.dir" value="test"/>
-      <test name="ca.nrc.cadc.ac.server.ldap.LdapUserDAOTest" />
-      <test name="ca.nrc.cadc.ac.server.ldap.LdapGroupDAOTest" />
+      <test name="ca.nrc.cadc.ac.server.web.users.GetUserListActionTest" />
       <formatter type="plain" usefile="false" />
     </junit>
   </target>
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupPersistence.java
index bdfa4e05c79516396085e5ffb90d031dc2d7c3e2..4da9df744f39564fc7bbd3343aa089b9505008e9 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupPersistence.java
@@ -79,7 +79,7 @@ import ca.nrc.cadc.ac.Role;
 import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.net.TransientException;
 
-public abstract interface GroupPersistence<T extends Principal>
+public interface GroupPersistence<T extends Principal>
 {
     /**
      * Get all group names.
@@ -88,7 +88,7 @@ public abstract interface GroupPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public Collection<String> getGroupNames()
+    Collection<String> getGroupNames()
             throws TransientException, AccessControlException;
     
     /**
@@ -102,7 +102,7 @@ public abstract interface GroupPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public abstract Group getGroup(String groupID)
+    Group getGroup(String groupID)
         throws GroupNotFoundException, TransientException,
                AccessControlException;
 
@@ -121,7 +121,7 @@ public abstract interface GroupPersistence<T extends Principal>
      * @throws GroupNotFoundException if one of the groups in group members or
      * group admins does not exist in the server.
      */
-    public abstract Group addGroup(Group group)
+    Group addGroup(Group group)
         throws GroupAlreadyExistsException, TransientException,
                AccessControlException, UserNotFoundException, 
                GroupNotFoundException;
@@ -135,7 +135,7 @@ public abstract interface GroupPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public abstract void deleteGroup(String groupID)
+    void deleteGroup(String groupID)
         throws GroupNotFoundException, TransientException,
                AccessControlException;
 
@@ -151,7 +151,7 @@ public abstract interface GroupPersistence<T extends Principal>
      * @throws AccessControlException If the operation is not permitted.
      * @throws UserNotFoundException If owner or group members not valid users.
      */
-    public abstract Group modifyGroup(Group group)
+    Group modifyGroup(Group group)
         throws GroupNotFoundException, TransientException,
                AccessControlException, UserNotFoundException;
 
@@ -170,9 +170,24 @@ public abstract interface GroupPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public abstract Collection<Group> getGroups(T userID, Role role, 
-                                                String groupID)
+    Collection<Group> getGroups(T userID, Role role, String groupID)
         throws UserNotFoundException, GroupNotFoundException,
                TransientException, AccessControlException;
     
+    /**
+     * Check whether the user is a member of the group.
+     *
+     * @param userID The userID.
+     * @param groupID The groupID.
+     *
+     * @return true or false
+     *
+     * @throws UserNotFoundException If the user is not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    boolean isMember(T userID, String groupID)
+        throws UserNotFoundException, TransientException,
+               AccessControlException;
+  
 }
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 1b369f709fc643f55e6713dfe80fcee4cdc38cbe..f88faffeb2851833b99c606de1f63e317703960f 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
@@ -70,14 +70,11 @@ package ca.nrc.cadc.ac.server;
 
 import java.security.AccessControlException;
 import java.security.Principal;
-import java.util.Collection;
 import java.util.Map;
 
 import ca.nrc.cadc.ac.*;
 import ca.nrc.cadc.net.TransientException;
 
-import com.unboundid.ldap.sdk.DN;
-
 
 public interface UserPersistence<T extends Principal>
 {
@@ -94,7 +91,7 @@ public interface UserPersistence<T extends Principal>
     /**
      * Add the new user.
      *
-     * @param user
+     * @param user      The user request to put into the request tree.
      *
      * @return User instance.
      * 
@@ -119,6 +116,21 @@ public interface UserPersistence<T extends Principal>
     User<T> getUser(T userID)
         throws UserNotFoundException, TransientException, 
                AccessControlException;
+
+    /**
+     * Get the user specified by userID whose account is pending approval.
+     *
+     * @param userID The userID.
+     *
+     * @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.
+     */
+    User<T> getPendingUser(T userID)
+            throws UserNotFoundException, TransientException,
+                   AccessControlException;
     
     /**
      * Attempt to login the specified user.
@@ -132,14 +144,14 @@ public interface UserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    Boolean loginUser(String userID, String password)
+    Boolean doLogin(String userID, String password)
             throws UserNotFoundException, TransientException, 
             AccessControlException;
    
     /**
      * Updated the user specified by User.
      *
-     * @param user
+     * @param user      The user instance to modify.
      *
      * @return User instance.
      * 
@@ -163,37 +175,4 @@ public interface UserPersistence<T extends Principal>
     void deleteUser(T userID)
         throws UserNotFoundException, TransientException, 
                AccessControlException;
-    
-    /**
-     * Get all groups the user specified by userID belongs to.
-     * 
-     * @param userID The userID.
-     * @param isAdmin return only admin Groups when true, else return non-admin
-     *                Groups.
-     * 
-     * @return Collection of group DN.
-     * 
-     * @throws UserNotFoundException  when the user is not found.
-     * @throws TransientException If an temporary, unexpected problem occurred.
-     * @throws AccessControlException If the operation is not permitted.
-     */
-    Collection<DN> getUserGroups(T userID, boolean isAdmin)
-        throws UserNotFoundException, TransientException,
-               AccessControlException;
-    
-    /**
-     * Check whether the user is a member of the group.
-     *
-     * @param userID The userID.
-     * @param groupID The groupID.
-     *
-     * @return true or false
-     *
-     * @throws UserNotFoundException If the user is not found.
-     * @throws TransientException If an temporary, unexpected problem occurred.
-     * @throws AccessControlException If the operation is not permitted.
-     */
-    boolean isMember(T userID, String groupID)
-        throws UserNotFoundException, TransientException,
-               AccessControlException;
 }
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 726b5f94cfda210c83e26d7f46327d29268f1baa..71ddf32316e6de250ab5ef5b9a9c2afdb26657b6 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
@@ -77,32 +77,14 @@ import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.StringUtil;
-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.*;
 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.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 public class LdapGroupDAO<T extends Principal> extends LdapDAO
 {
@@ -323,49 +305,47 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
      * 
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
-    public Collection<String> getGroupNames()
-        throws TransientException
+    public Collection<String> getGroupNames() throws TransientException
     {
         try
         {
-            Filter filter = Filter.createPresenceFilter("cn");
-            String [] attributes = new String[] {"cn", "nsaccountlock"};
-            
-            SearchRequest searchRequest = 
-                    new SearchRequest(config.getGroupsDN(), 
-                                      SearchScope.SUB, filter, attributes);
-    
-            SearchResult searchResult = null;
-            try
-            {
-                searchResult = getConnection().search(searchRequest);
-            }
-            catch (LDAPSearchException e)
+            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();
+
+            final SearchResult searchResult =
+                    getConnection().search(new SearchResultListener()
             {
-                if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
+                @Override
+                public void searchEntryReturned(
+                        final SearchResultEntry searchEntry)
                 {
-                    logger.debug("Could not find groups root", e);
-                    throw new IllegalStateException("Could not find groups root");
+                    groupNames.add(searchEntry.getAttributeValue("cn"));
                 }
-            }
-            
-            LdapDAO.checkLdapResult(searchResult.getResultCode());
-            List<String> groupNames = new ArrayList<String>();
-            for (SearchResultEntry next : searchResult.getSearchEntries())
-            {
-                if (!next.hasAttribute("nsaccountlock"))
+
+                @Override
+                public void searchReferenceReturned(
+                        final SearchResultReference searchReference)
                 {
-                    groupNames.add(next.getAttributeValue("cn"));
+
                 }
-            }
-            
+            }, config.getGroupsDN(), SearchScope.ONE, filter, attributes);
+
+            LdapDAO.checkLdapResult(searchResult.getResultCode());
+            long end = System.currentTimeMillis();
+
+            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);
             LdapDAO.checkLdapResult(e1.getResultCode());
-            throw new IllegalStateException("Unexpected exception: " + e1.getMatchedDN(), e1);
+            throw new IllegalStateException("Unexpected exception: "
+                                            + e1.getMatchedDN(), e1);
         }
         
     }
@@ -492,7 +472,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             User<X500Principal> owner;
             try
             {
-                owner = userPersist.getMember(groupOwner);
+                owner = userPersist.getX500User(groupOwner);
             }
             catch (UserNotFoundException e)
             {
@@ -524,7 +504,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
                             User<X500Principal> user;
                             try
                             {
-                                user = userPersist.getMember(memberDN);
+                                user = userPersist.getX500User(memberDN);
                             }
                             catch (UserNotFoundException e)
                             {
@@ -992,9 +972,9 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
 
         Group group = new Group(searchResult.getAttributeValue("cn"),
-                                userPersist.getMember(
-                                        new DN(searchResult.getAttributeValue(
-                                                "owner"))));
+                                userPersist.getX500User(
+                                    new DN(searchResult.getAttributeValue(
+                                        "owner"))));
         group.description = searchResult.getAttributeValue("description");
         return group;
     }
@@ -1096,7 +1076,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         try
         {
             User<X500Principal> subjectUser = 
-                    userPersist.getMember(getSubjectDN());
+                    userPersist.getX500User(getSubjectDN());
             if (subjectUser.equals(owner))
             {
                 return true;
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 f59bc1518d82c3522bf0cd104bc3ca8fecfc7ebf..e66dc2e5960f289be4050c3d87429b0a3f93c38d 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
@@ -249,4 +249,12 @@ public class LdapGroupPersistence<T extends Principal>
         }
     }
     
+    public boolean isMember(T userID, String groupID)
+            throws UserNotFoundException, TransientException,
+            AccessControlException
+    {
+        return (new LdapUserPersistence<T>()).isMember(userID, groupID);
+    }
+
+  
 }
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 2bf4a1d06b7c42e454569b6b7bd9b234d1f4e345..3340db29ed0bf544ae4937855a0c1d9b2471f956 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
@@ -127,6 +127,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
 
     // Returned User attributes
     protected static final String LDAP_OBJECT_CLASS = "objectClass";
+    protected static final String LDAP_INET_USER = "inetuser";
     protected static final String LDAP_INET_ORG_PERSON = "inetOrgPerson";
     protected static final String LDAP_CADC_ACCOUNT = "cadcaccount";
     protected static final String LDAP_NSACCOUNTLOCK = "nsaccountlock";
@@ -150,7 +151,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
             {
                     LDAP_FIRST_NAME, LDAP_LAST_NAME, LDAP_ADDRESS, LDAP_CITY,
                     LDAP_COUNTRY,
-                    LDAP_EMAIL, LDAP_INSTITUTE, LDAP_UID
+                    LDAP_EMAIL, LDAP_INSTITUTE
             };
     private String[] memberAttribs = new String[]
             {
@@ -189,7 +190,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @throws TransientException
      * @throws UserNotFoundException
      */
-    public Boolean loginUser(final String username, final String password)
+    public Boolean doLogin(final String username, final String password)
         throws TransientException, UserNotFoundException
     {
         try
@@ -208,7 +209,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         }
         catch (LDAPException e)
         {
-            logger.debug("loginUser Exception: " + e, e);
+            logger.debug("doLogin Exception: " + e, e);
 
             if (e.getResultCode() == ResultCode.INVALID_CREDENTIALS)
             {
@@ -362,11 +363,12 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
             DN userX500DN = getUserRequestsDN(user.getUserID().getName());
             List<Attribute> attributes = new ArrayList<Attribute>();
             addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_INET_ORG_PERSON);
+            addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_INET_USER);
             addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_CADC_ACCOUNT);
             addAttribute(attributes, LDAP_COMMON_NAME, user.getUserID()
                 .getName());
-            addAttribute(attributes, LADP_USER_PASSWORD, userRequest
-                    .getPassword());
+            addAttribute(attributes, LADP_USER_PASSWORD, new String(userRequest
+                    .getPassword()));
             addAttribute(attributes, LDAP_NUMERICID, 
                     String.valueOf(genNextNumericId()));
             for (Principal princ : user.getIdentities())
@@ -414,7 +416,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      *
      * @param userID The userID.
      * @return User instance.
-     * @throws UserNotFoundException  when the user is not found.
+     * @throws UserNotFoundException  when the user is not found in the main tree.
      * @throws TransientException     If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
@@ -425,6 +427,23 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         return getUser(userID, config.getUsersDN());
     }
 
+    /**
+     * Obtain a user who is awaiting approval.
+     *
+     * @param userID        The user ID of the pending user.
+     * @return              A User instance awaiting approval.
+     *
+     * @throws UserNotFoundException  when the user is not found in the user request tree.
+     * @throws TransientException     If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public User<T> getPendingUser(final T userID)
+            throws UserNotFoundException, TransientException,
+                   AccessControlException
+    {
+        return getUser(userID, config.getUserRequestsDN());
+    }
+
 
     /**
      * Get the user specified by userID.
@@ -447,7 +466,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
                     "Unsupported principal type " + userID.getClass());
         }
 
-        searchField = "(&(objectclass=inetorgperson)(" +
+        searchField = "(&(objectclass=inetorgperson)(objectclass=cadcaccount)(" +
                       searchField + "=" + userID.getName() + "))";
         logger.debug(searchField);
 
@@ -697,7 +716,8 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
     }
 
     /**
-     * Get all groups the user specified by userID belongs to.
+     * Get all groups the user specified by userID belongs to. This method is created
+     * to provide optimization for the LDAP server.
      *
      * @param userID  The userID.
      * @param isAdmin
@@ -706,7 +726,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @throws TransientException     If an temporary, unexpected problem occurred., e.getMessage(
      * @throws AccessControlException If the operation is not permitted.
      */
-    public Collection<DN> getUserGroups(final T userID, final boolean isAdmin)
+    protected Collection<DN> getUserGroups(final T userID, final boolean isAdmin)
             throws UserNotFoundException, TransientException,
                    AccessControlException
     {
@@ -773,7 +793,8 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
     }
 
     /**
-     * Check whether the user is a member of the group.
+     * Check whether the user is a member of the group. This method is created
+     * to provide optimization for the LDAP server.
      *
      * @param userID  The userID.
      * @param groupID The groupID.
@@ -832,7 +853,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @throws UserNotFoundException
      * @throws LDAPException
      */
-    User<X500Principal> getMember(DN userDN)
+    User<X500Principal> getX500User(DN userDN)
             throws UserNotFoundException, LDAPException
     {
         Filter filter =
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 05aedbac0b4c6e4eaae1f4cfbd1e09b7ebcf568f..6a042cc3785193c354f28626a785e803c8190f88 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
@@ -134,8 +134,7 @@ public class LdapUserPersistence<T extends Principal>
         try
         {
             userDAO = new LdapUserDAO<T>(this.config);
-            User<T> ret = userDAO.addUser(user);
-            return ret;
+            return userDAO.addUser(user);
         }
         finally
         {
@@ -164,8 +163,36 @@ public class LdapUserPersistence<T extends Principal>
         try
         {
             userDAO = new LdapUserDAO<T>(this.config);
-            User<T> ret = userDAO.getUser(userID);
-            return ret;
+            return userDAO.getUser(userID);
+        }
+        finally
+        {
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+
+    /**
+    * Get the user specified by userID whose account is pending approval.
+    *
+    * @param userID The userID.
+    * @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.
+    */
+    @Override
+    public User<T> getPendingUser(final T userID) throws UserNotFoundException,
+                                                         TransientException,
+                                                         AccessControlException
+    {
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(this.config);
+            return userDAO.getPendingUser(userID);
         }
         finally
         {
@@ -187,14 +214,14 @@ public class LdapUserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public Boolean loginUser(String userID, String password)
+    public Boolean doLogin(String userID, String password)
             throws UserNotFoundException, TransientException, AccessControlException
     {
         LdapUserDAO<T> userDAO = null;
         try
         {
             userDAO = new LdapUserDAO<T>(this.config);
-            return userDAO.loginUser(userID, password);
+            return userDAO.doLogin(userID, password);
         }
         finally
         {
@@ -204,11 +231,11 @@ public class LdapUserPersistence<T extends Principal>
             }
         }
     }
-       
+
     /**
      * Updated the user specified by User.
      *
-     * @param user
+     * @param user          The user to update.
      *
      * @return User instance.
      * 
@@ -224,8 +251,35 @@ public class LdapUserPersistence<T extends Principal>
         try
         {
             userDAO = new LdapUserDAO<T>(this.config);
-            User<T> ret = userDAO.modifyUser(user);
-            return ret;
+            return userDAO.modifyUser(user);
+        }
+        finally
+        {
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+
+    /**
+     * Update a user's password. The given user and authenticating user must match.
+     *
+     * @param user
+     * @param oldPassword   current password.
+     * @param newPassword   new password.
+     * @throws UserNotFoundException If the given user does not exist.
+     * @throws TransientException   If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public void setPassword(User<T> user, final String oldPassword, final String newPassword)
+            throws UserNotFoundException, TransientException, AccessControlException
+    {
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(this.config);
+            userDAO.setPassword(user, oldPassword, newPassword);
         }
         finally
         {
@@ -265,7 +319,8 @@ public class LdapUserPersistence<T extends Principal>
     }
     
     /**
-     * Get all groups the user specified by userID belongs to.
+     * Get all groups the user specified by userID belongs to. This method is created
+     * to provide optimization for the LDAP server.
      * 
      * @param userID The userID.
      * @param isAdmin return only admin Groups when true, else return non-admin
@@ -277,15 +332,14 @@ public class LdapUserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public Collection<DN> getUserGroups(T userID, boolean isAdmin)
+    Collection<DN> getUserGroups(T userID, boolean isAdmin)
         throws UserNotFoundException, TransientException, AccessControlException
     {
         LdapUserDAO<T> userDAO = null;
         try
         {
             userDAO = new LdapUserDAO<T>(this.config);
-            Collection<DN> ret = userDAO.getUserGroups(userID, isAdmin);
-            return ret;
+            return userDAO.getUserGroups(userID, isAdmin);
         }
         finally
         {
@@ -297,7 +351,8 @@ public class LdapUserPersistence<T extends Principal>
     }
     
     /**
-     * Check whether the user is a member of the group.
+     * Check whether the user is a member of the group. This method is created
+     * to provide optimization for the LDAP server.
      *
      * @param userID The userID.
      * @param groupID The groupID.
@@ -308,7 +363,7 @@ public class LdapUserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public boolean isMember(T userID, String groupID)
+    boolean isMember(T userID, String groupID)
         throws UserNotFoundException, TransientException,
                AccessControlException
     {
@@ -316,8 +371,7 @@ public class LdapUserPersistence<T extends Principal>
         try
         {
             userDAO = new LdapUserDAO<T>(this.config);
-            boolean ret = userDAO.isMember(userID, groupID);
-            return ret;
+            return userDAO.isMember(userID, groupID);
         }
         finally
         {
@@ -327,5 +381,4 @@ public class LdapUserPersistence<T extends Principal>
             }
         }
     }
-
 }
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
new file mode 100644
index 0000000000000000000000000000000000000000..9ebd42f75c00d63f1ccc05a6cee320bb8e3b6d7f
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/SyncOutput.java
@@ -0,0 +1,127 @@
+/*
+************************************************************************
+*******************  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.web;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.log4j.Logger;
+
+/**
+ *
+ * @author majorb
+ */
+public class SyncOutput
+{
+    private static final Logger log = Logger.getLogger(SyncOutput.class);
+
+    protected HttpServletResponse response;
+    protected PrintWriter writer;
+
+    public SyncOutput(HttpServletResponse response)
+    {
+        this.response = response;
+    }
+
+    public boolean isOpen()
+    {
+        return (writer != null);
+    }
+
+    public void setCode(int code)
+    {
+        if (writer != null)
+            return;
+
+        response.setStatus(code);
+    }
+
+    public void setHeader(String key, Object value)
+    {
+        if (writer != null)
+            return;
+
+        if (value == null)
+            response.setHeader(key, null);
+        else
+            response.setHeader(key, value.toString());
+    }
+
+    public PrintWriter getWriter()
+        throws IOException
+    {
+        if (writer == null)
+        {
+            log.debug("opening writer");
+            writer = response.getWriter();
+        }
+        return writer;
+    }
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/WebUtil.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/WebUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a19dcff8360b26c255a0744f195d66a5526ab1b
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/WebUtil.java
@@ -0,0 +1,100 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.util.StringUtil;
+
+/**
+ * Utility methods for the ac web classes.
+ */
+public class WebUtil
+{
+    public static String[] getPathSegments(String path)
+    {
+        String[] segments = new String[0];
+        if (path == null)
+        {
+            return segments;
+        }
+        if (path.startsWith("/"))
+        {
+            path = path.substring(1);
+        }
+        if (path.endsWith("/"))
+        {
+            path = path.substring(0, path.length() - 1);
+        }
+        if (StringUtil.hasText(path))
+        {
+            segments = path.split("/");
+        }
+        return segments;
+    }
+}
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 a6409ad49a03dfa8167385d66256577d3ae235f3..f576ec6ba47de6cf4cff2ee3e8fb394d142e3a32 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
@@ -74,7 +74,7 @@ import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.ac.server.PluginFactory;
 import ca.nrc.cadc.ac.server.RequestValidator;
-import ca.nrc.cadc.ac.xml.GroupsWriter;
+import ca.nrc.cadc.ac.xml.GroupListWriter;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.net.TransientException;
@@ -236,7 +236,8 @@ public class ACSearchRunner implements JobRunner
                 groups = new ArrayList<Group>();
             }
             syncOut.setResponseCode(HttpServletResponse.SC_OK);
-            GroupsWriter.write(groups, syncOut.getOutputStream());
+            GroupListWriter groupListWriter = new GroupListWriter();
+            groupListWriter.write(groups, syncOut.getOutputStream());
             
             // Mark the Job as completed.
 //            jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING, 
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
similarity index 86%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
index 4ef1811573d47b135823e0e584ae9b2ace14b720..2805eda54b4e7bebfe5f0fc719c84085651f6dfd 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
@@ -76,6 +76,7 @@ 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;
@@ -88,47 +89,42 @@ import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 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;
 
-public abstract class GroupsAction
-    implements PrivilegedExceptionAction<Object>
+public abstract class AbstractGroupAction implements PrivilegedExceptionAction<Object>
 {
-    private static final Logger log = Logger.getLogger(GroupsAction.class);
+    private static final Logger log = Logger.getLogger(AbstractGroupAction.class);
     protected GroupLogInfo logInfo;
-    protected HttpServletResponse response;
+    protected HttpServletRequest request;
+    protected SyncOutput syncOut;
 
-    GroupsAction(GroupLogInfo logInfo)
+    public AbstractGroupAction()
+    {
+    }
+
+    abstract void doAction() throws Exception;
+
+    void setLogInfo(GroupLogInfo logInfo)
     {
         this.logInfo = logInfo;
     }
 
-    public void doAction(Subject subject, HttpServletResponse response)
-        throws IOException
+    void setHttpServletRequest(HttpServletRequest request)
+    {
+        this.request = request;
+    }
+
+    void setSyncOut(SyncOutput syncOut)
+    {
+        this.syncOut = syncOut;
+    }
+
+    public Object run() throws PrivilegedActionException
     {
         try
         {
-            try
-            {
-                this.response = response;
-
-                if (subject == null)
-                {
-                    run();
-                }
-                else
-                {
-                    Subject.doAs(subject, this);
-                }
-            }
-            catch (PrivilegedActionException e)
-            {
-                Throwable cause = e.getCause();
-                if (cause != null)
-                {
-                    throw cause;
-                }
-                throw e;
-            }
+            doAction();
         }
         catch (AccessControlException e)
         {
@@ -201,30 +197,29 @@ public abstract class GroupsAction
             log.error(message, t);
             sendError(500, message);
         }
+        return null;
     }
 
     private void sendError(int responseCode)
-        throws IOException
     {
         sendError(responseCode, null);
     }
 
     private void sendError(int responseCode, String message)
-        throws IOException
     {
-        if (!this.response.isCommitted())
+        syncOut.setHeader("Content-Type", "text/plain");
+        if (message != null)
         {
-            this.response.setContentType("text/plain");
-            if (message != null)
+            try
             {
-                this.response.getWriter().write(message);
+                syncOut.getWriter() .write(message);
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write error message to output stream");
             }
-            this.response.setStatus(responseCode);
-        }
-        else
-        {
-            log.warn("Could not send error " + responseCode + " (" + message + ") because the response is already committed.");
         }
+        syncOut.setCode(responseCode);
     }
 
     <T extends Principal> GroupPersistence<T> getGroupPersistence()
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberAction.java
index 61669fa27d74b9f372b10fe30c4225c1e63d6a6b..0f5d204b5a0d8d70845f6ec58241e49d2a3f3008 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberAction.java
@@ -74,21 +74,20 @@ import ca.nrc.cadc.ac.server.GroupPersistence;
 import java.util.ArrayList;
 import java.util.List;
 
-public class AddGroupMemberAction extends GroupsAction
+public class AddGroupMemberAction extends AbstractGroupAction
 {
     private final String groupName;
     private final String groupMemberName;
 
-    AddGroupMemberAction(GroupLogInfo logInfo, String groupName,
+    AddGroupMemberAction(String groupName,
                          String groupMemberName)
     {
-        super(logInfo);
+        super();
         this.groupName = groupName;
         this.groupMemberName = groupMemberName;
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
         Group group = groupPersistence.getGroup(this.groupName);
@@ -102,7 +101,6 @@ public class AddGroupMemberAction extends GroupsAction
         List<String> addedMembers = new ArrayList<String>();
         addedMembers.add(toAdd.getID());
         logGroupInfo(group.getID(), null, addedMembers);
-        return null;
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberAction.java
index cdf6093abea691a7412e1fd0a4a6bacd36c12cd4..30c4b8be94e9931f4e5b073bcddde315ccffcc98 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberAction.java
@@ -77,24 +77,23 @@ import java.security.Principal;
 import java.util.ArrayList;
 import java.util.List;
 
-public class AddUserMemberAction extends GroupsAction
+public class AddUserMemberAction extends AbstractGroupAction
 {
     private final String groupName;
     private final String userID;
     private final String userIDType;
 
-    AddUserMemberAction(GroupLogInfo logInfo, String groupName, String userID,
+    AddUserMemberAction(String groupName, String userID,
                         String userIDType)
     {
-        super(logInfo);
+        super();
         this.groupName = groupName;
         this.userID = userID;
         this.userIDType = userIDType;
     }
 
     @SuppressWarnings("unchecked")
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
         Group group = groupPersistence.getGroup(this.groupName);
@@ -104,13 +103,12 @@ public class AddUserMemberAction extends GroupsAction
         {
             throw new MemberAlreadyExistsException();
         }
-        
+
         groupPersistence.modifyGroup(group);
 
         List<String> addedMembers = new ArrayList<String>();
         addedMembers.add(toAdd.getUserID().getName());
         logGroupInfo(group.getID(), null, addedMembers);
-        return null;
     }
 
 }
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 f282d722627e25796756e8921ae77ca2fb4f3940..a4341c40aa82975b1aebf363794f30dc1a00d95c 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
@@ -78,24 +78,25 @@ import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.ac.xml.GroupReader;
 import ca.nrc.cadc.ac.xml.GroupWriter;
 
-public class CreateGroupAction extends GroupsAction
+public class CreateGroupAction extends AbstractGroupAction
 {
     private final InputStream inputStream;
 
-    CreateGroupAction(GroupLogInfo logInfo, InputStream inputStream)
+    CreateGroupAction(InputStream inputStream)
     {
-        super(logInfo);
+        super();
         this.inputStream = inputStream;
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
-        Group group = GroupReader.read(this.inputStream);
+        GroupReader groupReader = new GroupReader();
+        Group group = groupReader.read(this.inputStream);
         Group newGroup = groupPersistence.addGroup(group);
-        this.response.setContentType("application/xml");
-        GroupWriter.write(newGroup, this.response.getOutputStream());
+        syncOut.setHeader("Content-Type", "application/xml");
+        GroupWriter groupWriter = new GroupWriter();
+        groupWriter.write(newGroup, syncOut.getWriter());
 
         List<String> addedMembers = null;
         if ((newGroup.getUserMembers().size() > 0) || (newGroup.getGroupMembers().size() > 0))
@@ -111,7 +112,6 @@ public class CreateGroupAction extends GroupsAction
             }
         }
         logGroupInfo(newGroup.getID(), null, addedMembers);
-        return null;
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupAction.java
index 2c96f279f30a5108de5be505761094a2c4e5bf9a..52928fbd95aa5c2cffe90c663d41a1ed4b55ce51 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupAction.java
@@ -74,18 +74,17 @@ import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 
-public class DeleteGroupAction extends GroupsAction
+public class DeleteGroupAction extends AbstractGroupAction
 {
     private final String groupName;
 
-    DeleteGroupAction(GroupLogInfo logInfo, String groupName)
+    DeleteGroupAction(String groupName)
     {
-        super(logInfo);
+        super();
         this.groupName = groupName;
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
         Group deletedGroup = groupPersistence.getGroup(this.groupName);
@@ -102,7 +101,6 @@ public class DeleteGroupAction extends GroupsAction
                 this.logInfo.deletedMembers.add(usr.getUserID().getName());
             }
         }
-        return null;
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupAction.java
index 3d7d9646f6692d46f3c76e2943702a7d55707bb2..990411864d359c02b8c5edb78c80e7b41a6a5ade 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupAction.java
@@ -71,24 +71,23 @@ import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.ac.xml.GroupWriter;
 
-public class GetGroupAction extends GroupsAction
+public class GetGroupAction extends AbstractGroupAction
 {
     private final String groupName;
 
-    GetGroupAction(GroupLogInfo logInfo, String groupName)
+    GetGroupAction( String groupName)
     {
-        super(logInfo);
+        super();
         this.groupName = groupName;
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
         Group group = groupPersistence.getGroup(this.groupName);
-        this.response.setContentType("application/xml");
-        GroupWriter.write(group, this.response.getOutputStream());
-        return null;
+        syncOut.setHeader("Content-Type", "application/xml");
+        GroupWriter groupWriter = new GroupWriter();
+        groupWriter.write(group, syncOut.getWriter());
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesAction.java
index a62ef9526d1ff961f6f9b0a88ea82283762f061c..ff26ddd8d19de845b6d6b7cc502f18f96c14b2a8 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesAction.java
@@ -76,25 +76,24 @@ import org.apache.log4j.Logger;
 
 import ca.nrc.cadc.ac.server.GroupPersistence;
 
-public class GetGroupNamesAction extends GroupsAction
+public class GetGroupNamesAction extends AbstractGroupAction
 {
-    
+
     private static final Logger log = Logger.getLogger(GetGroupNamesAction.class);
 
-    GetGroupNamesAction(GroupLogInfo logInfo)
+    GetGroupNamesAction()
     {
-        super(logInfo);
+        super();
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
         Collection<String> groups = groupPersistence.getGroupNames();
         log.debug("Found " + groups.size() + " group names");
-        response.setContentType("text/plain");
+        syncOut.setHeader("Content-Type", "text/plain");
         log.debug("Set content-type to text/plain");
-        Writer writer = response.getWriter();
+        Writer writer = syncOut.getWriter();
         boolean start = true;
         for (final String group : groups)
         {
@@ -105,7 +104,5 @@ public class GetGroupNamesAction extends GroupsAction
             writer.write(group);
             start = false;
         }
-        
-        return null;
     }
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupLogInfo.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupLogInfo.java
index 38739a0849e2755fdcd6c42503a9e402a3cae8dd..7840104fdeb406edfeac5be1999a37916f8361bd 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupLogInfo.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupLogInfo.java
@@ -72,6 +72,10 @@ import ca.nrc.cadc.log.ServletLogInfo;
 import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 
+/**
+ * Extension of regular servlet log info that tracks
+ * group membership changes.
+ */
 public class GroupLogInfo extends ServletLogInfo
 {
     public String groupID;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
similarity index 75%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsServlet.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
index cd4682325e14892bdb73c8cd96c7ed1782368730..096b5827a25ad73b5efcb91427879d0c3cd28943 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
@@ -69,6 +69,7 @@
 package ca.nrc.cadc.ac.server.web.groups;
 
 import java.io.IOException;
+import java.security.PrivilegedActionException;
 
 import javax.security.auth.Subject;
 import javax.servlet.http.HttpServlet;
@@ -77,16 +78,24 @@ import javax.servlet.http.HttpServletResponse;
 
 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;
 
-public class GroupsServlet extends HttpServlet
+/**
+ * Servlet for handling all requests to /groups
+ *
+ * @author majorb
+ */
+public class GroupServlet extends HttpServlet
 {
-    private static final Logger log = Logger.getLogger(GroupsServlet.class);
+    private static final long serialVersionUID = 7854660717655869213L;
+    private static final Logger log = Logger.getLogger(GroupServlet.class);
 
     /**
      * Create a GroupAction and run the action safely.
      */
-    private void doAction(HttpServletRequest request, HttpServletResponse response)
+    private void doAction(GroupsActionFactory factory, HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
         long start = System.currentTimeMillis();
@@ -96,14 +105,44 @@ public class GroupsServlet extends HttpServlet
             log.info(logInfo.start());
             Subject subject = AuthenticationUtil.getSubject(request);
             logInfo.setSubject(subject);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, logInfo);
-            action.doAction(subject, response);
+
+            AbstractGroupAction action = factory.createAction(request);
+
+            action.setLogInfo(logInfo);
+            action.setHttpServletRequest(request);
+            SyncOutput syncOut = new SyncOutput(response);
+            action.setSyncOut(syncOut);
+
+            try
+            {
+                if (subject == null)
+                {
+                    action.run();
+                }
+                else
+                {
+                    Subject.doAs(subject, action);
+                }
+            }
+            catch (PrivilegedActionException e)
+            {
+                Throwable cause = e.getCause();
+                if (cause != null)
+                {
+                    throw cause;
+                }
+                Exception exception = e.getException();
+                if (exception != null)
+                {
+                    throw exception;
+                }
+                throw e;
+            }
         }
         catch (IllegalArgumentException e)
         {
             log.debug(e.getMessage(), e);
             logInfo.setMessage(e.getMessage());
-            logInfo.setSuccess(false);
             response.getWriter().write(e.getMessage());
             response.setStatus(400);
         }
@@ -124,38 +163,38 @@ public class GroupsServlet extends HttpServlet
     }
 
     @Override
-    public void doGet(HttpServletRequest request, HttpServletResponse response)
+    public void doGet(final HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(GroupsActionFactory.httpGetFactory(), request, response);
     }
 
     @Override
     public void doPost(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(GroupsActionFactory.httpPostFactory(), request, response);
     }
 
     @Override
     public void doDelete(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(GroupsActionFactory.httpDeleteFactory(), request, response);
     }
 
     @Override
     public void doPut(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(GroupsActionFactory.httpPutFactory(), request, response);
     }
 
     @Override
     public void doHead(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(GroupsActionFactory.httpHeadFactory(), request, response);
     }
 
 }
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 c2aaf942be82e190f33b7f9e1f7cb40aa6fafe97..5cf7a054c70568d75fdddfa964a2a6334eb1c401 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
@@ -76,126 +76,206 @@ import javax.servlet.http.HttpServletRequest;
 
 import org.apache.log4j.Logger;
 
+import ca.nrc.cadc.ac.server.web.WebUtil;
 import ca.nrc.cadc.util.StringUtil;
 
-public class GroupsActionFactory
+/**
+ * This class provides static methods for each of the http methods for
+ * creating a factory object that will in turn create the correct group
+ * action.
+ *
+ * @author majorb
+ */
+public abstract class GroupsActionFactory
 {
     private static final Logger log = Logger.getLogger(GroupsActionFactory.class);
 
-    static GroupsAction getGroupsAction(HttpServletRequest request, GroupLogInfo logInfo)
-        throws IOException
-    {
-        GroupsAction action = null;
-        String method = request.getMethod();
-        String path = request.getPathInfo();
-        log.debug("method: " + method);
-        log.debug("path: " + path);
-
-        if (path == null)
-        {
-            path = "";
-        }
+    public abstract AbstractGroupAction createAction(HttpServletRequest request)
+        throws IllegalArgumentException, IOException;
 
-        if (path.startsWith("/"))
+    public static GroupsActionFactory httpGetFactory()
+    {
+        return new GroupsActionFactory()
         {
-            path = path.substring(1);
-        }
+            public AbstractGroupAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
+            {
+                AbstractGroupAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
 
-        if (path.endsWith("/"))
-        {
-            path = path.substring(0, path.length() - 1);
-        }
+                String[] segments = WebUtil.getPathSegments(path);
 
-        String[] segments = new String[0];
-        if (StringUtil.hasText(path))
-        {
-            segments = path.split("/");
-        }
+                if (segments.length == 0)
+                {
+                    action = new GetGroupNamesAction();
+                }
+                else if (segments.length == 1)
+                {
+                    String groupName = segments[0];
+                    action = new GetGroupAction(groupName);
+                }
 
-        if (segments.length == 0)
-        {
-            if (method.equals("GET"))
-            {
-                action = new GetGroupNamesAction(logInfo);
-            }
-            else if (method.equals("PUT"))
-            {
-                action = new CreateGroupAction(logInfo, request.getInputStream());
+                if (action != null)
+                {
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
+                }
+                throw new IllegalArgumentException("Bad GET request to " + path);
             }
+        };
+    }
 
-        }
-        else if (segments.length == 1)
+    public static GroupsActionFactory httpPutFactory()
+    {
+        return new GroupsActionFactory()
         {
-            String groupName = segments[0];
-            if (method.equals("GET"))
-            {
-                action = new GetGroupAction(logInfo, groupName);
-            }
-            else if (method.equals("DELETE"))
+            public AbstractGroupAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
             {
-                action = new DeleteGroupAction(logInfo, groupName);
-            }
-            else if (method.equals("POST"))
-            {
-                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)
+                AbstractGroupAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
+
+                String[] segments = WebUtil.getPathSegments(path);
+
+                if (segments.length == 0)
                 {
-                    sb.append(":");
-                    sb.append(requestURL.getPort());
+                    action = new CreateGroupAction(request.getInputStream());
+                }
+                else if (segments.length == 3)
+                {
+                    String groupName = segments[0];
+                    String memberCategory = segments[1];
+                    if (memberCategory.equals("groupMembers"))
+                    {
+                        String groupMemberName = segments[2];
+                        action = new AddGroupMemberAction(groupName, groupMemberName);
+                    }
+                    else if (memberCategory.equals("userMembers"))
+                    {
+                        String userMemberID = segments[2];
+                        String userMemberIDType = request.getParameter("idType");
+                        action = new AddUserMemberAction(groupName, userMemberID, userMemberIDType);
+                    }
                 }
-                sb.append(request.getContextPath());
-                sb.append(request.getServletPath());
-                sb.append("/");
-                sb.append(path);
 
-                action = new ModifyGroupAction(logInfo, groupName, sb.toString(),
-                                               request.getInputStream());
+                if (action != null)
+                {
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
+                }
+                throw new IllegalArgumentException("Bad PUT request to " + path);
             }
-        }
-        else if (segments.length == 3)
+        };
+    }
+
+    public static GroupsActionFactory httpPostFactory()
+    {
+        return new GroupsActionFactory()
         {
-            String groupName = segments[0];
-            String memberCategory = segments[1];
-            if (method.equals("PUT"))
+            public AbstractGroupAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
             {
-                if (memberCategory.equals("groupMembers"))
+                AbstractGroupAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
+
+                String[] segments = WebUtil.getPathSegments(path);
+
+                if (segments.length == 1)
                 {
-                    String groupMemberName = segments[2];
-                    action = new AddGroupMemberAction(logInfo, groupName, groupMemberName);
+
+
+                    String groupName = segments[0];
+
+                    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("/");
+                    sb.append(path);
+
+                    action = new ModifyGroupAction(groupName, sb.toString(), request.getInputStream());
                 }
-                else if (memberCategory.equals("userMembers"))
+
+                if (action != null)
                 {
-                    String userMemberID = URLDecoder.decode(segments[2], "UTF-8");
-                    String userMemberIDType = request.getParameter("idType");
-                    action = new AddUserMemberAction(logInfo, groupName, userMemberID, userMemberIDType);
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
                 }
+                throw new IllegalArgumentException("Bad POST request to " + path);
             }
-            else if (method.equals("DELETE"))
+        };
+
+    }
+
+    public static GroupsActionFactory httpDeleteFactory()
+    {
+        return new GroupsActionFactory()
+        {
+            public AbstractGroupAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
             {
-                if (memberCategory.equals("groupMembers"))
+                AbstractGroupAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
+
+                String[] segments = WebUtil.getPathSegments(path);
+
+                if (segments.length == 1)
                 {
-                    String groupMemberName = segments[2];
-                    action = new RemoveGroupMemberAction(logInfo, groupName, groupMemberName);
+                    String groupName = segments[0];
+                    action = new DeleteGroupAction(groupName);
                 }
-                else if (memberCategory.equals("userMembers"))
+                else if (segments.length == 3)
+                {
+                    String groupName = segments[0];
+                    String memberCategory = segments[1];
+
+                    if (memberCategory.equals("groupMembers"))
+                    {
+                        String groupMemberName = segments[2];
+                        action = new RemoveGroupMemberAction(groupName, groupMemberName);
+                    }
+                    else if (memberCategory.equals("userMembers"))
+                    {
+                        String memberUserID = segments[2];
+                        String memberUserIDType = request.getParameter("idType");
+                        action = new RemoveUserMemberAction(groupName, memberUserID, memberUserIDType);
+                    }
+                }
+
+                if (action != null)
                 {
-                    String memberUserID = URLDecoder.decode(segments[2], "UTF-8");
-                    String memberUserIDType = request.getParameter("idType");
-                    action = new RemoveUserMemberAction(logInfo, groupName, memberUserID, memberUserIDType);
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
                 }
+                throw new IllegalArgumentException("Bad DELETE request to " + path);
             }
-        }
+        };
 
-        if (action != null)
-        {
-            log.debug("Returning action: " + action.getClass());
-            return action;
-        }
-        throw new IllegalArgumentException("Bad groups request: " + method + " on " + path);
     }
 
+    public static GroupsActionFactory httpHeadFactory()
+    {
+        return new GroupsActionFactory()
+        {
+            public AbstractGroupAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
+            {
+                // http head not supported
+                throw new UnsupportedOperationException();
+            }
+        };
+
+    }
 }
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 444e335dc3594ec7543872a5f77db25ef0cc4c32..f4094251a8e57c4f7a351ff40b12d357157df1ab 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
@@ -77,26 +77,25 @@ import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.ac.xml.GroupReader;
 
-public class ModifyGroupAction extends GroupsAction
+public class ModifyGroupAction extends AbstractGroupAction
 {
     private final String groupName;
     private final String request;
     private final InputStream inputStream;
 
-    ModifyGroupAction(GroupLogInfo logInfo, String groupName,
-                      final String request, InputStream inputStream)
+    ModifyGroupAction(String groupName, final String request, InputStream inputStream)
     {
-        super(logInfo);
+        super();
         this.groupName = groupName;
         this.request = request;
         this.inputStream = inputStream;
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
-        Group group = GroupReader.read(this.inputStream);
+        GroupReader groupReader = new GroupReader();
+        Group group = groupReader.read(this.inputStream);
         Group oldGroup = groupPersistence.getGroup(this.groupName);
         groupPersistence.modifyGroup(group);
 
@@ -134,9 +133,8 @@ public class ModifyGroupAction extends GroupsAction
         }
         logGroupInfo(group.getID(), deletedMembers, addedMembers);
 
-        this.response.sendRedirect(request);
-
-        return null;
+        syncOut.setHeader("Location", "/" + group.getID());
+        syncOut.setCode(303);
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberAction.java
index a05d17f93d73a061e96bb44975b3f68548b3b7c5..e1b5848bdcab28c79d103972de9aab461568e82b 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberAction.java
@@ -74,20 +74,19 @@ import ca.nrc.cadc.ac.server.GroupPersistence;
 import java.util.ArrayList;
 import java.util.List;
 
-public class RemoveGroupMemberAction extends GroupsAction
+public class RemoveGroupMemberAction extends AbstractGroupAction
 {
     private final String groupName;
     private final String groupMemberName;
 
-    RemoveGroupMemberAction(GroupLogInfo logInfo, String groupName, String groupMemberName)
+    RemoveGroupMemberAction(String groupName, String groupMemberName)
     {
-        super(logInfo);
+        super();
         this.groupName = groupName;
         this.groupMemberName = groupMemberName;
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
         Group group = groupPersistence.getGroup(this.groupName);
@@ -101,7 +100,6 @@ public class RemoveGroupMemberAction extends GroupsAction
         List<String> deletedMembers = new ArrayList<String>();
         deletedMembers.add(toRemove.getID());
         logGroupInfo(group.getID(), deletedMembers, null);
-        return null;
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberAction.java
index 20dcf8b4a32af6119d4033ea5b72db0389218ba1..d04fe4a6cfe0172eeb574caa82c92ce750e80c53 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberAction.java
@@ -77,23 +77,22 @@ import java.security.Principal;
 import java.util.ArrayList;
 import java.util.List;
 
-public class RemoveUserMemberAction extends GroupsAction
+public class RemoveUserMemberAction extends AbstractGroupAction
 {
     private final String groupName;
     private final String userID;
     private final String userIDType;
 
-    RemoveUserMemberAction(GroupLogInfo logInfo, String groupName, String userID, String userIDType)
+    RemoveUserMemberAction(String groupName, String userID, String userIDType)
     {
-        super(logInfo);
+        super();
         this.groupName = groupName;
         this.userID = userID;
         this.userIDType = userIDType;
     }
 
     @SuppressWarnings("unchecked")
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         GroupPersistence groupPersistence = getGroupPersistence();
         Group group = groupPersistence.getGroup(this.groupName);
@@ -108,7 +107,6 @@ public class RemoveUserMemberAction extends GroupsAction
         List<String> deletedMembers = new ArrayList<String>();
         deletedMembers.add(toRemove.getUserID().getName());
         logGroupInfo(group.getID(), deletedMembers, null);
-        return null;
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
similarity index 68%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
index df6880b92f0ab5c5bf2116e956b3d88bfc7c5798..07a83782a467cf199f1106d3a21df5481fa67561 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
@@ -72,65 +72,75 @@ import ca.nrc.cadc.ac.PersonalDetails;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.json.JsonUserListWriter;
+import ca.nrc.cadc.ac.json.JsonUserReader;
+import ca.nrc.cadc.ac.json.JsonUserRequestReader;
+import ca.nrc.cadc.ac.json.JsonUserWriter;
 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.auth.AuthenticationUtil;
+import ca.nrc.cadc.ac.xml.UserListWriter;
+import ca.nrc.cadc.ac.xml.UserReader;
+import ca.nrc.cadc.ac.xml.UserRequestReader;
+import ca.nrc.cadc.ac.xml.UserWriter;
 import ca.nrc.cadc.net.TransientException;
 import org.apache.log4j.Logger;
 
-import javax.security.auth.Subject;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Writer;
 import java.security.AccessControlException;
 import java.security.Principal;
-import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 
-public abstract class UsersAction
-    implements PrivilegedExceptionAction<Object>
+public abstract class AbstractUserAction implements PrivilegedExceptionAction<Object>
 {
-    private static final Logger log = Logger.getLogger(UsersAction.class);
+    private static final Logger log = Logger.getLogger(AbstractUserAction.class);
     static final String DEFAULT_CONTENT_TYPE = "text/xml";
     static final String JSON_CONTENT_TYPE = "application/json";
 
+    protected String augmentUserDN;
     protected UserLogInfo logInfo;
-    protected HttpServletResponse response;
+    protected SyncOutput syncOut;
+
     protected String acceptedContentType = DEFAULT_CONTENT_TYPE;
 
-    UsersAction(UserLogInfo logInfo)
+    AbstractUserAction()
+    {
+    }
+
+    public abstract void doAction() throws Exception;
+
+    public void setAugmentUserDN(final String dn)
+    {
+    	this.augmentUserDN = dn;
+    }
+    
+    public String getAugmentUserDN()
+    {
+    	return this.augmentUserDN;
+    }
+    
+    public void setLogInfo(UserLogInfo logInfo)
     {
         this.logInfo = logInfo;
     }
 
-    public void doAction(Subject subject, HttpServletResponse response)
-        throws IOException
+    public void setSyncOut(SyncOutput syncOut)
+    {
+        this.syncOut = syncOut;
+    }
+
+    public Object run() throws IOException
     {
         try
         {
-            try
-            {
-                this.response = response;
-
-                if (subject == null)
-                {
-                    run();
-                }
-                else
-                {
-                    Subject.doAs(subject, this);
-                }
-            }
-            catch (PrivilegedActionException e)
-            {
-                Throwable cause = e.getCause();
-                if (cause != null)
-                {
-                    throw cause;
-                }
-                throw e;
-            }
+            doAction();
         }
         catch (AccessControlException e)
         {
@@ -175,6 +185,7 @@ public abstract class UsersAction
             log.error(message, t);
             sendError(500, message);
         }
+        return null;
     }
 
     private void sendError(int responseCode)
@@ -184,21 +195,20 @@ public abstract class UsersAction
     }
 
     private void sendError(int responseCode, String message)
-        throws IOException
     {
-        if (!this.response.isCommitted())
+        syncOut.setHeader("Content-Type", "text/plain");
+        if (message != null)
         {
-            this.response.setContentType("text/plain");
-            if (message != null)
+            try
             {
-                this.response.getWriter().write(message);
+                syncOut.getWriter() .write(message);
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write error message to output stream");
             }
-            this.response.setStatus(responseCode);
-        }
-        else
-        {
-            log.warn("Could not send error " + responseCode + " (" + message + ") because the response is already committed.");
         }
+        syncOut.setCode(responseCode);
     }
 
     @SuppressWarnings("unchecked")
@@ -233,12 +243,13 @@ public abstract class UsersAction
 
         if (acceptedContentType.equals(DEFAULT_CONTENT_TYPE))
         {
-            userRequest = ca.nrc.cadc.ac.xml.UserRequestReader.read(inputStream);
+            UserRequestReader requestReader = new UserRequestReader();
+            userRequest = requestReader.read(inputStream);
         }
         else if (acceptedContentType.equals(JSON_CONTENT_TYPE))
         {
-            userRequest =
-                    ca.nrc.cadc.ac.json.UserRequestReader.read(inputStream);
+            JsonUserRequestReader requestReader = new JsonUserRequestReader();
+            userRequest = requestReader.read(inputStream);
         }
         else
         {
@@ -250,6 +261,40 @@ public abstract class UsersAction
         return userRequest;
     }
 
+    /**
+     * Read the user from the given stream of marshalled data.
+     *
+     * @param inputStream       The stream to read in.
+     * @return                  User instance, never null.
+     *
+     * @throws IOException      Any errors in reading the stream.
+     */
+    protected final User<Principal> readUser(final InputStream inputStream)
+            throws IOException
+    {
+        syncOut.setHeader("Content-Type", acceptedContentType);
+        final User<Principal> user;
+
+        if (acceptedContentType.equals(DEFAULT_CONTENT_TYPE))
+        {
+            UserReader userReader = new UserReader();
+            user = userReader.read(inputStream);
+        }
+        else if (acceptedContentType.equals(JSON_CONTENT_TYPE))
+        {
+            JsonUserReader userReader = new JsonUserReader();
+            user = userReader.read(inputStream);
+        }
+        else
+        {
+            // Should never happen.
+            throw new IOException("Unknown content being asked for: "
+                                  + acceptedContentType);
+        }
+
+        return user;
+    }
+
     /**
      * Write a user to the response's writer.
      *
@@ -259,16 +304,18 @@ public abstract class UsersAction
     protected final <T extends Principal> void writeUser(final User<T> user)
             throws IOException
     {
-        response.setContentType(acceptedContentType);
-        final Writer writer = response.getWriter();
+        syncOut.setHeader("Content-Type", acceptedContentType);
+        final Writer writer = syncOut.getWriter();
 
         if (acceptedContentType.equals(DEFAULT_CONTENT_TYPE))
         {
-            ca.nrc.cadc.ac.xml.UserWriter.write(user, writer);
+            UserWriter userWriter = new UserWriter();
+            userWriter.write(user, writer);
         }
         else if (acceptedContentType.equals(JSON_CONTENT_TYPE))
         {
-            ca.nrc.cadc.ac.json.UserWriter.write(user, writer);
+            JsonUserWriter userWriter = new JsonUserWriter();
+            userWriter.write(user, writer);
         }
     }
 
@@ -280,16 +327,43 @@ public abstract class UsersAction
     protected final void writeUsers(final Map<String, PersonalDetails> users)
             throws IOException
     {
-        response.setContentType(acceptedContentType);
-        final Writer writer = response.getWriter();
+        syncOut.setHeader("Content-Type", acceptedContentType);
+        final Writer writer = syncOut.getWriter();
 
         if (acceptedContentType.equals(DEFAULT_CONTENT_TYPE))
         {
-            ca.nrc.cadc.ac.xml.UsersWriter.write(users, writer);
+            UserListWriter userListWriter = new UserListWriter();
+            userListWriter.write(users, writer);
         }
         else if (acceptedContentType.equals(JSON_CONTENT_TYPE))
         {
-            ca.nrc.cadc.ac.json.UsersWriter.write(users, writer);
+            JsonUserListWriter userListWriter = new JsonUserListWriter();
+            userListWriter.write(users, writer);
+        }
+    }
+
+    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/CreateUserAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/CreateUserAction.java
index d53ad283126b8456164722deabfba261daf877c2..aa40229c9395fbf8eab582749be6fd2ab4c8b690 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/CreateUserAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/CreateUserAction.java
@@ -70,9 +70,7 @@ package ca.nrc.cadc.ac.server.web.users;
 
 import java.io.InputStream;
 
-import ca.nrc.cadc.ac.ReaderException;
 import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserAlreadyExistsException;
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.ac.server.UserPersistence;
 
@@ -80,42 +78,25 @@ import javax.servlet.http.HttpServletResponse;
 import java.security.Principal;
 
 
-public class CreateUserAction extends UsersAction
+public class CreateUserAction extends AbstractUserAction
 {
     private final InputStream inputStream;
 
-    CreateUserAction(UserLogInfo logInfo, InputStream inputStream)
+    CreateUserAction(final InputStream inputStream)
     {
-        super(logInfo);
+        super();
         this.inputStream = inputStream;
     }
 
-    public Object run()
-        throws Exception
-    {
-        try
-        {
-            final UserPersistence<Principal> userPersistence =
-                    getUserPersistence();
-            final UserRequest<Principal> userRequest =
-                    readUserRequest(this.inputStream);
-            final User<Principal> newUser =
-                    userPersistence.addUser(userRequest);
 
-            writeUser(newUser);
-            logUserInfo(newUser.getUserID().getName());
-        }
-        catch (UserAlreadyExistsException e)
-        {
-            response.setStatus(HttpServletResponse.SC_CONFLICT);
-            response.getWriter().write("User already exists");
-        }
-        catch (ReaderException e)
-        {
-            throw new IllegalArgumentException("Invalid input", e);
-        }
+    public void doAction() throws Exception
+    {
+        final UserPersistence<Principal> userPersistence = getUserPersistence();
+        final UserRequest<Principal> userRequest = readUserRequest(this.inputStream);
+        final User<Principal> newUser = userPersistence.addUser(userRequest);
 
-        return null;
+        syncOut.setCode(201);
+        logUserInfo(newUser.getUserID().getName());
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/DeleteUserAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/DeleteUserAction.java
index 806e8f9812bec8751246ef1f7f8a84b8081a2d29..aa74d299c1798e2799a66e60606a3def3097cc78 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/DeleteUserAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/DeleteUserAction.java
@@ -72,23 +72,21 @@ import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.UserPersistence;
 import java.security.Principal;
 
-public class DeleteUserAction extends UsersAction
+public class DeleteUserAction extends AbstractUserAction
 {
     private final Principal userID;
 
-    DeleteUserAction(UserLogInfo logInfo, Principal userID)
+    DeleteUserAction(Principal userID)
     {
-        super(logInfo);
+        super();
         this.userID = userID;
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         UserPersistence userPersistence = getUserPersistence();
         User<? extends Principal> deletedUser = userPersistence.getUser(userID);
         userPersistence.deleteUser(deletedUser.getUserID());
-        return null;
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUserAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUserAction.java
index d4ee1d0efe03a6bf8fecc24790caa7e0636f07e4..4793c719fb078488473eaca1bbb215c17a9a2aac 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUserAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUserAction.java
@@ -68,27 +68,88 @@
  */package ca.nrc.cadc.ac.server.web.users;
 
 import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.ac.server.UserPersistence;
 
+import java.security.AccessControlContext;
+import java.security.AccessController;
 import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
 
+import javax.security.auth.Subject;
 
-public class GetUserAction extends UsersAction
+import org.apache.log4j.Logger;
+
+
+public class GetUserAction extends AbstractUserAction
 {
+    private static final Logger log = Logger.getLogger(GetUserAction.class);
     private final Principal userID;
 
-    GetUserAction(UserLogInfo logInfo, Principal userID)
+    GetUserAction(Principal userID)
     {
-        super(logInfo);
+        super();
         this.userID = userID;
     }
 
-    public Object run() throws Exception
+	public void doAction() throws Exception
     {
-        UserPersistence<Principal> userPersistence = getUserPersistence();
-        User<Principal> user = userPersistence.getUser(userID);
+        User<Principal> user;
+ 
+        if (isServops())
+        {
+        	Subject subject = new Subject();
+        	subject.getPrincipals().add(this.userID);
+        	user = (User<Principal>) Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        	{
+				@Override
+				public Object run() throws Exception 
+				{
+					return getUser(userID);
+				}
+        		
+        	});
+        }
+        else
+        {
+        	user = getUser(this.userID);
+        }
+
         writeUser(user);
-        return null;
     }
 
+    protected User<Principal> getUser(Principal principal) throws Exception
+    {
+        final UserPersistence<Principal> userPersistence = getUserPersistence();
+    	User<Principal> user;
+    	
+    	try
+        {
+            user = userPersistence.getUser(principal);
+        }
+        catch (UserNotFoundException e)
+        {
+            user = userPersistence.getPendingUser(principal);
+        }
+    	
+    	return user;
+    }
+    
+    protected boolean isServops()
+    {
+    	log.debug("alinga-- isServops(): augmentUserDN = " + this.augmentUserDN);
+    	boolean isServops = false;
+        AccessControlContext acc = AccessController.getContext();
+        Subject subject = Subject.getSubject(acc);
+        for (Principal principal : subject.getPrincipals())
+        {
+        	if (principal.getName().equals(this.getAugmentUserDN()))
+        	{
+        		isServops = true;
+        		break;
+        	}
+        }
+        
+        return isServops;
+    }
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUsersAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUserListAction.java
similarity index 94%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUsersAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUserListAction.java
index 008a8572ec167b146db84497e82df94e4d95aee0..c601bd33737758fa0739e6ed6064c16392cc1cde 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUsersAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/GetUserListAction.java
@@ -75,22 +75,18 @@ import org.apache.log4j.Logger;
 import ca.nrc.cadc.ac.server.UserPersistence;
 
 
-public class GetUsersAction extends UsersAction
+public class GetUserListAction extends AbstractUserAction
 {
-    
-    private static final Logger log = Logger.getLogger(GetUsersAction.class);
 
-    GetUsersAction(UserLogInfo logInfo)
+    private static final Logger log = Logger.getLogger(GetUserListAction.class);
+
+    GetUserListAction()
     {
-        super(logInfo);
     }
 
-    public Object run()
-        throws Exception
+    public void doAction() throws Exception
     {
         final UserPersistence userPersistence = getUserPersistence();
-
         writeUsers(userPersistence.getUsers());
-        return null;
     }
 }
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 0b1a4c303ff98c46618cd07640f48daa9ba6b84e..f1daa9db4f5c8870590ca10c0698b5cbe55ce33c 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
@@ -106,7 +106,7 @@ public class LoginServlet extends HttpServlet
             {
                 if (StringUtil.hasText(password))
                 {
-                	if (new LdapUserPersistence().loginUser(userID, password))
+                	if (new LdapUserPersistence().doLogin(userID, password))
                 	{
 	            	    String token = new SSOCookieManager().generate(new HttpPrincipal(userID));
 	            	    response.setContentType(CONTENT_TYPE);
@@ -135,9 +135,10 @@ public class LoginServlet extends HttpServlet
         catch (AccessControlException e)
         {
             log.debug(e.getMessage(), e);
-            logInfo.setMessage(e.getMessage());
+            String message = "Invalid credentials";
+            logInfo.setMessage(message);
     	    response.setContentType(CONTENT_TYPE);
-            response.getWriter().write(e.getMessage());
+            response.getWriter().write(message);
             response.setStatus(401);
         }
         catch (Throwable t)
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 c98c29683542a64d608bcaf623dc7bbfdfb13125..4e7ebe372ba2a3c0feb149274bf8bbfcbb38b58f 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
@@ -68,41 +68,53 @@
  */
 package ca.nrc.cadc.ac.server.web.users;
 
-import java.io.InputStream;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.UserPersistence;
-import ca.nrc.cadc.ac.xml.UserReader;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.auth.HttpPrincipal;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
 import java.security.Principal;
+import java.util.Iterator;
+import java.util.Set;
+
 
-public class ModifyUserAction extends UsersAction
+public class ModifyUserAction extends AbstractUserAction
 {
-    private final Principal userID;
-    private final String request;
     private final InputStream inputStream;
 
-    ModifyUserAction(UserLogInfo logInfo, Principal userID,
-                      final String request, InputStream inputStream)
+
+    ModifyUserAction(final InputStream inputStream)
     {
-        super(logInfo);
-        this.userID = userID;
-        this.request = request;
+        super();
+
         this.inputStream = inputStream;
     }
 
-    public Object run()
-        throws Exception
-    {
-        UserPersistence userPersistence = getUserPersistence();
-        User<? extends Principal> user = UserReader.read(this.inputStream);
-        User<? extends Principal> oldUser = userPersistence.getUser(userID);
-        userPersistence.modifyUser(user);
-
-        logUserInfo(user.getUserID().getName());
 
-        this.response.sendRedirect(request);
+    public void doAction() throws Exception
+    {
+        final User<Principal> user = readUser(this.inputStream);
+        final User<Principal> modifiedUser = modifyUser(user);
+        logUserInfo(modifiedUser.getUserID().getName());
 
-        return null;
+        redirectGet(modifiedUser);
     }
 
+    /**
+     * Perform the modification at the persistence level.
+     *
+     * @param user          The user to modify.
+     * @param <T>           The ID (Principal) type.
+     * @return              The modified User.
+     * @throws Exception    Any problems during update.
+     */
+    <T extends Principal> User<T> modifyUser(final User<T> user)
+            throws Exception
+    {
+        final UserPersistence<T> userPersistence = getUserPersistence();
+        return userPersistence.modifyUser(user);
+    }
 }
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
new file mode 100644
index 0000000000000000000000000000000000000000..5d07ea7147d9581fe140c98956454362652f750e
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/PasswordServlet.java
@@ -0,0 +1,179 @@
+/*
+ ************************************************************************
+ *******************  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.server.web.users;
+
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+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.User;
+import ca.nrc.cadc.ac.server.ldap.LdapUserPersistence;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.log.ServletLogInfo;
+import ca.nrc.cadc.util.StringUtil;
+
+
+/**
+ * Servlet to handle password changes.  Passwords are an integral part of the
+ * access control system and are handled differently to accommodate stricter
+ * guidelines.
+ * <p/>
+ * This servlet handles POST only.  It relies on the Subject being set higher
+ * up by the AccessControlFilter as configured in the web descriptor.
+ */
+public class PasswordServlet extends HttpServlet
+{
+    private static final Logger log = Logger.getLogger(PasswordServlet.class);
+
+
+    /**
+     * Attempt to change password.
+     *
+     * @param request  The HTTP Request.
+     * @param response The HTTP Response.
+     * @throws IOException Any errors that are not expected.
+     */
+    public void doPost(final HttpServletRequest request,
+                       final HttpServletResponse response)
+            throws IOException
+    {
+        final long start = System.currentTimeMillis();
+        final ServletLogInfo logInfo = new ServletLogInfo(request);
+        log.info(logInfo.start());
+        try
+        {
+            final Subject subject = AuthenticationUtil.getSubject(request);
+            if ((subject == null)
+                || (subject.getPrincipals(HttpPrincipal.class).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))
+                {
+                    if (StringUtil.hasText(newPassword))
+                    {
+                        (new LdapUserPersistence<HttpPrincipal>())
+                            .setPassword(user, oldPassword, newPassword);
+                    }
+                    else
+                    {
+                        throw new IllegalArgumentException("Missing new password");
+                    }
+                }
+                else
+                {
+                    throw new IllegalArgumentException("Missing old password");
+                }
+            }
+        }
+        catch (IllegalArgumentException e)
+        {
+            log.debug(e.getMessage(), e);
+            logInfo.setMessage(e.getMessage());
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        }
+        catch (AccessControlException e)
+        {
+            log.debug(e.getMessage(), e);
+            logInfo.setMessage(e.getMessage());
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+        catch (Throwable t)
+        {
+            String message = "Internal Server Error: " + t.getMessage();
+            log.error(message, t);
+            logInfo.setSuccess(false);
+            logInfo.setMessage(message);
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        }
+        finally
+        {
+            logInfo.setElapsedTime(System.currentTimeMillis() - start);
+            log.info(logInfo.end());
+        }
+    }
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersActionFactory.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserActionFactory.java
similarity index 56%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersActionFactory.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserActionFactory.java
index 37ddf3638cae55e5556f6c10afbeecf00bea82f1..ae8ec41014d2f0ed8d67a7bc50a664c270de1542 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersActionFactory.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserActionFactory.java
@@ -70,14 +70,13 @@ package ca.nrc.cadc.ac.server.web.users;
 
 import ca.nrc.cadc.ac.IdentityType;
 import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.web.WebUtil;
 import ca.nrc.cadc.auth.CookiePrincipal;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.auth.NumericPrincipal;
 import ca.nrc.cadc.auth.OpenIdPrincipal;
-import ca.nrc.cadc.util.StringUtil;
 
 import java.io.IOException;
-import java.net.URL;
 import java.security.Principal;
 import javax.security.auth.x500.X500Principal;
 import javax.servlet.http.HttpServletRequest;
@@ -85,102 +84,150 @@ import javax.servlet.http.HttpServletRequest;
 import org.apache.log4j.Logger;
 
 
-public class UsersActionFactory
+public abstract class UserActionFactory
 {
-    private static final Logger log = Logger
-            .getLogger(UsersActionFactory.class);
+    private static final Logger log = Logger.getLogger(UserActionFactory.class);
 
-    static UsersAction getUsersAction(HttpServletRequest request, UserLogInfo logInfo)
-            throws IOException
-    {
-        UsersAction action = null;
-        String method = request.getMethod();
-        String path = request.getPathInfo();
-        log.debug("method: " + method);
-        log.debug("path: " + path);
+    public abstract AbstractUserAction createAction(HttpServletRequest request)
+        throws IllegalArgumentException, IOException;
 
-        if (path == null)
+    public static UserActionFactory httpGetFactory()
+    {
+        return new UserActionFactory()
         {
-            path = "";
-        }
+            public AbstractUserAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
+            {
+                AbstractUserAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
 
-        if (path.startsWith("/"))
-        {
-            path = path.substring(1);
-        }
+                String[] segments = WebUtil.getPathSegments(path);
 
-        if (path.endsWith("/"))
-        {
-            path = path.substring(0, path.length() - 1);
-        }
+                if (segments.length == 0)
+                {
+                    action = new GetUserListAction();
+                }
+                else if (segments.length == 1)
+                {
+                    User user = getUser(segments[0], request.getParameter("idType"), path);
+                        action = new GetUserAction(user.getUserID());
+                }
 
-        String[] segments = new String[0];
-        if (StringUtil.hasText(path))
-        {
-            segments = path.split("/");
-        }
+                if (action != null)
+                {
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
+                }
 
-        if (segments.length == 0)
-        {
-            if (method.equals("GET"))
-            {
-                action = new GetUsersAction(logInfo);
-            }
-            else if (method.equals("PUT"))
-            {
-                action = new CreateUserAction(logInfo, request
-                        .getInputStream());
+                 throw new IllegalArgumentException("Bad GET request to " + path);
             }
+        };
+    }
 
-        }
-        else
+    public static UserActionFactory httpPutFactory()
+    {
+        return new UserActionFactory()
         {
-            User user = getUser(segments[0], request.getParameter("idType"),
-                                method, path);
-            if (method.equals("GET"))
+            public AbstractUserAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
             {
-                action = new GetUserAction(logInfo, user.getUserID());
+                AbstractUserAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
+
+                String[] segments = WebUtil.getPathSegments(path);
+
+                if (segments.length == 0)
+                {
+                    action = new CreateUserAction(request.getInputStream());
+                }
+
+                if (action != null)
+                {
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
+                }
+
+                 throw new IllegalArgumentException("Bad PUT request to " + path);
             }
-            else if (method.equals("DELETE"))
+        };
+    }
+
+    public static UserActionFactory httpPostFactory()
+    {
+        return new UserActionFactory()
+        {
+            public AbstractUserAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
             {
-                action = new DeleteUserAction(logInfo, user.getUserID());
+                AbstractUserAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
+
+                String[] segments = WebUtil.getPathSegments(path);
+
+                if (segments.length == 1)
+                {
+                    action = new ModifyUserAction(request.getInputStream());
+                }
+
+                if (action != null)
+                {
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
+                }
+
+                 throw new IllegalArgumentException("Bad POST request to " + path);
             }
-            else if (method.equals("POST"))
+        };
+    }
+
+    public static UserActionFactory httpDeleteFactory()
+    {
+        return new UserActionFactory()
+        {
+            public AbstractUserAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
             {
-                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)
+                AbstractUserAction action = null;
+                String path = request.getPathInfo();
+                log.debug("path: " + path);
+
+                String[] segments = WebUtil.getPathSegments(path);
+
+                if (segments.length == 1)
+                {
+                    User user = getUser(segments[0], request.getParameter("idType"), path);
+                    action = new DeleteUserAction(user.getUserID());
+                }
+
+                if (action != null)
                 {
-                    sb.append(":");
-                    sb.append(requestURL.getPort());
+                    log.debug("Returning action: " + action.getClass());
+                    return action;
                 }
-                sb.append(request.getContextPath());
-                sb.append(request.getServletPath());
-                sb.append("/");
-                sb.append(path);
-
-                action = new ModifyUserAction(logInfo, user.getUserID(), sb
-                        .toString(),
-                                              request.getInputStream());
+
+                 throw new IllegalArgumentException("Bad DELETE request to " + path);
             }
-        }
+        };
+    }
 
-        if (action != null)
+    public static UserActionFactory httpHeadFactory()
+    {
+        return new UserActionFactory()
         {
-            log.debug("Returning action: " + action.getClass());
-            return action;
-        }
-        final String error = "Bad users request: " + method + " on " + path;
-        throw new IllegalArgumentException(error);
+            public AbstractUserAction createAction(HttpServletRequest request)
+                throws IllegalArgumentException, IOException
+            {
+                // http head not supported
+                throw new UnsupportedOperationException();
+            }
+        };
     }
 
     private static User<? extends Principal> getUser(final String userName,
                                                      final String idType,
-                                                     final String method,
                                                      final String path)
     {
         if (idType == null || idType.isEmpty())
@@ -210,9 +257,7 @@ public class UsersActionFactory
         }
         else
         {
-            final String error = "Bad users request: " + method + " on " + path +
-                                 " because of unknown principal type " + idType;
-            throw new IllegalArgumentException(error);
+            throw new IllegalArgumentException("Unregonized userid");
         }
     }
 
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserServlet.java
similarity index 73%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersServlet.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserServlet.java
index c3f2e53c75821f6de8d170d5d1a02393196ca316..69a5c20a786b9cf364713c9555fd13a2b195cb60 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UserServlet.java
@@ -69,27 +69,49 @@
 package ca.nrc.cadc.ac.server.web.users;
 
 import java.io.IOException;
+import java.security.PrivilegedActionException;
 
 import javax.security.auth.Subject;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import ca.nrc.cadc.ac.UserAlreadyExistsException;
 import ca.nrc.cadc.util.StringUtil;
+
 import org.apache.log4j.Logger;
 
+import ca.nrc.cadc.ac.server.web.SyncOutput;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 
-public class UsersServlet extends HttpServlet
+public class UserServlet extends HttpServlet
 {
-    private static final Logger log = Logger.getLogger(UsersServlet.class);
 
+    private static final long serialVersionUID = 5289130885807305288L;
+    private static final Logger log = Logger.getLogger(UserServlet.class);
+    private String augmentUserDN;
+    
+    @Override
+    public void init(final ServletConfig config) throws ServletException
+    {
+        super.init(config);
+
+        try
+        {
+        	this.augmentUserDN = config.getInitParameter(UserServlet.class.getName() + ".augmentUserDN");
+            log.info("augmentUserDN: " + augmentUserDN);
+        }
+        catch(Exception ex)
+        {
+            log.error("failed to init: " + ex);
+        }
+    }
 
     /**
      * Create a UserAction and run the action safely.
      */
-    private void doAction(HttpServletRequest request, HttpServletResponse response)
+    private void doAction(UserActionFactory factory, HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
         long start = System.currentTimeMillis();
@@ -100,9 +122,40 @@ public class UsersServlet extends HttpServlet
             log.info(logInfo.start());
             Subject subject = AuthenticationUtil.getSubject(request);
             logInfo.setSubject(subject);
-            UsersAction action = UsersActionFactory.getUsersAction(request, logInfo);
+
+            AbstractUserAction action = factory.createAction(request);
+            SyncOutput syncOut = new SyncOutput(response);
+
+            action.setAugmentUserDN(this.augmentUserDN);
+            action.setLogInfo(logInfo);
+            action.setSyncOut(syncOut);
             action.setAcceptedContentType(getAcceptedContentType(request));
-            action.doAction(subject, response);
+
+            try
+            {
+                if (subject == null)
+                {
+                    action.run();
+                }
+                else
+                {
+                    Subject.doAs(subject, action);
+                }
+            }
+            catch (PrivilegedActionException e)
+            {
+                Throwable cause = e.getCause();
+                if (cause != null)
+                {
+                    throw cause;
+                }
+                Exception exception = e.getException();
+                if (exception != null)
+                {
+                    throw exception;
+                }
+                throw e;
+            }
         }
         catch (IllegalArgumentException e)
         {
@@ -132,35 +185,35 @@ public class UsersServlet extends HttpServlet
     public void doGet(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(UserActionFactory.httpGetFactory(), request, response);
     }
 
     @Override
     public void doPost(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(UserActionFactory.httpGetFactory(), request, response);
     }
 
     @Override
     public void doDelete(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(UserActionFactory.httpDeleteFactory(), request, response);
     }
 
     @Override
     public void doPut(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(UserActionFactory.httpPutFactory(), request, response);
     }
 
     @Override
     public void doHead(HttpServletRequest request, HttpServletResponse response)
         throws IOException
     {
-        doAction(request, response);
+        doAction(UserActionFactory.httpHeadFactory(), request, response);
     }
 
     /**
@@ -174,13 +227,13 @@ public class UsersServlet extends HttpServlet
         final String requestedContentType = request.getHeader("Accept");
 
         if (StringUtil.hasText(requestedContentType)
-            && requestedContentType.contains(UsersAction.JSON_CONTENT_TYPE))
+            && requestedContentType.contains(AbstractUserAction.JSON_CONTENT_TYPE))
         {
-            return UsersAction.JSON_CONTENT_TYPE;
+            return AbstractUserAction.JSON_CONTENT_TYPE;
         }
         else
         {
-            return UsersAction.DEFAULT_CONTENT_TYPE;
+            return AbstractUserAction.DEFAULT_CONTENT_TYPE;
         }
     }
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTestImpl.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTestImpl.java
index 7d3be479f44fc01e2eaaab7805e645a1dc9f573a..2dd0201455e5fee826af689dc471595545e0669c 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTestImpl.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTestImpl.java
@@ -70,13 +70,14 @@ package ca.nrc.cadc.ac.server.ldap;
 
 import java.security.AccessControlException;
 
+import ca.nrc.cadc.net.TransientException;
 import com.unboundid.ldap.sdk.LDAPConnection;
 import com.unboundid.ldap.sdk.LDAPException;
 
 
 public class LdapDAOTestImpl extends LdapDAO
 {
-    public LdapDAOTestImpl(LdapConfig config)
+    public LdapDAOTestImpl(LdapConfig config) throws TransientException
     {
         super(config);
     }
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 2f9bcdf27a8fb962fd5d1645f997975200956706..b5f7b7fc706285aec7404148022f0a4ffae7f2a1 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
@@ -150,7 +150,7 @@ public class LdapGroupDAOTest extends AbstractLdapDAOTest
         anonSubject.getPrincipals().add(unknownUser.getUserID());
     }
 
-    LdapGroupDAO<X500Principal> getGroupDAO()
+    LdapGroupDAO<X500Principal> getGroupDAO() throws Exception
     {
         return new LdapGroupDAO<X500Principal>(config,
                 new LdapUserDAO<X500Principal>(config));
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
index 78a63a13285e89e87d18806b24dfcbae0c4953f8..cb143e90e8fedc9c87677763f09ac55b3c23b67f 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
@@ -93,11 +93,12 @@ import ca.nrc.cadc.ac.UserDetails;
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.auth.NumericPrincipal;
+import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.Log4jInit;
 
 import com.unboundid.ldap.sdk.DN;
 
-public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
+public class LdapUserDAOTest extends AbstractLdapDAOTest
 {
     private static final Logger log = Logger.getLogger(LdapUserDAOTest.class);
 
@@ -107,9 +108,11 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
     static String testUserDN;
     static User<X500Principal> testUser;
     static User<X500Principal> testMember;
+    static User<HttpPrincipal> testPendingUser;
     static LdapConfig config;
     static Random ran = new Random(); // source of randomness for numeric ids
 
+
     @BeforeClass
     public static void setUpBeforeClass()
             throws Exception
@@ -120,6 +123,17 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
         config = getLdapConfig();
         X500Principal testUserX500Princ = new X500Principal(testUserX509DN);
         testUser = new User<X500Principal>(testUserX500Princ);
+
+        testPendingUser =
+                new User<HttpPrincipal>(new HttpPrincipal("CADCtestRequest"));
+        testPendingUser.details.add(new PersonalDetails("CADCtest", "Request"));
+        testPendingUser.getIdentities().add(
+                new HttpPrincipal("CADCtestRequest"));
+        testPendingUser.getIdentities().add(
+                new X500Principal(
+                        "uid=CADCtestRequest,ou=userrequests,ou=ds,dc=testcanfar"));
+        testPendingUser.getIdentities().add(new NumericPrincipal(66666));
+
         testUser.details.add(new PersonalDetails("CADC", "DAOTest1"));
         testUser.getIdentities().add(new HttpPrincipal("CadcDaoTest1"));
         testUser.getIdentities().add(testUserX500Princ);
@@ -136,7 +150,7 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
         
     }
 
-    LdapUserDAO getUserDAO()
+    <T extends Principal> LdapUserDAO<T> getUserDAO() throws Exception
     {
         return new LdapUserDAO(config){
             protected int genNextNumericId()
@@ -146,7 +160,7 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
         };
     }
 
-    String getUserID()
+    String createUserID()
     {
         return "CadcDaoTestUser-" + System.currentTimeMillis();
     }
@@ -157,7 +171,7 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
     @Test
     public void testAddUser() throws Exception
     {
-        String userID = getUserID();
+        String userID = createUserID();
         final User<HttpPrincipal> expected = new User<HttpPrincipal>(new HttpPrincipal(userID));
         expected.getIdentities().add(new HttpPrincipal(userID));
         expected.getIdentities().add(new X500Principal("cn=" + userID + ",ou=cadc,o=hia,c=ca"));
@@ -167,15 +181,44 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
         expected.details.add(new PersonalDetails("foo", "bar"));
 
         final UserRequest<HttpPrincipal> userRequest =
-                new UserRequest<HttpPrincipal>(expected, "123456");
+                new UserRequest<HttpPrincipal>(expected, "123456".toCharArray());
 
         Subject subject = new Subject();
         subject.getPrincipals().add(testUser.getUserID());
 
-        User<HttpPrincipal> actual = getUserDAO().addUser(userRequest);
+        final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+        User<HttpPrincipal> actual = userDAO.addUser(userRequest);
         check(expected, actual);
     }
 
+    @Test
+    public void testGetPendingUser() throws Exception
+    {
+        final Subject subject = new Subject();
+        subject.getPrincipals().add(testUser.getUserID());
+
+        // do everything as owner
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+                    final User<HttpPrincipal> actual =
+                            userDAO.getPendingUser(testPendingUser.getUserID());
+                    check(testPendingUser, actual);
+
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+    }
+
     /**
      * Test of getUser method, of class LdapUserDAO.
      */
@@ -188,12 +231,13 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
         // do everything as owner
         Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
         {
-            public Object run()
-                throws Exception
+            public Object run() throws Exception
             {
                 try
                 {
-                    User<X500Principal> actual = getUserDAO().getUser(testUser.getUserID());
+                    final LdapUserDAO<X500Principal> userDAO = getUserDAO();
+                    final User<X500Principal> actual =
+                            userDAO.getUser(testUser.getUserID());
                     check(testUser, actual);
 
                     return null;
@@ -204,7 +248,6 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
                 }
             }
         });
-
     }
 
     /**
@@ -301,7 +344,7 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    User<X500Principal> actual = getUserDAO().getMember(new DN(testUserDN));
+                    User<X500Principal> actual = getUserDAO().getX500User(new DN(testUserDN));
                     check(testMember, actual);
                     return null;
                 }
@@ -324,7 +367,7 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    User<X500Principal> actual = getUserDAO().getMember(new DN(testUserDN));
+                    User<X500Principal> actual = getUserDAO().getX500User(new DN(testUserDN));
                     check(testMember, actual);
                     return null;
                 }
@@ -404,15 +447,16 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
 
         // Create a test user with a known password
         final User<HttpPrincipal> testUser2;
-        final String username = getUserID();
-        final String password = "foo";
-        final String newPassword = "bar";
+        final String username = createUserID();
+        final char[] password = "foo".toCharArray();
+        final char[] newPassword = "bar".toCharArray();
 
         HttpPrincipal principal = new HttpPrincipal(username);
         testUser2 = new User<HttpPrincipal>(principal);
         testUser2.getIdentities().add(principal);
         testUser2.details.add(new PersonalDetails("firstName", "lastName"));
-        final UserRequest userRequest = new UserRequest(testUser2, password);
+        final UserRequest<HttpPrincipal> userRequest =
+                new UserRequest<HttpPrincipal>(testUser2, password);
 
         // add the user
         Subject subject = new Subject();
@@ -424,7 +468,8 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    return getUserDAO().newUser(userRequest);
+                    final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+                    return userDAO.newUser(userRequest);
                 }
                 catch (Exception e)
                 {
@@ -442,7 +487,7 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    getUserDAO().loginUser(username, password);
+                    getUserDAO().doLogin(username, String.valueOf(password));
                 }
                 catch (Exception e)
                 {
@@ -460,7 +505,9 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    getUserDAO().setPassword(testUser2, password, newPassword);
+                    final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+                    userDAO.setPassword(testUser2, String.valueOf(password),
+                                        String.valueOf(newPassword));
                     fail("should throw exception if subject and user are not the same");
                 }
                 catch (Exception ignore){}
@@ -477,7 +524,9 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    getUserDAO().setPassword(testUser2, password, newPassword);
+                    final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+                    userDAO.setPassword(testUser2, String.valueOf(password),
+                                        String.valueOf(newPassword));
                 }
                 catch (Exception e)
                 {
@@ -496,7 +545,7 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    getUserDAO().loginUser(username, password);
+                    getUserDAO().doLogin(username, String.valueOf(password));
                 }
                 catch (Exception e)
                 {
@@ -513,8 +562,8 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
     {
         // Create a test user
         final User<HttpPrincipal> testUser2;
-        final String username = getUserID();
-        final String password = "foo";
+        final String username = createUserID();
+        final char[] password = "foo".toCharArray();
 
         HttpPrincipal principal = new HttpPrincipal(username);
         testUser2 = new User<HttpPrincipal>(principal);
@@ -524,7 +573,8 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
         nextUserNumericID = ran.nextInt(Integer.MAX_VALUE);
         testUser2.getIdentities().add(new NumericPrincipal(nextUserNumericID));
         testUser2.details.add(new PersonalDetails("firstName", "lastName"));
-        final UserRequest userRequest = new UserRequest(testUser2, password);
+        final UserRequest<HttpPrincipal> userRequest =
+                new UserRequest<HttpPrincipal>(testUser2, password);
 
         // add the user
         Subject subject = new Subject();
@@ -536,7 +586,8 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    return getUserDAO().newUser(userRequest);
+                    final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+                    return userDAO.newUser(userRequest);
                 }
                 catch (Exception e)
                 {
@@ -569,7 +620,8 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    getUserDAO().modifyUser(testUser2);
+                    final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+                    userDAO.modifyUser(testUser2);
                     fail("should throw exception if subject and user are not the same");
                 }
                 catch (Exception ignore)
@@ -589,7 +641,8 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
             {
                 try
                 {
-                    return getUserDAO().modifyUser(testUser2);
+                    final LdapUserDAO<HttpPrincipal> userDAO = getUserDAO();
+                    return userDAO.modifyUser(testUser2);
                 }
                 catch (Exception e)
                 {
@@ -603,7 +656,8 @@ public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
         check(testUser2, updatedUser);
     }
 
-    private static void check(final User<? extends Principal> user1, final User<? extends Principal> user2)
+    private static void check(final User<? extends Principal> user1,
+                              final User<? extends Principal> user2)
     {
         assertEquals(user1, user2);
         assertEquals(user1.details, user2.details);
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupActionTest.java
similarity index 89%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupActionTest.java
index def8852585eaf9d5102a31ede3712bb7214504f0..502e314d2992ee758465b10252986e13cc486220 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupActionTest.java
@@ -91,10 +91,10 @@ import static org.junit.Assert.*;
  *
  * @author jburke
  */
-public class GroupsActionTest
+public class AbstractGroupActionTest
 {
-    private final static Logger log = Logger.getLogger(GroupsActionTest.class);
-    
+    private final static Logger log = Logger.getLogger(AbstractGroupActionTest.class);
+
     @BeforeClass
     public static void setUpClass()
     {
@@ -109,7 +109,7 @@ public class GroupsActionTest
         Exception e = new AccessControlException("");
         testDoAction(message, responseCode, e);
     }
-    
+
     @Test
     public void testDoActionIllegalArgumentException() throws Exception
     {
@@ -118,7 +118,7 @@ public class GroupsActionTest
         Exception e = new IllegalArgumentException("message");
         testDoAction(message, responseCode, e);
     }
-    
+
     @Test
     public void testDoActionMemberNotFoundException() throws Exception
     {
@@ -127,7 +127,7 @@ public class GroupsActionTest
         Exception e = new MemberNotFoundException("foo");
         testDoAction(message, responseCode, e);
     }
-    
+
     @Test
     public void testDoActionGroupNotFoundException() throws Exception
     {
@@ -136,7 +136,7 @@ public class GroupsActionTest
         Exception e = new GroupNotFoundException("foo");
         testDoAction(message, responseCode, e);
     }
-    
+
     @Test
     public void testDoActionUserNotFoundException() throws Exception
     {
@@ -154,7 +154,7 @@ public class GroupsActionTest
         Exception e = new MemberAlreadyExistsException("foo");
         testDoAction(message, responseCode, e);
     }
-    
+
     @Test
     public void testDoActionGroupAlreadyExistsException() throws Exception
     {
@@ -163,7 +163,7 @@ public class GroupsActionTest
         Exception e = new GroupAlreadyExistsException("foo");
         testDoAction(message, responseCode, e);
     }
-    
+
     @Test
     public void testDoActionUnsupportedOperationException() throws Exception
     {
@@ -172,7 +172,7 @@ public class GroupsActionTest
         Exception e = new UnsupportedOperationException();
         testDoAction(message, responseCode, e);
     }
-    
+
     @Test
     public void testDoActionTransientException() throws Exception
     {
@@ -188,16 +188,9 @@ public class GroupsActionTest
             EasyMock.expectLastCall().once();
             EasyMock.replay(response);
 
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            logInfo.setSuccess(false);
-            EasyMock.expectLastCall().once();
-            logInfo.setMessage("Internal Transient Error: foo");
-            EasyMock.expectLastCall().once();
-            EasyMock.replay(logInfo);
-
-            GroupsActionImpl action = new GroupsActionImpl(logInfo);
+            GroupsActionImpl action = new GroupsActionImpl();
             action.setException(new TransientException("foo"));
-            action.doAction(null, response);
+            action.doAction();
         }
         catch (Throwable t)
         {
@@ -205,7 +198,7 @@ public class GroupsActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
     private void testDoAction(String message, int responseCode, Exception e)
         throws Exception
     {
@@ -221,14 +214,9 @@ public class GroupsActionTest
             EasyMock.expectLastCall().once();
             EasyMock.replay(response);
 
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            logInfo.setMessage(message);
-            EasyMock.expectLastCall().once();
-            EasyMock.replay(logInfo);
-
-            GroupsActionImpl action = new GroupsActionImpl(logInfo);
+            GroupsActionImpl action = new GroupsActionImpl();
             action.setException(e);
-            action.doAction(null, response);
+            action.doAction();
         }
         catch (Throwable t)
         {
@@ -237,16 +225,16 @@ public class GroupsActionTest
         }
     }
 
-    public class GroupsActionImpl extends GroupsAction
+    public class GroupsActionImpl extends AbstractGroupAction
     {
         Exception exception;
-        
-        public GroupsActionImpl(GroupLogInfo logInfo)
+
+        public GroupsActionImpl()
         {
-            super(logInfo);
+            super();
         }
 
-        public Object run() throws Exception
+        public void doAction() throws Exception
         {
             throw exception;
         }
@@ -256,5 +244,5 @@ public class GroupsActionTest
             this.exception = e;
         }
     }
-    
+
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberActionTest.java
index 8550e9ba2dfa13797a5da1a34c5a93fcd8261c10..f8075c5abd57eceab1fe478be97bc2060086e3eb 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberActionTest.java
@@ -88,7 +88,7 @@ import static org.junit.Assert.*;
 public class AddGroupMemberActionTest
 {
     private final static Logger log = Logger.getLogger(AddGroupMemberActionTest.class);
-    
+
     @BeforeClass
     public static void setUpClass()
     {
@@ -103,15 +103,13 @@ public class AddGroupMemberActionTest
             Group group = new Group("group", null);
             Group member = new Group("member", null);
             group.getGroupMembers().add(member);
-            
+
             final GroupPersistence groupPersistence = createMock(GroupPersistence.class);
             expect(groupPersistence.getGroup("group")).andReturn(group);
             expect(groupPersistence.getGroup("member")).andReturn(member);
             replay(groupPersistence);
-            
-            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
-            
-            AddGroupMemberAction action = new AddGroupMemberAction(logInfo, "group", "member")
+
+            AddGroupMemberAction action = new AddGroupMemberAction("group", "member")
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
@@ -119,10 +117,10 @@ public class AddGroupMemberActionTest
                     return groupPersistence;
                 };
             };
-            
+
             try
             {
-                action.run();
+                action.doAction();
                 fail("duplicate group member should throw GroupAlreadyExistsException");
             }
             catch (GroupAlreadyExistsException ignore) {}
@@ -133,7 +131,7 @@ public class AddGroupMemberActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
     @Test
     public void testRun() throws Exception
     {
@@ -152,10 +150,8 @@ public class AddGroupMemberActionTest
             expect(groupPersistence.modifyGroup(group)).andReturn(modified);
 
             replay(groupPersistence);
-            
-            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
-            
-            AddGroupMemberAction action = new AddGroupMemberAction(logInfo, "group", "member")
+
+            AddGroupMemberAction action = new AddGroupMemberAction("group", "member")
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
@@ -164,7 +160,11 @@ public class AddGroupMemberActionTest
                 };
             };
 
-            action.run();
+            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
+            action.setLogInfo(logInfo);
+
+            action.doAction();
+
         }
         catch (Throwable t)
         {
@@ -172,5 +172,5 @@ public class AddGroupMemberActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberActionTest.java
index 1cdb6f4fc1cf397c8388bbb7322fd39cbb845e61..8eae8ace9c01d0fce6d452861e0f61030008e954 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberActionTest.java
@@ -82,6 +82,8 @@ import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
 import org.junit.BeforeClass;
 import org.junit.Test;
+
+import static org.easymock.EasyMock.createMock;
 import static org.junit.Assert.*;
 
 /**
@@ -91,7 +93,7 @@ import static org.junit.Assert.*;
 public class AddUserMemberActionTest
 {
     private final static Logger log = Logger.getLogger(AddUserMemberActionTest.class);
-    
+
     @BeforeClass
     public static void setUpClass()
     {
@@ -103,43 +105,42 @@ public class AddUserMemberActionTest
     public void testExceptions()
     {
         try
-        {   
+        {
             String userID = "foo";
             String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
             Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
             User<Principal> user = new User<Principal>(userPrincipal);
-            
+
             Group group = new Group("group", null);
             group.getUserMembers().add(user);
-            
+
             final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
             EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
             EasyMock.replay(groupPersistence);
-            
+
             final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
             EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
             EasyMock.replay(userPersistence);
-            
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            
-            AddUserMemberAction action = new AddUserMemberAction(logInfo, "group", userID, userIDType)
+
+
+            AddUserMemberAction action = new AddUserMemberAction("group", userID, userIDType)
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
                 {
                     return groupPersistence;
                 };
-                
+
                 @Override
                 <T extends Principal> UserPersistence<T> getUserPersistence()
                 {
                     return userPersistence;
                 };
             };
-            
+
             try
             {
-                action.run();
+                action.doAction();
                 fail("duplicate group member should throw MemberAlreadyExistsException");
             }
             catch (MemberAlreadyExistsException ignore) {}
@@ -150,7 +151,7 @@ public class AddUserMemberActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
     @Test
     @SuppressWarnings("unchecked")
     public void testRun() throws Exception
@@ -161,38 +162,38 @@ public class AddUserMemberActionTest
             String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
             Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
             User<Principal> user = new User<Principal>(userPrincipal);
-            
+
             Group group = new Group("group", null);
             Group modified = new Group("group", null);
             modified.getUserMembers().add(user);
-            
+
             final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
             EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
             EasyMock.expect(groupPersistence.modifyGroup(group)).andReturn(modified);
             EasyMock.replay(groupPersistence);
-            
+
             final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
             EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
             EasyMock.replay(userPersistence);
-            
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            
-            AddUserMemberAction action = new AddUserMemberAction(logInfo, "group", userID, userIDType)
+
+            AddUserMemberAction action = new AddUserMemberAction("group", userID, userIDType)
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
                 {
                     return groupPersistence;
                 };
-                
+
                 @Override
                 <T extends Principal> UserPersistence<T> getUserPersistence()
                 {
                     return userPersistence;
                 };
             };
-            
-            action.run();
+
+            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
+            action.setLogInfo(logInfo);
+            action.doAction();
         }
         catch (Throwable t)
         {
@@ -200,5 +201,5 @@ public class AddUserMemberActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupActionTest.java
index 0994610208aab34a25f8b521508c6146283df352..ca58a75c3d9760419cb50be1f7c6e5feea82f4fc 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupActionTest.java
@@ -77,6 +77,8 @@ import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
 import org.junit.BeforeClass;
 import org.junit.Test;
+
+import static org.easymock.EasyMock.createMock;
 import static org.junit.Assert.*;
 
 /**
@@ -86,7 +88,7 @@ import static org.junit.Assert.*;
 public class DeleteGroupActionTest
 {
    private final static Logger log = Logger.getLogger(DeleteGroupActionTest.class);
-    
+
     @BeforeClass
     public static void setUpClass()
     {
@@ -97,18 +99,16 @@ public class DeleteGroupActionTest
     public void testRun()
     {
         try
-        {   
+        {
             Group group = new Group("group", null);
-            
+
             final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
             EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
             groupPersistence.deleteGroup("group");
             EasyMock.expectLastCall().once();
             EasyMock.replay(groupPersistence);
-            
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            
-            DeleteGroupAction action = new DeleteGroupAction(logInfo, "group")
+
+            DeleteGroupAction action = new DeleteGroupAction("group")
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
@@ -117,7 +117,9 @@ public class DeleteGroupActionTest
                 };
             };
 
-            action.run();
+            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
+            action.setLogInfo(logInfo);
+            action.doAction();
         }
         catch (Throwable t)
         {
@@ -125,5 +127,5 @@ public class DeleteGroupActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesActionTest.java
index 1f2d4d67b0f71e4a2734230b8308f04cd27d5353..0ade66ad54e553d496d9df4366ec2698a7130bee 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesActionTest.java
@@ -85,6 +85,7 @@ import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Collection;
 
+import static org.easymock.EasyMock.createMock;
 import static org.junit.Assert.fail;
 
 /**
@@ -134,11 +135,9 @@ public class GetGroupNamesActionTest
             EasyMock.expectLastCall();
             EasyMock.expect(mockResponse.getWriter()).andReturn(mockWriter).once();
 
-            GroupLogInfo mockLog = EasyMock.createMock(GroupLogInfo.class);
+            EasyMock.replay(mockPersistence, mockWriter, mockResponse);
 
-            EasyMock.replay(mockPersistence, mockWriter, mockResponse, mockLog);
-
-            GetGroupNamesAction action = new GetGroupNamesAction(mockLog)
+            GetGroupNamesAction action = new GetGroupNamesAction()
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
@@ -147,7 +146,9 @@ public class GetGroupNamesActionTest
                 };
             };
 
-            action.run();
+            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
+            action.setLogInfo(logInfo);
+            action.doAction();
         }
         catch (Throwable t)
         {
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupActionFactoryTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupActionFactoryTest.java
index 3a9b19ccb053c960256ea8c67cd32fe39dac6544..0aad33de8b38c44848aba054f3561874cea02a80 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupActionFactoryTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupActionFactoryTest.java
@@ -93,9 +93,8 @@ public class GroupActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("groupName/groupMembers/groupToAdd");
-            EasyMock.expect(request.getMethod()).andReturn("PUT");
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            AbstractGroupAction action = GroupsActionFactory.httpPutFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof AddGroupMemberAction);
         }
@@ -114,9 +113,8 @@ public class GroupActionFactoryTest
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("groupName/userMembers/userToAdd");
             EasyMock.expect(request.getParameter("idType")).andReturn("x509");
-            EasyMock.expect(request.getMethod()).andReturn("PUT");
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            AbstractGroupAction action = GroupsActionFactory.httpPutFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof AddUserMemberAction);
         }
@@ -134,10 +132,10 @@ public class GroupActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("");
-            EasyMock.expect(request.getMethod()).andReturn("PUT");
             EasyMock.expect(request.getInputStream()).andReturn(null);
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+
+            AbstractGroupAction action = GroupsActionFactory.httpPutFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof CreateGroupAction);
         }
@@ -155,9 +153,9 @@ public class GroupActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("groupName");
-            EasyMock.expect(request.getMethod()).andReturn("DELETE");
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+
+            AbstractGroupAction action = GroupsActionFactory.httpDeleteFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof DeleteGroupAction);
         }
@@ -175,9 +173,8 @@ public class GroupActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("groupName");
-            EasyMock.expect(request.getMethod()).andReturn("GET");
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            AbstractGroupAction action = GroupsActionFactory.httpGetFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof GetGroupAction);
         }
@@ -195,9 +192,8 @@ public class GroupActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("");
-            EasyMock.expect(request.getMethod()).andReturn("GET");
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            AbstractGroupAction action = GroupsActionFactory.httpGetFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof GetGroupNamesAction);
         }
@@ -218,13 +214,12 @@ public class GroupActionFactoryTest
 
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("groupName");
-            EasyMock.expect(request.getMethod()).andReturn("POST");
             EasyMock.expect(request.getRequestURL()).andReturn(sb);
             EasyMock.expect(request.getContextPath()).andReturn("");
             EasyMock.expect(request.getServletPath()).andReturn("");
             EasyMock.expect(request.getInputStream()).andReturn(null);
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            AbstractGroupAction action = GroupsActionFactory.httpPostFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof ModifyGroupAction);
         }
@@ -242,9 +237,8 @@ public class GroupActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("groupName/groupMembers/groupToRenove");
-            EasyMock.expect(request.getMethod()).andReturn("DELETE");
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            AbstractGroupAction action = GroupsActionFactory.httpDeleteFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof RemoveGroupMemberAction);
         }
@@ -262,10 +256,9 @@ public class GroupActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("groupName/userMembers/userToRemove");
-            EasyMock.expect(request.getMethod()).andReturn("DELETE");
             EasyMock.expect(request.getParameter("idType")).andReturn("x509");
             EasyMock.replay(request);
-            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            AbstractGroupAction action = GroupsActionFactory.httpDeleteFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof RemoveUserMemberAction);
         }
@@ -276,10 +269,7 @@ public class GroupActionFactoryTest
         }
     }
 
-    @Test
-    public void testBadRequests()
-    {
-        try
+    /*
         {
             TestRequest[] testRequests =
             {
@@ -304,6 +294,151 @@ public class GroupActionFactoryTest
                 new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments", "HEAD"),
                 new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments", "DELETE"),
             };
+            */
+
+    @Test
+    public void testBadGetRequests()
+    {
+        try
+        {
+            TestRequest[] testRequests =
+            {
+                new TestRequest("groupName/groupMembers"),
+                new TestRequest("groupName/misspelled/id"),
+                new TestRequest("groupName/groupMembers/groupMemberName"),
+                new TestRequest("groupName/userMembers/userMemberName"),
+                new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments")
+            };
+
+            for (TestRequest testRequest : testRequests)
+            {
+
+                log.debug("Testing: " + testRequest);
+
+                HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+                EasyMock.expect(request.getPathInfo()).andReturn(testRequest.path);
+                if (testRequest.paramName != null)
+                {
+                    EasyMock.expect(request.getParameter(testRequest.paramName)).andReturn(testRequest.paramValue);
+                }
+                EasyMock.replay(request);
+                try
+                {
+                    GroupsActionFactory.httpGetFactory().createAction(request);
+                    Assert.fail("Should have been a bad request: on " + testRequest.path);
+                }
+                catch (IllegalArgumentException e)
+                {
+                    // expected
+                }
+                EasyMock.verify(request);
+            }
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testBadPutRequests()
+    {
+        try
+        {
+            TestRequest[] testRequests =
+            {
+                    new TestRequest("groupName/groupMembers"),
+                    new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments")
+            };
+
+            for (TestRequest testRequest : testRequests)
+            {
+
+                log.debug("Testing: " + testRequest);
+
+                HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+                EasyMock.expect(request.getPathInfo()).andReturn(testRequest.path);
+                if (testRequest.paramName != null)
+                {
+                    EasyMock.expect(request.getParameter(testRequest.paramName)).andReturn(testRequest.paramValue);
+                }
+                EasyMock.replay(request);
+                try
+                {
+                    GroupsActionFactory.httpPutFactory().createAction(request);
+                    Assert.fail("Should have been a bad request: on " + testRequest.path);
+                }
+                catch (IllegalArgumentException e)
+                {
+                    // expected
+                }
+                EasyMock.verify(request);
+            }
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testBadPostRequests()
+    {
+        try
+        {
+            TestRequest[] testRequests =
+            {
+                    new TestRequest(""),
+                    new TestRequest("groupName/groupMembers"),
+                    new TestRequest("groupName/groupMembers/groupMemberName"),
+                    new TestRequest("groupName/userMembers/userMemberName"),
+                    new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments")
+            };
+
+            for (TestRequest testRequest : testRequests)
+            {
+
+                log.debug("Testing: " + testRequest);
+
+                HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+                EasyMock.expect(request.getPathInfo()).andReturn(testRequest.path);
+                if (testRequest.paramName != null)
+                {
+                    EasyMock.expect(request.getParameter(testRequest.paramName)).andReturn(testRequest.paramValue);
+                }
+                EasyMock.replay(request);
+                try
+                {
+                    GroupsActionFactory.httpPostFactory().createAction(request);
+                    Assert.fail("Should have been a bad request: on " + testRequest.path);
+                }
+                catch (IllegalArgumentException e)
+                {
+                    // expected
+                }
+                EasyMock.verify(request);
+            }
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testBadDeleteRequests()
+    {
+        try
+        {
+            TestRequest[] testRequests =
+            {
+                    new TestRequest(""),
+                    new TestRequest("groupName/groupMembers"),
+                    new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments"),
+            };
 
             for (TestRequest testRequest : testRequests)
             {
@@ -312,7 +447,6 @@ public class GroupActionFactoryTest
 
                 HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
                 EasyMock.expect(request.getPathInfo()).andReturn(testRequest.path);
-                EasyMock.expect(request.getMethod()).andReturn(testRequest.method);
                 if (testRequest.paramName != null)
                 {
                     EasyMock.expect(request.getParameter(testRequest.paramName)).andReturn(testRequest.paramValue);
@@ -320,8 +454,8 @@ public class GroupActionFactoryTest
                 EasyMock.replay(request);
                 try
                 {
-                    GroupsActionFactory.getGroupsAction(request, null);
-                    Assert.fail("Should have been a bad request: " + testRequest.method + " on " + testRequest.path);
+                    GroupsActionFactory.httpDeleteFactory().createAction(request);
+                    Assert.fail("Should have been a bad request: on " + testRequest.path);
                 }
                 catch (IllegalArgumentException e)
                 {
@@ -340,25 +474,23 @@ public class GroupActionFactoryTest
     private class TestRequest
     {
         public String path;
-        public String method;
         public String paramName;
         public String paramValue;
 
-        public TestRequest(String path, String method)
+        public TestRequest(String path)
         {
-            this(path, method, null, null);
+            this(path, null, null);
         }
-        public TestRequest(String path, String method, String paramName, String paramValue)
+        public TestRequest(String path, String paramName, String paramValue)
         {
             this.path = path;
-            this.method = method;
             this.paramName = paramName;
             this.paramValue = paramValue;
         }
         @Override
         public String toString()
         {
-            return method + " on path " + path +
+            return "paht " + path +
                 ((paramName == null) ? "" : "?" + paramName + "=" + paramValue);
         }
 
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberActionTest.java
index 749d0076eed5527337e293ac48f4b11951d54a55..acb8224e91380a5c03e9d024eae0cd3dfa682298 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberActionTest.java
@@ -78,6 +78,8 @@ import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
 import org.junit.BeforeClass;
 import org.junit.Test;
+
+import static org.easymock.EasyMock.createMock;
 import static org.junit.Assert.*;
 
 /**
@@ -87,7 +89,7 @@ import static org.junit.Assert.*;
 public class RemoveGroupMemberActionTest
 {
     private final static Logger log = Logger.getLogger(RemoveGroupMemberActionTest.class);
-    
+
     @BeforeClass
     public static void setUpClass()
     {
@@ -101,15 +103,13 @@ public class RemoveGroupMemberActionTest
         {
             Group group = new Group("group", null);
             Group member = new Group("member", null);
-            
+
             final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
             EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
             EasyMock.expect(groupPersistence.getGroup("member")).andReturn(member);
             EasyMock.replay(groupPersistence);
-            
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            
-            RemoveGroupMemberAction action = new RemoveGroupMemberAction(logInfo, "group", "member")
+
+            RemoveGroupMemberAction action = new RemoveGroupMemberAction( "group", "member")
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
@@ -117,10 +117,10 @@ public class RemoveGroupMemberActionTest
                     return groupPersistence;
                 };
             };
-            
+
             try
             {
-                action.run();
+                action.doAction();
                 fail("unknown group member should throw GroupNotFoundException");
             }
             catch (GroupNotFoundException ignore) {}
@@ -131,7 +131,7 @@ public class RemoveGroupMemberActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
     @Test
     public void testRun() throws Exception
     {
@@ -140,19 +140,17 @@ public class RemoveGroupMemberActionTest
             Group member = new Group("member", null);
             Group group = new Group("group", null);
             group.getGroupMembers().add(member);
-            
+
             Group modified = new Group("group", null);
             modified.getGroupMembers().add(member);
-            
+
             final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
             EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
             EasyMock.expect(groupPersistence.getGroup("member")).andReturn(member);
             EasyMock.expect(groupPersistence.modifyGroup(group)).andReturn(modified);
             EasyMock.replay(groupPersistence);
-            
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            
-            RemoveGroupMemberAction action = new RemoveGroupMemberAction(logInfo, "group", "member")
+
+            RemoveGroupMemberAction action = new RemoveGroupMemberAction("group", "member")
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
@@ -161,7 +159,9 @@ public class RemoveGroupMemberActionTest
                 };
             };
 
-            action.run();
+            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
+            action.setLogInfo(logInfo);
+            action.doAction();
         }
         catch (Throwable t)
         {
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberActionTest.java
index 05609091e46dba3e1b60d8c307622578d75b62b3..92f5263dabbed86331eb015a9e0098bc02a146b0 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberActionTest.java
@@ -81,6 +81,8 @@ import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
 import org.junit.BeforeClass;
 import org.junit.Test;
+
+import static org.easymock.EasyMock.createMock;
 import static org.junit.Assert.*;
 
 /**
@@ -90,7 +92,7 @@ import static org.junit.Assert.*;
 public class RemoveUserMemberActionTest
 {
    private final static Logger log = Logger.getLogger(RemoveUserMemberActionTest.class);
-    
+
     @BeforeClass
     public static void setUpClass()
     {
@@ -102,42 +104,40 @@ public class RemoveUserMemberActionTest
     public void testExceptions()
     {
         try
-        {   
+        {
             String userID = "foo";
             String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
             Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
             User<Principal> user = new User<Principal>(userPrincipal);
-            
+
             Group group = new Group("group", null);
-            
+
             final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
             EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
             EasyMock.replay(groupPersistence);
-            
+
             final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
             EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
             EasyMock.replay(userPersistence);
-            
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            
-            RemoveUserMemberAction action = new RemoveUserMemberAction(logInfo, "group", userID, userIDType)
+
+            RemoveUserMemberAction action = new RemoveUserMemberAction("group", userID, userIDType)
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
                 {
                     return groupPersistence;
                 };
-                
+
                 @Override
                 <T extends Principal> UserPersistence<T> getUserPersistence()
                 {
                     return userPersistence;
                 };
             };
-            
+
             try
             {
-                action.run();
+                action.doAction();
                 fail("unknown group member should throw MemberNotFoundException");
             }
             catch (MemberNotFoundException ignore) {}
@@ -148,7 +148,7 @@ public class RemoveUserMemberActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
     @Test
     @SuppressWarnings("unchecked")
     public void testRun() throws Exception
@@ -159,38 +159,38 @@ public class RemoveUserMemberActionTest
             String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
             Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
             User<Principal> user = new User<Principal>(userPrincipal);
-            
+
             Group group = new Group("group", null);
             group.getUserMembers().add(user);
             Group modified = new Group("group", null);
-            
+
             final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
             EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
             EasyMock.expect(groupPersistence.modifyGroup(group)).andReturn(modified);
             EasyMock.replay(groupPersistence);
-            
+
             final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
             EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
             EasyMock.replay(userPersistence);
-            
-            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
-            
-            RemoveUserMemberAction action = new RemoveUserMemberAction(logInfo, "group", userID, userIDType)
+
+            RemoveUserMemberAction action = new RemoveUserMemberAction("group", userID, userIDType)
             {
                 @Override
                 <T extends Principal> GroupPersistence<T> getGroupPersistence()
                 {
                     return groupPersistence;
                 };
-                
+
                 @Override
                 <T extends Principal> UserPersistence<T> getUserPersistence()
                 {
                     return userPersistence;
                 };
             };
-            
-            action.run();
+
+            GroupLogInfo logInfo = createMock(GroupLogInfo.class);
+            action.setLogInfo(logInfo);
+            action.doAction();
         }
         catch (Throwable t)
         {
@@ -198,5 +198,5 @@ public class RemoveUserMemberActionTest
             fail("unexpected error: " + t.getMessage());
         }
     }
-    
+
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UsersActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/AbstractUserActionTest.java
similarity index 86%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UsersActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/AbstractUserActionTest.java
index 9f68864b52147a94be6ea974d9ae7fd5cdfe178c..4885e98e5687b878074e5bd6efd850ba0cdb98ad 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UsersActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/AbstractUserActionTest.java
@@ -68,16 +68,12 @@
  */
 package ca.nrc.cadc.ac.server.web.users;
 
-import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserNotFoundException;
-import ca.nrc.cadc.ac.server.UserPersistence;
-import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.Log4jInit;
 
 import java.io.*;
 import java.security.AccessControlException;
-import java.security.Principal;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.log4j.Level;
@@ -85,17 +81,14 @@ import org.apache.log4j.Logger;
 
 import static org.easymock.EasyMock.*;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
-
 /**
  * @author jburke
  */
-public class UsersActionTest
+public class AbstractUserActionTest
 {
-    private final static Logger log = Logger.getLogger(UsersActionTest.class);
+    private final static Logger log = Logger.getLogger(AbstractUserActionTest.class);
 
     @BeforeClass
     public static void setUpClass()
@@ -153,16 +146,9 @@ public class UsersActionTest
         expectLastCall().once();
         replay(response);
 
-        UserLogInfo logInfo = createMock(UserLogInfo.class);
-        logInfo.setSuccess(false);
-        expectLastCall().once();
-        logInfo.setMessage("Internal Transient Error: foo");
-        expectLastCall().once();
-        replay(logInfo);
-
-        UsersActionImpl action = new UsersActionImpl(logInfo);
+        UsersActionImpl action = new UsersActionImpl();
         action.setException(new TransientException("foo"));
-        action.doAction(null, response);
+        action.doAction();
     }
 
     private void testDoAction(String message, int responseCode, Exception e)
@@ -180,26 +166,21 @@ public class UsersActionTest
         expectLastCall().once();
         replay(response);
 
-        UserLogInfo logInfo = createMock(UserLogInfo.class);
-        logInfo.setMessage(message);
-        expectLastCall().once();
-        replay(logInfo);
-
-        UsersActionImpl action = new UsersActionImpl(logInfo);
+        UsersActionImpl action = new UsersActionImpl();
         action.setException(e);
-        action.doAction(null, response);
+        action.doAction();
     }
 
-    public class UsersActionImpl extends UsersAction
+    public class UsersActionImpl extends AbstractUserAction
     {
         Exception exception;
 
-        public UsersActionImpl(UserLogInfo logInfo)
+        public UsersActionImpl()
         {
-            super(logInfo);
+            super();
         }
 
-        public Object run() throws Exception
+        public void doAction() throws Exception
         {
             throw exception;
         }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserActionTest.java
index e116d69fc410b6e5c740bec4259f440753f03bd7..8fbc0a4a2ae91f65aa5507bb0ec5eb6d0f275c3b 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserActionTest.java
@@ -68,7 +68,10 @@
 package ca.nrc.cadc.ac.server.web.users;
 
 import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.json.JsonUserWriter;
 import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.ac.server.web.SyncOutput;
+import ca.nrc.cadc.ac.xml.UserWriter;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import org.junit.Test;
 
@@ -78,7 +81,6 @@ import java.io.StringWriter;
 import java.io.Writer;
 
 import static org.easymock.EasyMock.*;
-import static org.easymock.EasyMock.verify;
 import static org.junit.Assert.assertEquals;
 
 public class GetUserActionTest
@@ -86,13 +88,13 @@ public class GetUserActionTest
     @Test
     public void writeUserXML() throws Exception
     {
-        final HttpServletResponse mockResponse =
-                createMock(HttpServletResponse.class);
+        final SyncOutput mockSyncOut =
+                createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
         final HttpPrincipal userID = new HttpPrincipal("CADCtest");
 
-        final GetUserAction testSubject = new GetUserAction(null, userID)
+        final GetUserAction testSubject = new GetUserAction(userID)
         {
             @Override
             UserPersistence<HttpPrincipal> getUserPersistence()
@@ -106,34 +108,33 @@ public class GetUserActionTest
         final PrintWriter printWriter = new PrintWriter(writer);
 
         expect(mockUserPersistence.getUser(userID)).andReturn(user).once();
-        expect(mockResponse.getWriter()).andReturn(printWriter).once();
-        mockResponse.setContentType("text/xml");
+        expect(mockSyncOut.getWriter()).andReturn(printWriter).once();
+        mockSyncOut.setHeader("Content-Type", "text/xml");
         expectLastCall().once();
 
-        replay(mockResponse, mockUserPersistence);
+        replay(mockSyncOut, mockUserPersistence);
 
-        testSubject.doAction(null, mockResponse);
+        testSubject.setSyncOut(mockSyncOut);
+        testSubject.doAction();
 
-        assertEquals("Wrong XML output.",
-                     "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
-                     "<user>\r\n" +
-                     "  <userID>\r\n" +
-                     "    <identity type=\"HTTP\">CADCtest</identity>\r\n" +
-                     "  </userID>\r\n" +
-                     "</user>\r\n", writer.toString());
-        verify(mockResponse, mockUserPersistence);
+        StringBuilder sb = new StringBuilder();
+        UserWriter userWriter = new UserWriter();
+        userWriter.write(user, sb);
+        assertEquals(sb.toString(), writer.toString());
+
+        verify(mockSyncOut, mockUserPersistence);
     }
 
     @Test
     public void writeUserJSON() throws Exception
     {
-        final HttpServletResponse mockResponse =
-                createMock(HttpServletResponse.class);
+        final SyncOutput mockSyncOut =
+                createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
         final HttpPrincipal userID = new HttpPrincipal("CADCtest");
 
-        final GetUserAction testSubject = new GetUserAction(null, userID)
+        final GetUserAction testSubject = new GetUserAction(userID)
         {
             @Override
             UserPersistence<HttpPrincipal> getUserPersistence()
@@ -142,23 +143,28 @@ public class GetUserActionTest
             }
         };
 
-        testSubject.setAcceptedContentType(UsersAction.JSON_CONTENT_TYPE);
+        testSubject.setAcceptedContentType(AbstractUserAction.JSON_CONTENT_TYPE);
 
         final User<HttpPrincipal> user = new User<HttpPrincipal>(userID);
         final Writer writer = new StringWriter();
         final PrintWriter printWriter = new PrintWriter(writer);
 
         expect(mockUserPersistence.getUser(userID)).andReturn(user).once();
-        expect(mockResponse.getWriter()).andReturn(printWriter).once();
-        mockResponse.setContentType("application/json");
+        expect(mockSyncOut.getWriter()).andReturn(printWriter).once();
+        mockSyncOut.setHeader("Content-Type", "application/json");
         expectLastCall().once();
 
-        replay(mockResponse, mockUserPersistence);
-        testSubject.doAction(null, mockResponse);
+        replay(mockSyncOut, mockUserPersistence);
+        testSubject.setSyncOut(mockSyncOut);
+        UserLogInfo logInfo = createMock(UserLogInfo.class);
+        testSubject.setLogInfo(logInfo);
+        testSubject.doAction();
+
+        StringBuilder sb = new StringBuilder();
+        JsonUserWriter userWriter = new JsonUserWriter();
+        userWriter.write(user, sb);
+        assertEquals(sb.toString(), writer.toString());
 
-        assertEquals("Wrong JSON output.",
-                     "{\"user\":{\"userID\":{\"identity\":{\"name\":\"CADCtest\",\"type\":\"HTTP\"}}}}",
-                     writer.toString());
-        verify(mockResponse, mockUserPersistence);
+        verify(mockSyncOut, mockUserPersistence);
     }
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUsersActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
similarity index 66%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUsersActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
index f1dc7a29aeb521d739a16325c62b97af3a83510a..8e3a6db4ccc8259169fbbb50232317d7e0fa9e86 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUsersActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
@@ -70,7 +70,10 @@ package ca.nrc.cadc.ac.server.web.users;
 
 
 import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.json.JsonUserListWriter;
 import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.ac.server.web.SyncOutput;
+import ca.nrc.cadc.ac.xml.UserListWriter;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import org.apache.log4j.Level;
 
@@ -82,12 +85,11 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
 import static org.easymock.EasyMock.*;
+
 import org.junit.BeforeClass;
 import org.junit.Test;
 import static org.junit.Assert.assertEquals;
@@ -97,7 +99,7 @@ import org.skyscreamer.jsonassert.JSONAssert;
  *
  * @author adriand
  */
-public class GetUsersActionTest
+public class GetUserListActionTest
 {
     @BeforeClass
     public static void setUpClass()
@@ -109,8 +111,8 @@ public class GetUsersActionTest
     @SuppressWarnings("unchecked")
     public void testWriteUsersJSON() throws Exception
     {
-        final HttpServletResponse mockResponse =
-                createMock(HttpServletResponse.class);
+        final SyncOutput mockSyncOut =
+                createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
         final Map<String, PersonalDetails> userEntries =
@@ -122,7 +124,7 @@ public class GetUsersActionTest
                             new PersonalDetails("USER", Integer.toString(i)));
         }
 
-        final GetUsersAction testSubject = new GetUsersAction(null)
+        final GetUserListAction testSubject = new GetUserListAction()
         {
             @Override
             UserPersistence<HttpPrincipal> getUserPersistence()
@@ -131,34 +133,38 @@ public class GetUsersActionTest
             }
         };
 
-        testSubject.setAcceptedContentType(UsersAction.JSON_CONTENT_TYPE);
+        testSubject.setAcceptedContentType(AbstractUserAction.JSON_CONTENT_TYPE);
 
-        final Writer writer = new StringWriter();
-        final PrintWriter printWriter = new PrintWriter(writer);
+        final Writer actualWriter = new StringWriter();
+        final PrintWriter actualPrintWriter = new PrintWriter(actualWriter);
 
         expect(mockUserPersistence.getUsers()).andReturn(
                 userEntries).once();
-        expect(mockResponse.getWriter()).andReturn(printWriter).once();
-        mockResponse.setContentType("application/json");
+        expect(mockSyncOut.getWriter()).andReturn(actualPrintWriter).once();
+        mockSyncOut.setHeader("Content-Type", "application/json");
         expectLastCall().once();
 
-        replay(mockResponse, mockUserPersistence);
-        testSubject.doAction(null, mockResponse);
+        replay(mockSyncOut, mockUserPersistence);
+        testSubject.setSyncOut(mockSyncOut);
+        UserLogInfo logInfo = createMock(UserLogInfo.class);
+        testSubject.setLogInfo(logInfo);
+        testSubject.doAction();
 
-        final JSONArray expected =
-                new JSONArray("[{\"id\":\"USER_1\",\"firstName\":\"USER\",\"lastName\":\"1\"},{\"id\":\"USER_3\",\"firstName\":\"USER\",\"lastName\":\"3\"},{\"id\":\"USER_2\",\"firstName\":\"USER\",\"lastName\":\"2\"},{\"id\":\"USER_4\",\"firstName\":\"USER\",\"lastName\":\"4\"},{\"id\":\"USER_5\",\"firstName\":\"USER\",\"lastName\":\"5\"}]");
-        final JSONArray result = new JSONArray(writer.toString());
+        final Writer expectedWriter = new StringWriter();
+        final PrintWriter expectedPrintWriter = new PrintWriter(expectedWriter);
+        JsonUserListWriter userListWriter = new JsonUserListWriter();
+        userListWriter.write(userEntries, expectedPrintWriter);
+        JSONAssert.assertEquals(expectedWriter.toString(), actualWriter.toString(), false);
 
-        JSONAssert.assertEquals(expected, result, true);
-        verify(mockResponse, mockUserPersistence);
+        verify(mockSyncOut, mockUserPersistence);
     }
 
     @Test
     @SuppressWarnings("unchecked")
     public void testWriteUsersXML() throws Exception
     {
-        final HttpServletResponse mockResponse =
-                createMock(HttpServletResponse.class);
+        final SyncOutput mockSyncOut =
+                createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
         final Map<String, PersonalDetails> userEntries =
@@ -170,7 +176,7 @@ public class GetUsersActionTest
                             new PersonalDetails("USER", Integer.toString(i)));
         }
 
-        final GetUsersAction testSubject = new GetUsersAction(null)
+        final GetUserListAction testSubject = new GetUserListAction()
         {
             @Override
             UserPersistence<HttpPrincipal> getUserPersistence()
@@ -179,44 +185,27 @@ public class GetUsersActionTest
             }
         };
 
-        final Writer writer = new StringWriter();
-        final PrintWriter printWriter = new PrintWriter(writer);
+        final Writer actualWriter = new StringWriter();
+        final PrintWriter actualPrintWriter = new PrintWriter(actualWriter);
 
         expect(mockUserPersistence.getUsers()).andReturn(
                 userEntries).once();
-        expect(mockResponse.getWriter()).andReturn(printWriter).once();
-        mockResponse.setContentType("text/xml");
+        expect(mockSyncOut.getWriter()).andReturn(actualPrintWriter).once();
+        mockSyncOut.setHeader("Content-Type", "text/xml");
         expectLastCall().once();
 
-        replay(mockResponse, mockUserPersistence);
-        testSubject.doAction(null, mockResponse);
-
-        final String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
-                                "<users>\r\n" +
-                                "  <user id=\"USER_1\">\r\n" +
-                                "    <firstName>USER</firstName>\r\n" +
-                                "    <lastName>1</lastName>\r\n" +
-                                "  </user>\r\n" +
-                                "  <user id=\"USER_3\">\r\n" +
-                                "    <firstName>USER</firstName>\r\n" +
-                                "    <lastName>3</lastName>\r\n" +
-                                "  </user>\r\n" +
-                                "  <user id=\"USER_2\">\r\n" +
-                                "    <firstName>USER</firstName>\r\n" +
-                                "    <lastName>2</lastName>\r\n" +
-                                "  </user>\r\n" +
-                                "  <user id=\"USER_4\">\r\n" +
-                                "    <firstName>USER</firstName>\r\n" +
-                                "    <lastName>4</lastName>\r\n" +
-                                "  </user>\r\n" +
-                                "  <user id=\"USER_5\">\r\n" +
-                                "    <firstName>USER</firstName>\r\n" +
-                                "    <lastName>5</lastName>\r\n" +
-                                "  </user>\r\n" +
-                                "</users>\r\n";
-        final String result = writer.toString();
-
-        assertEquals("Wrong XML", expected, result);
-        verify(mockResponse, mockUserPersistence);
+        replay(mockSyncOut, mockUserPersistence);
+        testSubject.setSyncOut(mockSyncOut);
+        UserLogInfo logInfo = createMock(UserLogInfo.class);
+        testSubject.setLogInfo(logInfo);
+        testSubject.doAction();
+
+        final Writer expectedWriter = new StringWriter();
+        final PrintWriter expectedPrintWriter = new PrintWriter(expectedWriter);
+        UserListWriter userListWriter = new UserListWriter();
+        userListWriter.write(userEntries, 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
new file mode 100644
index 0000000000000000000000000000000000000000..ff4538b29c58b0c6a33548b1907f78077309a866
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/ModifyUserActionTest.java
@@ -0,0 +1,166 @@
+/*
+ ************************************************************************
+ *******************  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.server.web.users;
+
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.json.JsonUserWriter;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.ac.server.web.SyncOutput;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.Principal;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+
+
+public class ModifyUserActionTest
+{
+    @Test
+    public void run() throws Exception
+    {
+        final HttpPrincipal httpPrincipal = new HttpPrincipal("CADCtest");
+        User<Principal> expected = new User<Principal>(httpPrincipal);
+        expected.getIdentities().add(httpPrincipal);
+        final PersonalDetails pd = new PersonalDetails("CADC", "Test");
+        pd.email = "CADC.Test@nrc-cnrc.gc.ca";
+        expected.details.add(pd);
+
+        final StringBuilder sb = new StringBuilder();
+        final JsonUserWriter userWriter = new JsonUserWriter();
+        userWriter.write(expected, sb);
+
+        final byte[] input = sb.toString().getBytes();
+        final InputStream inputStream = new ByteArrayInputStream(input);
+
+        // Should match the JSON above, without the e-mail modification.
+        Principal principal = new HttpPrincipal("CADCtest");
+        final User<Principal> userObject =
+                new User<Principal>(principal);
+        userObject.getIdentities().add(principal);
+        final PersonalDetails personalDetail =
+                new PersonalDetails("CADC", "Test");
+        personalDetail.email = "CADC.Test@nrc-cnrc.gc.ca";
+        userObject.details.add(personalDetail);
+
+        final HttpServletRequest mockRequest =
+                createMock(HttpServletRequest.class);
+        final SyncOutput mockSyncOut =
+                createMock(SyncOutput.class);
+
+        @SuppressWarnings("unchecked")
+        final UserPersistence<Principal> mockUserPersistence =
+                createMock(UserPersistence.class);
+
+        expect(mockUserPersistence.modifyUser(userObject)).andReturn(
+                userObject).once();
+//
+//        expect(mockRequest.getRemoteAddr()).andReturn(requestURL).
+//                once();
+
+        mockSyncOut.setHeader("Location", "/CADCtest?idType=http");
+        expectLastCall().once();
+
+        mockSyncOut.setCode(303);
+        expectLastCall().once();
+
+        mockSyncOut.setHeader("Content-Type", "application/json");
+        expectLastCall().once();
+
+        replay(mockRequest, mockSyncOut, mockUserPersistence);
+
+        final ModifyUserAction testSubject = new ModifyUserAction(inputStream)
+        {
+            @Override
+            @SuppressWarnings("unchecked")
+            UserPersistence<Principal> getUserPersistence()
+            {
+                return mockUserPersistence;
+            }
+        };
+
+        testSubject.setAcceptedContentType("application/json");
+        testSubject.syncOut = mockSyncOut;
+        UserLogInfo logInfo = createMock(UserLogInfo.class);
+        testSubject.setLogInfo(logInfo);
+        testSubject.doAction();
+
+        verify(mockRequest, 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 4241c015a6a3f7a2d21b0a0e41f0fac1ff37d45b..131d167a64f5ef259a1fccc97d94ef3523a86d59 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
@@ -92,10 +92,9 @@ public class UserActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("");
-            EasyMock.expect(request.getMethod()).andReturn("PUT");
             EasyMock.expect(request.getInputStream()).andReturn(null);
             EasyMock.replay(request);
-            UsersAction action = UsersActionFactory.getUsersAction(request, null);
+            AbstractUserAction action = UserActionFactory.httpPutFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof CreateUserAction);
         }
@@ -113,10 +112,9 @@ public class UserActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("userName");
-            EasyMock.expect(request.getMethod()).andReturn("DELETE");
             EasyMock.expect(request.getParameter("idType")).andReturn("sessionID");
             EasyMock.replay(request);
-            UsersAction action = UsersActionFactory.getUsersAction(request, null);
+            AbstractUserAction action = UserActionFactory.httpDeleteFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof DeleteUserAction);
         }
@@ -134,10 +132,9 @@ public class UserActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("userName");
-            EasyMock.expect(request.getMethod()).andReturn("GET");
             EasyMock.expect(request.getParameter("idType")).andReturn("sessionID");
             EasyMock.replay(request);
-            UsersAction action = UsersActionFactory.getUsersAction(request, null);
+            AbstractUserAction action = UserActionFactory.httpGetFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof GetUserAction);
         }
@@ -155,11 +152,10 @@ public class UserActionFactoryTest
         {
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("");
-            EasyMock.expect(request.getMethod()).andReturn("GET");
             EasyMock.replay(request);
-            UsersAction action = UsersActionFactory.getUsersAction(request, null);
+            AbstractUserAction action = UserActionFactory.httpGetFactory().createAction(request);
             EasyMock.verify(request);
-            Assert.assertTrue("Wrong action", action instanceof GetUsersAction);
+            Assert.assertTrue("Wrong action", action instanceof GetUserListAction);
         }
         catch (Throwable t)
         {
@@ -178,14 +174,13 @@ public class UserActionFactoryTest
 
             HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
             EasyMock.expect(request.getPathInfo()).andReturn("userName");
-            EasyMock.expect(request.getMethod()).andReturn("POST");
-            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.getInputStream()).andReturn(null);
-            EasyMock.expect(request.getParameter("idType")).andReturn("sessionID");
+            //EasyMock.expect(request.getParameter("idType")).andReturn("sessionID");
             EasyMock.replay(request);
-            UsersAction action = UsersActionFactory.getUsersAction(request, null);
+            AbstractUserAction action = UserActionFactory.httpPostFactory().createAction(request);
             EasyMock.verify(request);
             Assert.assertTrue("Wrong action", action instanceof ModifyUserAction);
         }
@@ -197,41 +192,23 @@ public class UserActionFactoryTest
     }
 
     @Test
-    public void testBadRequests()
+    public void testBadPostRequest()
     {
         try
         {
-            TestRequest[] testRequests =
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("");
+            EasyMock.replay(request);
+            try
             {
-                new TestRequest("", "POST"),
-                new TestRequest("", "DELETE"),
-                new TestRequest("", "HEAD"),
-            };
-
-            for (TestRequest testRequest : testRequests)
+                UserActionFactory.httpPostFactory().createAction(request);
+                Assert.fail("Should have been a bad request");
+            }
+            catch (IllegalArgumentException e)
             {
-
-                log.debug("Testing: " + testRequest);
-
-                HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
-                EasyMock.expect(request.getPathInfo()).andReturn(testRequest.path);
-                EasyMock.expect(request.getMethod()).andReturn(testRequest.method);
-                if (testRequest.paramName != null)
-                {
-                    EasyMock.expect(request.getParameter(testRequest.paramName)).andReturn(testRequest.paramValue);
-                }
-                EasyMock.replay(request);
-                try
-                {
-                    UsersActionFactory.getUsersAction(request, null);
-                    Assert.fail("Should have been a bad request: " + testRequest.method + " on " + testRequest.path);
-                }
-                catch (IllegalArgumentException e)
-                {
-                    // expected
-                }
-                EasyMock.verify(request);
+                // expected
             }
+            EasyMock.verify(request);
         }
         catch (Throwable t)
         {
@@ -240,31 +217,56 @@ public class UserActionFactoryTest
         }
     }
 
-    private class TestRequest
+    @Test
+    public void testBadDeleteRequest()
     {
-        public String path;
-        public String method;
-        public String paramName;
-        public String paramValue;
-
-        public TestRequest(String path, String method)
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("");
+            EasyMock.replay(request);
+            try
+            {
+                UserActionFactory.httpDeleteFactory().createAction(request);
+                Assert.fail("Should have been a bad request");
+            }
+            catch (IllegalArgumentException e)
+            {
+                // expected
+            }
+            EasyMock.verify(request);
+        }
+        catch (Throwable t)
         {
-            this(path, method, null, null);
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
         }
-        public TestRequest(String path, String method, String paramName, String paramValue)
+    }
+
+    @Test
+    public void testBadHeadRequest()
+    {
+        try
         {
-            this.path = path;
-            this.method = method;
-            this.paramName = paramName;
-            this.paramValue = paramValue;
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.replay(request);
+            try
+            {
+                UserActionFactory.httpHeadFactory().createAction(request);
+                Assert.fail("Should have been a bad request");
+            }
+            catch (UnsupportedOperationException e)
+            {
+                // expected
+            }
+            EasyMock.verify(request);
         }
-        @Override
-        public String toString()
+        catch (Throwable t)
         {
-            return method + " on path " + path +
-                ((paramName == null) ? "" : "?" + paramName + "=" + paramValue);
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
         }
-
     }
 
+
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UsersServletTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UserServletTest.java
similarity index 88%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UsersServletTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UserServletTest.java
index 8e0a27f5b988c9cecbdcbd567d418c8825e4671e..3ab00e88c44a487a6367aec89f39d44ecb9142e9 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UsersServletTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/UserServletTest.java
@@ -8,14 +8,14 @@ import static org.easymock.EasyMock.*;
 import static org.junit.Assert.*;
 
 
-public class UsersServletTest
+public class UserServletTest
 {
     @Test
     public void getAcceptedContentTypeJSON() throws Exception
     {
         final HttpServletRequest mockRequest =
                 createMock(HttpServletRequest.class);
-        final UsersServlet testSubject = new UsersServlet();
+        final UserServlet testSubject = new UserServlet();
 
         expect(mockRequest.getHeader("Accept")).
                 andReturn("application/json").once();
@@ -33,7 +33,7 @@ public class UsersServletTest
     {
         final HttpServletRequest mockRequest =
                 createMock(HttpServletRequest.class);
-        final UsersServlet testSubject = new UsersServlet();
+        final UserServlet testSubject = new UserServlet();
 
         expect(mockRequest.getHeader("Accept")).andReturn(null).once();
 
diff --git a/projects/cadcAccessControl/build.xml b/projects/cadcAccessControl/build.xml
index 3296ce950c082b9ba95b90a8d517461a1d0ca14f..4eab166665173e7055d0335620c699ddf90e1ffc 100644
--- a/projects/cadcAccessControl/build.xml
+++ b/projects/cadcAccessControl/build.xml
@@ -117,7 +117,7 @@
     
     <property name="testingJars" value="${build}/class:${jars}:${xerces}:${asm}:${cglib}:${easymock}:${junit}:${objenesis}" />
 
-    <target name="test" depends="compile,compile-test">
+    <target name="single-test" depends="compile,compile-test">
         <echo message="Running test suite..." />
         <junit printsummary="yes" haltonfailure="yes" fork="yes">
             <classpath>
@@ -125,11 +125,7 @@
                 <pathelement path="${build}/test/class"/>
                 <pathelement path="${testingJars}"/>
             </classpath>
-            <test name="ca.nrc.cadc.ac.json.GroupPropertyReaderWriterTest" />
-            <test name="ca.nrc.cadc.ac.json.UserDetailsReaderWriterTest" />
-            <test name="ca.nrc.cadc.ac.json.IdentityReaderWriterTest" />
-            <test name="ca.nrc.cadc.ac.json.UserReaderWriterTest" />
-            <test name="ca.nrc.cadc.ac.json.GroupReaderWriterTest" />
+            <test name="ca.nrc.cadc.ac.json.JsonGroupReaderWriterTest" />
             <formatter type="plain" usefile="false" />
         </junit>
     </target>
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
index b7ad0bcc44ef8c9da5156bd1c74ee320c3f07150..97a63b6bb2827991999266e3e547cdf4bd31f42d 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
@@ -134,18 +134,15 @@ public class User<T extends Principal>
         {
             return false;
         }
-        User other = (User) obj;
+
+        final User other = (User) obj;
         if (userID instanceof X500Principal)
         {
             return AuthenticationUtil.equals(userID, other.userID);
         }
         else
         {
-            if (!userID.equals(other.userID))
-            {
-                return false;
-            }
-            return true;
+            return userID.equals(other.userID);
         }
     }
 
@@ -173,4 +170,31 @@ public class User<T extends Principal>
 
         return matchedDetails;
     }
+
+    /**
+     * Obtain a set of identities whose type match the given one.
+     *
+     * @param identityClass     The class to search on.
+     * @param <S>               The Principal type.
+     * @return                  Set of matched identities, or empty Set.
+     *                          Never null.
+     */
+    public <S extends Principal> Set<S> getIdentities(
+            final Class<S> identityClass)
+    {
+        final Set<S> matchedIdentities = new HashSet<S>();
+
+        for (final Principal p : identities)
+        {
+            if (p.getClass() == identityClass)
+            {
+                // This casting shouldn't happen, but it's the only way to
+                // do this without a lot of work.
+                // jenkinsd 2014.09.26
+                matchedIdentities.add((S) p);
+            }
+        }
+
+        return matchedIdentities;
+    }
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/UserAlreadyExistsException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserAlreadyExistsException.java
similarity index 100%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/UserAlreadyExistsException.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserAlreadyExistsException.java
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserRequest.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserRequest.java
index d1c28cfac115e291be92c9fc60618f32de33113f..664b18d2a99d3f39e2927e9e7e6e89986371d2b6 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserRequest.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserRequest.java
@@ -74,15 +74,15 @@ import java.security.Principal;
 public class UserRequest<T extends Principal>
 {
     private User<T> user;
-    private String password;
+    private char[] password;
 
-    public UserRequest(final User<T> user, final String password)
+    public UserRequest(final User<T> user, final char[] password)
     {
         if (user == null)
         {
             throw new IllegalArgumentException("null user");
         }
-        if (password == null || password.isEmpty())
+        if (password == null || password.length == 0)
         {
             throw new IllegalArgumentException("null or empty password");
         }
@@ -95,11 +95,19 @@ public class UserRequest<T extends Principal>
         return this.user;
     }
 
-    public String getPassword()
+    public char[] getPassword()
     {
         return this.password;
     }
 
+    public void clear()
+    {
+        for (int i = 0; i < password.length; i++)
+        {
+            password[i] = '0';
+        }
+    }
+
     @Override
     public boolean equals(Object o)
     {
@@ -115,7 +123,6 @@ public class UserRequest<T extends Principal>
         UserRequest<?> that = (UserRequest<?>) o;
 
         return user.equals(that.user);
-
     }
 
     @Override
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 8f4eab02036b1155294d80566c74a434875ad142..bad17d1f5f80cee819082265ca7f344940ac14b1 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
@@ -68,7 +68,34 @@
  */
 package ca.nrc.cadc.ac.client;
 
-import java.io.*;
+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;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -84,27 +111,6 @@ 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 ca.nrc.cadc.util.StringUtil;
-import org.apache.log4j.Logger;
-
-import ca.nrc.cadc.ac.xml.GroupReader;
-import ca.nrc.cadc.ac.xml.GroupWriter;
-import ca.nrc.cadc.ac.xml.GroupsReader;
-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 org.json.JSONObject;
-
 
 /**
  * Client class for performing group searching and group actions
@@ -217,7 +223,7 @@ public class GMSClient
     {
         final URL usersListURL = new URL(this.baseURL + "/users");
         return new HttpDownload(usersListURL,
-                                new JSONUserListInputStreamWrapper(webUsers));
+                                new JsonUserListInputStreamWrapper(webUsers));
     }
 
     /**
@@ -242,7 +248,8 @@ public class GMSClient
         clearCache();
 
         StringBuilder groupXML = new StringBuilder();
-        GroupWriter.write(group, groupXML);
+        GroupWriter groupWriter = new GroupWriter();
+        groupWriter.write(group, groupXML);
         log.debug("createGroup: " + groupXML);
 
         byte[] bytes = groupXML.toString().getBytes("UTF-8");
@@ -283,7 +290,8 @@ public class GMSClient
         try
         {
             log.debug("createGroup returned: " + retXML);
-            return GroupReader.read(retXML);
+            GroupReader groupReader = new GroupReader();
+            return groupReader.read(retXML);
         }
         catch (Exception bug)
         {
@@ -339,7 +347,8 @@ public class GMSClient
         {
             String groupXML = new String(out.toByteArray(), "UTF-8");
             log.debug("getGroup returned: " + groupXML);
-            return GroupReader.read(groupXML);
+            GroupReader groupReader = new GroupReader();
+            return groupReader.read(groupXML);
         }
         catch (Exception bug)
         {
@@ -441,7 +450,8 @@ public class GMSClient
         clearCache();
 
         StringBuilder groupXML = new StringBuilder();
-        GroupWriter.write(group, groupXML);
+        GroupWriter groupWriter = new GroupWriter();
+        groupWriter.write(group, groupXML);
         log.debug("updateGroup: " + groupXML);
 
         HttpPost transfer = new HttpPost(updateGroupURL, groupXML.toString(),
@@ -482,7 +492,8 @@ public class GMSClient
         {
             String retXML = transfer.getResponseBody();
             log.debug("getGroup returned: " + retXML);
-            return GroupReader.read(retXML);
+            GroupReader groupReader = new GroupReader();
+            return groupReader.read(retXML);
         }
         catch (Exception bug)
         {
@@ -887,7 +898,8 @@ public class GMSClient
         {
             String groupsXML = new String(out.toByteArray(), "UTF-8");
             log.debug("getMemberships returned: " + groupsXML);
-            List<Group> groups = GroupsReader.read(groupsXML);
+            GroupListReader groupListReader = new GroupListReader();
+            List<Group> groups = groupListReader.read(groupsXML);
             setCachedGroups(userID, groups, role);
             return groups;
         }
@@ -1004,7 +1016,8 @@ public class GMSClient
         {
             String groupsXML = new String(out.toByteArray(), "UTF-8");
             log.debug("getMembership returned: " + groupsXML);
-            List<Group> groups = GroupsReader.read(groupsXML);
+            GroupListReader groupListReader = new GroupListReader();
+            List<Group> groups = groupListReader.read(groupsXML);
             if (groups.size() == 0)
             {
                 return null;
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapper.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JsonUserListInputStreamWrapper.java
similarity index 96%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapper.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JsonUserListInputStreamWrapper.java
index 8e3a47447965467f2f822dc5a532fff1ab0571a0..f00b0aeef2821f4740ae2eab7130b33d0001009f 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapper.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JsonUserListInputStreamWrapper.java
@@ -82,15 +82,15 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.List;
 
-public class JSONUserListInputStreamWrapper implements InputStreamWrapper
+public class JsonUserListInputStreamWrapper implements InputStreamWrapper
 {
     private static final Logger LOGGER = Logger
-            .getLogger(JSONUserListInputStreamWrapper.class);
+            .getLogger(JsonUserListInputStreamWrapper.class);
     private final List<User<HttpPrincipal>> output;
 
 
-    public JSONUserListInputStreamWrapper(
-            final List<User<HttpPrincipal>> output)
+    public JsonUserListInputStreamWrapper(
+        final List<User<HttpPrincipal>> output)
     {
         this.output = output;
     }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/GroupReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/GroupReader.java
deleted file mode 100755
index f726b87a5ea426ffd94bbf914b1da5124bb533ee..0000000000000000000000000000000000000000
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/GroupReader.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- ************************************************************************
- *******************  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.json;
-
-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 org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.net.URISyntaxException;
-import java.security.Principal;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.util.Scanner;
-
-public class GroupReader
-{
-    /**
-     * Construct a Group from a InputStream.
-     *
-     * @param in InputStream.
-     * @return Group Group.
-     * @throws ReaderException
-     * @throws IOException
-     * @throws URISyntaxException
-     */
-    public static 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.
-     *
-     * @param reader Reader.
-     * @return Group Group.
-     * @throws ReaderException
-     * @throws IOException
-     * @throws URISyntaxException
-     */
-    public static Group read(Reader reader)
-        throws ReaderException, IOException
-    {
-        if (reader == null)
-        {
-            throw new IllegalArgumentException("reader must not be null");
-        }
-
-        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
-     * @throws URISyntaxException
-     */
-    public static 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
-        {
-            return parseGroup(new JSONObject(json).getJSONObject("group"));
-        }
-        catch (JSONException e)
-        {
-            String error = "Unable to parse JSON to Group because " +
-                           e.getMessage();
-            throw new ReaderException(error, e);
-        }
-    }
-
-    protected static Group parseGroup(JSONObject groupObject)
-        throws ReaderException, JSONException
-    {
-        String uri = groupObject.getString("uri");
-
-        // 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;
-        if (groupObject.has("owner"))
-        {
-            JSONObject ownerObject = groupObject.getJSONObject("owner");
-            JSONObject userObject = ownerObject.getJSONObject("user");
-            user = UserReader.parseUser(userObject);
-        }
-
-        Group group = new Group(groupID, user);
-
-        // description
-        if (groupObject.has("description"))
-        {
-            group.description = groupObject.getString("description");
-        }
-
-        // lastModified
-        if (groupObject.has("lastModified"))
-        {
-            try
-            {
-                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
-                group.lastModified = df.parse(groupObject.getString("lastModified"));
-            }
-            catch (ParseException e)
-            {
-                String error = "Unable to parse group lastModified because " + e.getMessage();
-
-                throw new ReaderException(error);
-            }
-        }
-
-        // properties
-        if (groupObject.has("description"))
-        {
-            JSONArray propertiesArray = groupObject.getJSONArray("properties");
-            for (int i = 0; i < propertiesArray.length(); i++)
-            {
-                JSONObject propertiesObject = propertiesArray.getJSONObject(i);
-                JSONObject propertyObject = propertiesObject.getJSONObject("property");
-                group.getProperties().add(GroupPropertyReader.read(propertyObject));
-            }
-        }
-
-        // groupMembers
-        if (groupObject.has("groupMembers"))
-        {
-            JSONArray groupMembersArray = groupObject.getJSONArray("groupMembers");
-            for (int i = 0; i < groupMembersArray.length(); i++)
-            {
-                JSONObject groupMembersObject = groupMembersArray.getJSONObject(i);
-                JSONObject groupMemberObject = groupMembersObject.getJSONObject("group");
-                group.getGroupMembers().add(parseGroup(groupMemberObject));
-            }
-        }
-
-        // userMembers
-        if (groupObject.has("userMembers"))
-        {
-            JSONArray userMembersArray = groupObject.getJSONArray("userMembers");
-            for (int i = 0; i < userMembersArray.length(); i++)
-            {
-                JSONObject userMemberObject = userMembersArray.getJSONObject(i);
-                JSONObject userObject = userMemberObject.getJSONObject("user");
-                group.getUserMembers().add(UserReader.parseUser(userObject));
-            }
-        }
-
-        // groupAdmins
-        if (groupObject.has("groupAdmins"))
-        {
-            JSONArray groupAdminsArray = groupObject.getJSONArray("groupAdmins");
-            for (int i = 0; i < groupAdminsArray.length(); i++)
-            {
-                JSONObject groupAdminsObject = groupAdminsArray.getJSONObject(i);
-                JSONObject groupAdminObject = groupAdminsObject.getJSONObject("group");
-                group.getGroupAdmins().add(parseGroup(groupAdminObject));
-            }
-        }
-
-        // userAdmins
-        if (groupObject.has("userAdmins"))
-        {
-            JSONArray userAdminsArray = groupObject.getJSONArray("userAdmins");
-            for (int i = 0; i < userAdminsArray.length(); i++)
-            {
-                JSONObject userAdminObject = userAdminsArray.getJSONObject(i);
-                JSONObject userObject = userAdminObject.getJSONObject("user");
-                group.getUserAdmins().add(UserReader.parseUser(userObject));
-            }
-        }
-
-        return group;
-    }
-}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/GroupWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/GroupWriter.java
deleted file mode 100755
index d5a1f1b684a75eb1f3fedd04286df23e95cac6e5..0000000000000000000000000000000000000000
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/GroupWriter.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- ************************************************************************
- *******************  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.json;
-
-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.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.text.DateFormat;
-
-public class GroupWriter
-{
-    /**
-     * Write a Group to a StringBuilder.
-     * @param group
-     * @param builder
-     * @throws IOException
-     * @throws WriterException
-     */
-    public static 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
-     */
-    public static 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.
-     *
-     * @param group Group to write.
-     * @param writer  Writer to write to.
-     * @throws IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    public static void write(Group group, Writer writer)
-        throws IOException, WriterException
-    {
-        if (group == null)
-        {
-            throw new WriterException("null group");
-        }
-
-        try
-        {
-            getGroupObject(group).write(writer);
-        }
-        catch (JSONException e)
-        {
-            final String error = "Unable to create JSON for Group " +
-                                 " because " + e.getMessage();
-            throw new WriterException(error, e);
-        }
-    }
-
-    /**
-     *
-     * @param group
-     * @return
-     * @throws WriterException
-     */
-    public static JSONObject getGroupObject(Group group)
-        throws WriterException, JSONException
-    {
-        return getGroupObject(group, true);
-    }
-
-    public static JSONObject getGroupObject(Group group, boolean deepCopy)
-        throws WriterException, JSONException
-    {
-        JSONObject groupObject = new JSONObject();
-        groupObject.put("uri", AC.GROUP_URI + group.getID());
-
-        // Group owner
-        if (group.getOwner() != null)
-        {
-            groupObject.put("owner", UserWriter.getUserObject(group.getOwner()));
-        }
-
-        if (deepCopy)
-        {
-            // Group description
-            if (group.description != null)
-            {
-                groupObject.put("description", group.description);
-            }
-
-            // lastModified
-            if (group.lastModified != null)
-            {
-                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
-                groupObject.put("lastModified", df.format(group.lastModified));
-            }
-
-            // Group properties
-            if (!group.getProperties().isEmpty())
-            {
-                JSONArray propertiesArray = new JSONArray();
-                for (GroupProperty property : group.getProperties())
-                {
-                    JSONObject propertyObject = new JSONObject();
-                    propertyObject.put("property", GroupPropertyWriter.write(property));
-                    propertiesArray.put(propertyObject);
-                }
-                groupObject.put("properties", propertiesArray);
-            }
-
-            // Group groupMembers.
-            if ((group.getGroupMembers() != null) && (!group.getGroupMembers().isEmpty()))
-            {
-                JSONArray groupMembersArray = new JSONArray();
-                for (Group groupMember : group.getGroupMembers())
-                {
-                    groupMembersArray.put(getGroupObject(groupMember, false));
-                }
-                groupObject.put("groupMembers", groupMembersArray);
-            }
-
-            // Group userMembers
-            if ((group.getUserMembers() != null) && (!group.getUserMembers().isEmpty()))
-            {
-                JSONArray userMembersArray = new JSONArray();
-                for (User<? extends Principal> userMember : group.getUserMembers())
-                {
-                    userMembersArray.put(UserWriter.getUserObject(userMember));
-                }
-                groupObject.put("userMembers", userMembersArray);
-            }
-
-            // Group groupAdmins.
-            if ((group.getGroupAdmins() != null) && (!group.getGroupAdmins().isEmpty()))
-            {
-                JSONArray groupAdminsArray = new JSONArray();
-                for (Group groupAdmin : group.getGroupAdmins())
-                {
-                    groupAdminsArray.put(getGroupObject(groupAdmin, false));
-                }
-                groupObject.put("groupAdmins", groupAdminsArray);
-            }
-
-            // Group userAdmins
-            if ((group.getUserAdmins() != null) && (!group.getUserAdmins().isEmpty()))
-            {
-                JSONArray userAdminsArray = new JSONArray();
-                for (User<? extends Principal> userAdmin : group.getUserAdmins())
-                {
-                    userAdminsArray.put(UserWriter.getUserObject(userAdmin));
-                }
-                groupObject.put("userAdmins", userAdminsArray);
-            }
-        }
-
-        return new JSONObject().put("group", groupObject);
-    }
-
-}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
new file mode 100755
index 0000000000000000000000000000000000000000..a240184be0794193897d8924ce1002747b5ecb4c
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
@@ -0,0 +1,173 @@
+/*
+ ************************************************************************
+ *******************  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.json;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.ReaderException;
+import ca.nrc.cadc.ac.xml.GroupReader;
+import ca.nrc.cadc.xml.JsonInputter;
+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;
+
+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.
+     *
+     * @param reader Reader.
+     * @return Group Group.
+     * @throws ReaderException
+     * @throws IOException
+     */
+    @Override
+    public Group read(Reader reader)
+        throws ReaderException, IOException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        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();
+            jsonInputter.getListElementMap().put("identities", "identity");
+            jsonInputter.getListElementMap().put("properties", "property");
+            jsonInputter.getListElementMap().put("details", "userDetails");
+            jsonInputter.getListElementMap().put("groupMembers", "group");
+            jsonInputter.getListElementMap().put("groupAdmins", "group");
+            jsonInputter.getListElementMap().put("userMembers", "user");
+            jsonInputter.getListElementMap().put("userAdmins", "user");
+
+            Document document = jsonInputter.input(json);
+            return GroupReader.parseGroup(document.getRootElement());
+        }
+        catch (JSONException e)
+        {
+            String error = "Unable to parse JSON to Group because " +
+                           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
new file mode 100755
index 0000000000000000000000000000000000000000..2db229327ce52306744846b95f0a4ffb19f310ce
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java
@@ -0,0 +1,160 @@
+/*
+ ************************************************************************
+ *******************  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.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;
+
+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.
+     *
+     * @param group Group to write.
+     * @param writer  Writer to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws WriterException
+     */
+    @Override
+    public void write(Group group, Writer writer)
+        throws IOException, WriterException
+    {
+        if (group == null)
+        {
+            throw new WriterException("null group");
+        }
+
+        Element children = GroupWriter.getGroupElement(group);
+        Element groupElement = new Element("group");
+        groupElement.addContent(children);
+        Document document = new Document();
+        document.setRootElement(groupElement);
+
+        JsonOutputter jsonOutputter = new JsonOutputter();
+        jsonOutputter.getListElementNames().add("properties");
+        jsonOutputter.getListElementNames().add("userMembers");
+        jsonOutputter.getListElementNames().add("groupMembers");
+        jsonOutputter.getListElementNames().add("userAdmins");
+        jsonOutputter.getListElementNames().add("groupAdmins");
+        jsonOutputter.getListElementNames().add("identities");
+        jsonOutputter.getListElementNames().add("details");
+
+        jsonOutputter.output(document, writer);
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UsersWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
similarity index 99%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UsersWriter.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
index 308148bcdadde1530c994bc1bea70a7a61f9cd14..5bf9317a44b6c30c61b50f569471f652dc875daa 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UsersWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
@@ -80,7 +80,7 @@ import java.util.Map;
 /**
  * Class to write out, as JSON, a list of user entries.
  */
-public class UsersWriter
+public class JsonUserListWriter
 {
     public static void write(final Map<String, PersonalDetails> users,
                              final Writer writer) throws IOException
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
similarity index 76%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserReader.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
index b6aedd7f92293b5f7c49580ff44fd5ef20078567..2c4a8e376ec338c5fa2157d08767ca79f8999d5d 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
@@ -70,10 +70,10 @@ package ca.nrc.cadc.ac.json;
 
 import ca.nrc.cadc.ac.ReaderException;
 import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserDetails;
-import org.json.JSONArray;
+import ca.nrc.cadc.ac.xml.UserReader;
+import ca.nrc.cadc.xml.JsonInputter;
+import org.jdom2.Document;
 import org.json.JSONException;
-import org.json.JSONObject;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -81,7 +81,7 @@ import java.io.Reader;
 import java.security.Principal;
 import java.util.Scanner;
 
-public class UserReader
+public class JsonUserReader extends UserReader
 {
     /**
      * Construct a User from a InputStream.
@@ -91,7 +91,8 @@ public class UserReader
      * @throws ReaderException
      * @throws IOException
      */
-    public static User<Principal> read(InputStream in)
+    @Override
+    public User<Principal> read(InputStream in)
         throws IOException
     {
         if (in == null)
@@ -113,7 +114,8 @@ public class UserReader
      * @throws ReaderException
      * @throws IOException
      */
-    public static User<Principal> read(Reader reader)
+    @Override
+    public User<Principal> read(Reader reader)
         throws IOException
     {
         if (reader == null)
@@ -135,7 +137,8 @@ public class UserReader
      * @throws ReaderException
      * @throws IOException
      */
-    public static User<Principal> read(String json)
+    @Override
+    public User<Principal> read(String json)
         throws IOException
     {
         if (json == null || json.isEmpty())
@@ -146,50 +149,19 @@ public class UserReader
         // Create a JSONObject from the JSON
         try
         {
-            return parseUser(new JSONObject(json).getJSONObject("user"));
+            JsonInputter jsonInputter = new JsonInputter();
+            jsonInputter.getListElementMap().put("identities", "identity");
+            jsonInputter.getListElementMap().put("details", "userDetails");
+
+            Document document = jsonInputter.input(json);
+            return UserReader.parseUser(document.getRootElement());
         }
         catch (JSONException e)
         {
             String error = "Unable to parse JSON to User because " +
-                           e.getMessage();
+                e.getMessage();
             throw new ReaderException(error, e);
         }
     }
 
-    protected static User<Principal> parseUser(JSONObject userObject)
-        throws ReaderException, JSONException
-    {
-        JSONObject userIDObject = userObject.getJSONObject("userID");
-        JSONObject userIDIdentityObject = userIDObject.getJSONObject("identity");
-
-        Principal userID = IdentityReader.read(userIDIdentityObject);
-        User<Principal> user = new User<Principal>(userID);
-
-        // identities
-        if (userObject.has("identities"))
-        {
-            JSONArray identitiesArray = userObject.getJSONArray("identities");
-            for (int i = 0; i < identitiesArray.length(); i++)
-            {
-                JSONObject identitiesObject = identitiesArray.getJSONObject(i);
-                JSONObject identityObject = identitiesObject.getJSONObject(("identity"));
-                user.getIdentities().add(IdentityReader.read(identityObject));
-            }
-        }
-
-        // details
-        if (userObject.has("details"))
-        {
-            JSONArray detailsArray = userObject.getJSONArray("details");
-            for (int i = 0; i < detailsArray.length(); i++)
-            {
-                JSONObject detailsObject = detailsArray.getJSONObject(i);
-                JSONObject userDetailsObject = detailsObject.getJSONObject(UserDetails.NAME);
-                user.details.add(UserDetailsReader.read(userDetailsObject));
-            }
-        }
-
-        return user;
-    }
-
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserRequestReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
similarity index 84%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserRequestReader.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
index 4e0ed7a9d0095fa85fb07c6079c50b2b10dfb359..b8d61e8cea9567a2caac1beb562766a5e05b1bfa 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserRequestReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
@@ -69,46 +69,20 @@
 package ca.nrc.cadc.ac.json;
 
 import ca.nrc.cadc.ac.ReaderException;
-import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.xml.UserRequestReader;
+import ca.nrc.cadc.xml.JsonInputter;
+import org.jdom2.Document;
 import org.json.JSONException;
-import org.json.JSONObject;
 
-import java.io.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
 import java.security.Principal;
 import java.util.Scanner;
 
-public class UserRequestReader
+public class JsonUserRequestReader extends UserRequestReader
 {
-    /**
-     * Construct a UserRequest from an XML String source.
-     *
-     * @param json String of the XML.
-     * @return UserRequest UserRequest.
-     * @throws IOException
-     */
-    public static UserRequest<Principal> read(String json)
-        throws IOException
-    {
-        if (json == null)
-        {
-            throw new IllegalArgumentException("XML must not be null");
-        }
-        else
-        {
-            try
-            {
-                return parseUserRequest(new JSONObject(json));
-            }
-            catch (JSONException e)
-            {
-                String error = "Unable to parse JSON to User because " +
-                               e.getMessage();
-                throw new ReaderException(error, e);
-            }
-        }
-    }
-
     /**
      * Construct a User from a InputStream.
      *
@@ -117,7 +91,8 @@ public class UserRequestReader
      * @throws ReaderException
      * @throws IOException
      */
-    public static UserRequest<Principal> read(InputStream in)
+    @Override
+    public UserRequest<Principal> read(InputStream in)
             throws IOException
     {
         if (in == null)
@@ -139,7 +114,8 @@ public class UserRequestReader
      * @throws ReaderException
      * @throws IOException
      */
-    public static UserRequest<Principal> read(Reader reader)
+    @Override
+    public UserRequest<Principal> read(Reader reader)
             throws IOException
     {
         if (reader == null)
@@ -153,16 +129,39 @@ public class UserRequestReader
         return read(json);
     }
 
-
-    protected static UserRequest<Principal> parseUserRequest(
-            JSONObject userRequestObject)
-        throws ReaderException, JSONException
+    /**
+     * 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
     {
-        final User<Principal> user =
-                ca.nrc.cadc.ac.json.UserReader.parseUser(
-                        userRequestObject.getJSONObject("user"));
+        if (json == null)
+        {
+            throw new IllegalArgumentException("JSON must not be null");
+        }
+        else
+        {
+            try
+            {
+                JsonInputter jsonInputter = new JsonInputter();
+                jsonInputter.getListElementMap().put("identities", "identity");
+                jsonInputter.getListElementMap().put("details", "userDetails");
 
-        return new UserRequest<Principal>(user, userRequestObject.
-                getString("password"));
+                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);
+            }
+        }
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ebb07a652a9b972252d528e869ac86938d33c12
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
@@ -0,0 +1,63 @@
+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;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.security.Principal;
+
+/**
+ * Class to write a JSON representation of a UserRequest object.
+ */
+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.
+     *
+     * @param userRequest UserRequest to write.
+     * @param writer Writer to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws WriterException
+     */
+    public static void write(UserRequest<? extends Principal> userRequest, Writer writer)
+        throws IOException, WriterException
+    {
+        if (userRequest == null)
+        {
+            throw new WriterException("null UserRequest");
+        }
+
+        Element children = UserRequestWriter.getUserRequestElement(userRequest);
+        Element userRequestElement = new Element("userRequest");
+        userRequestElement.addContent(children);
+        Document document = new Document();
+        document.setRootElement(userRequestElement);
+
+        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/UserWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
similarity index 72%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserWriter.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
index 2865235807f885d24cbe3986b370c50e5bf8d753..444a670a4453e0bd2e6b7f481fb9247e73df0525 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UserWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
@@ -71,7 +71,12 @@ 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;
@@ -85,7 +90,7 @@ import java.io.Writer;
 import java.security.Principal;
 import java.util.Set;
 
-public class UserWriter
+public class JsonUserWriter extends UserWriter
 {
     /**
      * Write a User as a JSON string to a StringBuilder.
@@ -95,7 +100,8 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(User<? extends Principal> user, StringBuilder builder)
+    @Override
+    public void write(User<? extends Principal> user, StringBuilder builder)
         throws IOException, WriterException
     {
         write(user, new StringBuilderWriter(builder));
@@ -109,7 +115,8 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(User<? extends Principal> user, OutputStream out)
+    @Override
+    public void write(User<? extends Principal> user, OutputStream out)
         throws IOException, WriterException
     {                
         OutputStreamWriter outWriter;
@@ -132,7 +139,8 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(User<? extends Principal> user, Writer writer)
+    @Override
+    public void write(User<? extends Principal> user, Writer writer)
         throws IOException, WriterException
     {
         if (user == null)
@@ -140,62 +148,17 @@ public class UserWriter
             throw new WriterException("null User");
         }
 
-        try
-        {
-            getUserObject(user).write(writer);
-        }
-        catch (JSONException e)
-        {
-            final String error = "Unable to create JSON for User " +
-                                 " because " + e.getMessage();
-            throw new WriterException(error, e);
-        }
-    }
+        Element children = UserWriter.getUserElement(user);
+        Element userElement = new Element("user");
+        userElement.addContent(children);
+        Document document = new Document();
+        document.setRootElement(userElement);
 
-    /**
-     * Build a User JSONObject from a User.
-     *
-     * @param user User.
-     * @return JSONObject.
-     * @throws WriterException
-     */
-    public static JSONObject getUserObject(User<? extends Principal> user)
-        throws WriterException, JSONException
-    {
-        JSONObject userObject = new JSONObject();
-        JSONObject userIDIdentityObject = new JSONObject();
-        userIDIdentityObject.put("identity", IdentityWriter.write(user.getUserID()));
-        userObject.put("userID", userIDIdentityObject);
-
-        // identities
-        Set<Principal> identities = user.getIdentities();
-        if (!identities.isEmpty())
-        {
-            JSONArray identityArray = new JSONArray();
-            for (Principal identity : identities)
-            {
-                JSONObject identityObject = new JSONObject();
-                identityObject.put("identity" , IdentityWriter.write(identity));
-                identityArray.put(identityObject);
-            }
-            userObject.put("identities", identityArray);
-        }
-
-        // details
-        if (!user.details.isEmpty())
-        {
-            JSONArray detailsArray = new JSONArray();
-            Set<UserDetails> userDetails = user.details;
-            for (UserDetails userDetail : userDetails)
-            {
-                JSONObject detailsObject = new JSONObject();
-                detailsObject.put(UserDetails.NAME , UserDetailsWriter.write(userDetail));
-                detailsArray.put(detailsObject);
-            }
-            userObject.put("details", detailsArray);
-        }
+        JsonOutputter jsonOutputter = new JsonOutputter();
+        jsonOutputter.getListElementNames().add("identities");
+        jsonOutputter.getListElementNames().add("details");
 
-        return new JSONObject().put("user", userObject);
+        jsonOutputter.output(document, writer);
     }
 
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupsReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
similarity index 95%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupsReader.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
index c0c31fbe4a2ecfbbb7f44e0baf411cf15f08745c..cd962918e443f8d3db33cd7eead08d475285543d 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupsReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
@@ -84,7 +84,11 @@ import org.jdom2.Document;
 import org.jdom2.Element;
 import org.jdom2.JDOMException;
 
-public class GroupsReader
+/**
+ * Class to read an XML representation of a list of Groups
+ * into a List of Group objects.
+ */
+public class GroupListReader
 {
     /**
      * Construct a list of Group's from an XML String source.
@@ -95,7 +99,7 @@ public class GroupsReader
      * @throws java.io.IOException
      * @throws java.net.URISyntaxException
      */
-    public static List<Group> read(String xml)
+    public List<Group> read(String xml)
         throws ReaderException, IOException, URISyntaxException
     {
         if (xml == null)
@@ -114,7 +118,7 @@ public class GroupsReader
      * @throws java.io.IOException
      * @throws java.net.URISyntaxException
      */
-    public static List<Group> read(InputStream in)
+    public List<Group> read(InputStream in)
         throws ReaderException, IOException, URISyntaxException
     {
         if (in == null)
@@ -142,7 +146,7 @@ public class GroupsReader
      * @throws java.io.IOException
      * @throws java.net.URISyntaxException
      */
-    public static List<Group> read(Reader reader)
+    public List<Group> read(Reader reader)
         throws ReaderException, IOException, URISyntaxException
     {
         if (reader == null)
@@ -182,7 +186,7 @@ public class GroupsReader
         List<Element> groupElements = groupsElement.getChildren("group");
         for (Element groupElement : groupElements)
         {
-            groups.add(ca.nrc.cadc.ac.xml.GroupReader.parseGroup(groupElement));
+            groups.add(GroupReader.parseGroup(groupElement));
         }
 
         return groups;
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupsWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
similarity index 90%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupsWriter.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
index fb92d2cd268c54985b379954a90493888043de8e..b3707fb3d7ce6bb3e2f3c16a6e9f59c01606b786 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupsWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
@@ -15,7 +15,10 @@ import org.jdom2.Element;
 import org.jdom2.output.Format;
 import org.jdom2.output.XMLOutputter;
 
-public class GroupsWriter
+/**
+ * Class to write a XML representation from a Collection of Groups objects.
+ */
+public class GroupListWriter
 {
     /**
      * Write a List of Group's to a StringBuilder.
@@ -24,7 +27,7 @@ public class GroupsWriter
      * @throws java.io.IOException
      * @throws WriterException
      */
-    public static void write(Collection<Group> groups, StringBuilder builder)
+    public void write(Collection<Group> groups, StringBuilder builder)
         throws IOException, WriterException
     {
         write(groups, new StringBuilderWriter(builder));
@@ -38,7 +41,7 @@ public class GroupsWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(Collection<Group> groups, OutputStream out)
+    public void write(Collection<Group> groups, OutputStream out)
         throws IOException, WriterException
     {
         OutputStreamWriter outWriter;
@@ -61,7 +64,7 @@ public class GroupsWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(Collection<Group> groups, Writer writer)
+    public void write(Collection<Group> groups, Writer writer)
         throws IOException, WriterException
     {
         if (groups == null)
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 8cfc37eec446e677bb9f648cfe4a52d2dc82f2b9..0f0d1dca1233d54a01c9cbb0da2ba9685fffc18c 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
@@ -90,6 +90,9 @@ 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
 {
 
@@ -102,7 +105,7 @@ public class GroupReader
      * @throws java.io.IOException
      * @throws java.net.URISyntaxException
      */
-    public static Group read(String xml)
+    public Group read(String xml)
         throws ReaderException, IOException, URISyntaxException
     {
         if (xml == null)
@@ -121,7 +124,7 @@ public class GroupReader
      * @throws java.io.IOException
      * @throws java.net.URISyntaxException
      */
-    public static Group read(InputStream in)
+    public Group read(InputStream in)
         throws ReaderException, IOException
     {
         if (in == null)
@@ -149,7 +152,7 @@ public class GroupReader
      * @throws java.io.IOException
      * @throws java.net.URISyntaxException
      */
-    public static Group read(Reader reader)
+    public Group read(Reader reader)
         throws ReaderException, IOException
     {
         if (reader == null)
@@ -181,7 +184,7 @@ public class GroupReader
         return parseGroup(root);
     }
 
-    protected static Group parseGroup(Element groupElement)
+    public static Group parseGroup(Element groupElement)
         throws ReaderException
     {
         String uri = groupElement.getAttributeValue("uri");
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 18f307a80afea5fb5552e34f2b9c9b338e871c1b..380ba79a8d7ec2fa8e203f8e7f6d8bafbb51ff0e 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
@@ -90,6 +90,9 @@ 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
 {
     /**
@@ -99,7 +102,7 @@ public class GroupWriter
      * @throws java.io.IOException
      * @throws WriterException
      */
-    public static void write(Group group, StringBuilder builder)
+    public void write(Group group, StringBuilder builder)
         throws IOException, WriterException
     {
         write(group, new StringBuilderWriter(builder));
@@ -113,7 +116,7 @@ public class GroupWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(Group group, OutputStream out)
+    public void write(Group group, OutputStream out)
         throws IOException, WriterException
     {
         OutputStreamWriter outWriter;
@@ -136,7 +139,7 @@ public class GroupWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(Group group, Writer writer)
+    public void write(Group group, Writer writer)
         throws IOException, WriterException
     {
         if (group == null)
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UsersWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
similarity index 96%
rename from projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UsersWriter.java
rename to projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
index 757af4901c0a2572ac563350ca76e142dd7d12ea..f08d4de4c4ba371da692b858c25e51198cb6abe0 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UsersWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
@@ -78,7 +78,10 @@ import java.io.IOException;
 import java.io.Writer;
 import java.util.Map;
 
-public class UsersWriter
+/**
+ * Class to write a XML representation of a Collection of User's.
+ */
+public class UserListWriter
 {
     /**
      * Write the Map of User entries as XML.
@@ -87,8 +90,8 @@ public class UsersWriter
      * @param writer            The Writer to output to.
      * @throws IOException      Any writing errors.
      */
-    public static void write(final Map<String, PersonalDetails> users,
-                             final Writer writer) throws IOException
+    public void write(final Map<String, PersonalDetails> users,
+                      final Writer writer) throws IOException
     {
         // Create the root users Element.
         final Element usersElement = new Element("users");
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 e9b957c29ed85fcc8133c781a2b8618e15650116..0accd9da28358bd855b9b73c0d10ce8265044872 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
@@ -85,6 +85,9 @@ 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
 {
     /**
@@ -96,7 +99,7 @@ public class UserReader
      * @throws java.io.IOException
      * @throws java.net.URISyntaxException
      */
-    public static User<Principal> read(String xml)
+    public User<Principal> read(String xml)
         throws IOException, URISyntaxException
     {
         if (xml == null)
@@ -113,7 +116,7 @@ public class UserReader
      * @return User User.
      * @throws java.io.IOException
      */
-    public static User<Principal> read(InputStream in)
+    public User<Principal> read(InputStream in)
         throws IOException
     {
         if (in == null)
@@ -140,7 +143,7 @@ public class UserReader
      * @throws ReaderException
      * @throws java.io.IOException
      */
-    public static User<Principal> read(Reader reader)
+    public User<Principal> read(Reader reader)
         throws IOException
     {
         if (reader == null)
@@ -166,7 +169,7 @@ public class UserReader
         return parseUser(root);
     }
 
-    protected static User<Principal> parseUser(Element userElement)
+    public static User<Principal> parseUser(Element userElement)
         throws ReaderException
     {
         // userID element of the User element
@@ -185,7 +188,8 @@ public class UserReader
             throw new ReaderException(error);
         }
 
-        Principal userID = IdentityReader.read(userIDIdentityElement);
+        IdentityReader identityReader = new IdentityReader();
+        Principal userID = identityReader.read(userIDIdentityElement);
 
         User<Principal> user = new User<Principal>(userID);
 
@@ -196,7 +200,7 @@ public class UserReader
             List<Element> identityElements = identitiesElement.getChildren("identity");
             for (Element identityElement : identityElements)
             {
-                user.getIdentities().add(IdentityReader.read(identityElement));
+                user.getIdentities().add(identityReader.read(identityElement));
             }
 
         }
@@ -205,10 +209,11 @@ public class UserReader
         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));
+                user.details.add(userDetailsReader.read(userDetailsElement));
             }
         }
 
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 da30cafda3aa6e952fa5eec3682318f9543b5555..e44eba1e4dc37fe8f9fa81b0f7b9629bf271e65e 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
@@ -84,6 +84,9 @@ 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
 {
     /**
@@ -93,7 +96,7 @@ public class UserRequestReader
      * @return UserRequest UserRequest.
      * @throws java.io.IOException
      */
-    public static UserRequest<Principal> read(String xml)
+    public UserRequest<Principal> read(String xml)
         throws IOException
     {
         if (xml == null)
@@ -111,7 +114,7 @@ public class UserRequestReader
      * @throws ReaderException
      * @throws java.io.IOException
      */
-    public static UserRequest<Principal> read(InputStream in)
+    public UserRequest<Principal> read(InputStream in)
         throws IOException
     {
         if (in == null)
@@ -138,7 +141,7 @@ public class UserRequestReader
      * @throws ReaderException
      * @throws java.io.IOException
      */
-    public static UserRequest<Principal> read(Reader reader)
+    public UserRequest<Principal> read(Reader reader)
         throws IOException
     {
         if (reader == null)
@@ -186,6 +189,6 @@ public class UserRequestReader
         }
         String password = passwordElement.getText();
 
-        return new UserRequest<Principal>(user, password);
+        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 10aaf852a0fd05e5add47518984e50ce132856cc..fe0ffee96a6fbbd1a9e9a70f77d575dfbdf0a629 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
@@ -81,6 +81,9 @@ import java.io.IOException;
 import java.io.Writer;
 import java.security.Principal;
 
+/**
+ * Class to write a XML representation of a UserRequest object.
+ */
 public class UserRequestWriter
 {
     /**
@@ -91,7 +94,7 @@ public class UserRequestWriter
      * @throws java.io.IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(UserRequest<? extends Principal> userRequest, StringBuilder builder)
+    public void write(UserRequest<? extends Principal> userRequest, StringBuilder builder)
         throws IOException, WriterException
     {
         write(userRequest, new StringBuilderWriter(builder));
@@ -135,7 +138,7 @@ public class UserRequestWriter
 
         // password element
         Element passwordElement = new Element("password");
-        passwordElement.setText(userRequest.getPassword());
+        passwordElement.setText(String.valueOf(userRequest.getPassword()));
         userRequestElement.addContent(passwordElement);
 
         return userRequestElement;
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 b1d6642de691ebc47e3516054b08bdb0a640976a..86327ad8f27e7e77bfaa4b17291fe4d8830e2fbf 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
@@ -86,6 +86,9 @@ 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
 {
     /**
@@ -96,7 +99,7 @@ public class UserWriter
      * @throws java.io.IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(User<? extends Principal> user, StringBuilder builder)
+    public void write(User<? extends Principal> user, StringBuilder builder)
         throws IOException, WriterException
     {
         write(user, new StringBuilderWriter(builder));
@@ -110,7 +113,7 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(User<? extends Principal> user, OutputStream out)
+    public void write(User<? extends Principal> user, OutputStream out)
         throws IOException, WriterException
     {                
         OutputStreamWriter outWriter;
@@ -133,7 +136,7 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(User<? extends Principal> user, Writer writer)
+    public void write(User<? extends Principal> user, Writer writer)
         throws IOException, WriterException
     {
         if (user == null)
@@ -158,8 +161,9 @@ public class UserWriter
         Element userElement = new Element("user");
 
         // userID element
+        IdentityWriter identityWriter = new IdentityWriter();
         Element userIDElement = new Element("userID");
-        userIDElement.addContent(IdentityWriter.write(user.getUserID()));
+        userIDElement.addContent(identityWriter.write(user.getUserID()));
         userElement.addContent(userIDElement);
 
         // identities
@@ -169,7 +173,7 @@ public class UserWriter
             Element identitiesElement = new Element("identities");
             for (Principal identity : identities)
             {
-                identitiesElement.addContent(IdentityWriter.write(identity));
+                identitiesElement.addContent(identityWriter.write(identity));
             }
             userElement.addContent(identitiesElement);
         }
@@ -177,11 +181,12 @@ public class UserWriter
         // 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));
+                detailsElement.addContent(userDetailsWriter.write(userDetail));
             }
             userElement.addContent(detailsElement);
         }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserRequestTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserRequestTest.java
index 9507d25e5b83156d2627652c90560565c4d30ee3..7bc85ed92c3c65e1ddd07c0b4c543ea4e6790a02 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserRequestTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserRequestTest.java
@@ -83,7 +83,9 @@ public class UserRequestTest
     @Test
     public void simpleEqualityTests() throws Exception
     {
-        UserRequest<HttpPrincipal> ur1 = new UserRequest<HttpPrincipal>(new User(new HttpPrincipal(("foo"))), "password");
+        UserRequest<HttpPrincipal> ur1 =
+            new UserRequest<HttpPrincipal>(
+                new User<HttpPrincipal>(new HttpPrincipal(("foo"))), "password".toCharArray());
         UserRequest<HttpPrincipal> ur2 = ur1;
         assertEquals(ur1, ur2);
         assertEquals(ur1.getUser(), ur2.getUser());
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapperTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JsonUserListInputStreamWrapperTest.java
similarity index 96%
rename from projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapperTest.java
rename to projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JsonUserListInputStreamWrapperTest.java
index fe977e4e3e5120c17b8e407df7bc210767384099..d8bd34ea6a1aae47bc8b0bd4b7182f3838932329 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapperTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JsonUserListInputStreamWrapperTest.java
@@ -80,15 +80,15 @@ import org.junit.Test;
 import static org.junit.Assert.*;
 
 
-public class JSONUserListInputStreamWrapperTest
+public class JsonUserListInputStreamWrapperTest
 {
     @Test
     public void readInputStream() throws Exception
     {
         final List<User<HttpPrincipal>> output =
                 new ArrayList<User<HttpPrincipal>>();
-        final JSONUserListInputStreamWrapper testSubject =
-                new JSONUserListInputStreamWrapper(output);
+        final JsonUserListInputStreamWrapper testSubject =
+                new JsonUserListInputStreamWrapper(output);
         final InputStream inputStream =
                 new ByteArrayInputStream("[{\"id\":\"CADCTest\",\"firstName\":\"CADCtest\",\"lastName\":\"USER\"}\n,{\"id\":\"User_2\",\"firstName\":\"User\",\"lastName\":\"2\"}]".getBytes());
 
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/GroupReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonGroupReaderWriterTest.java
similarity index 74%
rename from projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/GroupReaderWriterTest.java
rename to projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonGroupReaderWriterTest.java
index b60eee86191be9b62deda3174b51b9a1fe55a5e3..1de1fab5bf1cf8acc3e633644719c1204c55fb2f 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/GroupReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonGroupReaderWriterTest.java
@@ -70,11 +70,16 @@ package ca.nrc.cadc.ac.json;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupProperty;
+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.OpenIdPrincipal;
+import ca.nrc.cadc.util.Log4jInit;
+import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import javax.security.auth.x500.X500Principal;
@@ -93,9 +98,15 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class GroupReaderWriterTest
+public class JsonGroupReaderWriterTest
 {
-    private static Logger log = Logger.getLogger(GroupReaderWriterTest.class);
+    private static Logger log = Logger.getLogger(JsonGroupReaderWriterTest.class);
+
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac.json", Level.INFO);
+    }
 
     @Test
     public void testReaderExceptions()
@@ -104,7 +115,8 @@ public class GroupReaderWriterTest
         try
         {
             String s = null;
-            Group g = GroupReader.read(s);
+            JsonGroupReader reader = new JsonGroupReader();
+            Group g = reader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -112,7 +124,8 @@ public class GroupReaderWriterTest
         try
         {
             InputStream in = null;
-            Group g = GroupReader.read(in);
+            JsonGroupReader reader = new JsonGroupReader();
+            Group g = reader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -120,7 +133,8 @@ public class GroupReaderWriterTest
         try
         {
             Reader r = null;
-            Group g = GroupReader.read(r);
+            JsonGroupReader reader = new JsonGroupReader();
+            Group g = reader.read(r);
             fail("null element should throw ReaderException");
         }
         catch (IllegalArgumentException e) {}
@@ -132,7 +146,8 @@ public class GroupReaderWriterTest
     {
         try
         {
-            GroupWriter.write(null, new StringBuilder());
+            JsonGroupWriter writer = new JsonGroupWriter();
+            writer.write(null, new StringBuilder());
             fail("null Group should throw WriterException");
         }
         catch (WriterException e) {}
@@ -142,41 +157,63 @@ public class GroupReaderWriterTest
     public void testMinimalReadWrite()
         throws Exception
     {
-        Group expected = new Group("groupID", null);
+        Group expected = new Group("groupID");
                 
         StringBuilder json = new StringBuilder();
-        GroupWriter.write(expected, json);
+        JsonGroupWriter writer = new JsonGroupWriter();
+        writer.write(expected, json);
         assertFalse(json.toString().isEmpty());
-        
-        Group actual = GroupReader.read(json.toString());
+
+        JsonGroupReader reader = new JsonGroupReader();
+        Group actual = reader.read(json.toString());
         assertNotNull(actual);
         assertEquals(expected, actual);
     }
-    
+
     @Test
     public void testMaximalReadWrite()
         throws Exception
     {
-        Group expected = new Group("groupID", new User<Principal>(new HttpPrincipal("foo")));
+        User<Principal> owner = new User<Principal>(new HttpPrincipal("foo"));
+        X500Principal x500Principal = new X500Principal("cn=foo,o=bar");
+        owner.getIdentities().add(x500Principal);
+        PersonalDetails personalDetails = new PersonalDetails("foo", "bar");
+        personalDetails.address = "address";
+        personalDetails.email = "email";
+        personalDetails.institute = "institute";
+        personalDetails.city = "city";
+        personalDetails.country = "country";
+        owner.details.add(personalDetails);
+        PosixDetails posixDetails = new PosixDetails(123L, 456L, "/dev/null");
+        owner.details.add(posixDetails);
+
+        Group expected = new Group("groupID", owner);
         expected.description = "description";
         expected.lastModified = new Date();
-        expected.getProperties().add(new GroupProperty("key", "value", true));
-        
+        expected.getProperties().add(new GroupProperty("key1", "value1", true));
+        expected.getProperties().add(new GroupProperty("key2", "value2", true));
+        expected.getProperties().add(new GroupProperty("key3", "value3", true));
+
         Group groupMember = new Group("member", new User<Principal>(new OpenIdPrincipal("bar")));
         User<Principal> userMember = new User<Principal>(new HttpPrincipal("baz"));
         Group groupAdmin = new Group("admin", new User<Principal>(new X500Principal("cn=foo,o=ca")));
         User<Principal> userAdmin = new User<Principal>(new HttpPrincipal("admin"));
-        
+
         expected.getGroupMembers().add(groupMember);
         expected.getUserMembers().add(userMember);
         expected.getGroupAdmins().add(groupAdmin);
         expected.getUserAdmins().add(userAdmin);
-        
-        StringBuilder json = new StringBuilder();
-        GroupWriter.write(expected, json);
-        assertFalse(json.toString().isEmpty());
 
-        Group actual = GroupReader.read(json.toString());
+        JsonGroupWriter writer = new JsonGroupWriter();
+        StringBuilder sb = new StringBuilder();
+        writer.write(expected, sb);
+
+        String json = sb.toString();
+        log.debug(json);
+
+        JsonGroupReader reader = new JsonGroupReader();
+        Group actual = reader.read(json);
+
         assertNotNull(actual);
         assertEquals(expected, actual);
         assertEquals(expected.description, actual.description);
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/UserReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
similarity index 87%
rename from projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/UserReaderWriterTest.java
rename to projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
index 6e468da861196bcd48c20c419b02ffa21bb0c684..dccefe894c2a5b2a3addf25e226cee910061ea74 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/UserReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
@@ -91,9 +91,9 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class UserReaderWriterTest
+public class JsonUserReaderWriterTest
 {
-    private static Logger log = Logger.getLogger(UserReaderWriterTest.class);
+    private static Logger log = Logger.getLogger(JsonUserReaderWriterTest.class);
 
     @Test
     public void testReaderExceptions()
@@ -102,7 +102,8 @@ public class UserReaderWriterTest
         try
         {
             String s = null;
-            User<? extends Principal> u = UserReader.read(s);
+            JsonUserReader reader = new JsonUserReader();
+            User<? extends Principal> u = reader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -110,7 +111,8 @@ public class UserReaderWriterTest
         try
         {
             InputStream in = null;
-            User<? extends Principal> u = UserReader.read(in);
+            JsonUserReader reader = new JsonUserReader();
+            User<? extends Principal> u = reader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -118,7 +120,8 @@ public class UserReaderWriterTest
         try
         {
             Reader r = null;
-            User<? extends Principal> u = UserReader.read(r);
+            JsonUserReader reader = new JsonUserReader();
+            User<? extends Principal> u = reader.read(r);
             fail("null Reader should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -130,7 +133,8 @@ public class UserReaderWriterTest
     {
         try
         {
-            UserWriter.write(null, new StringBuilder());
+            JsonUserWriter writer = new JsonUserWriter();
+            writer.write(null, new StringBuilder());
             fail("null User should throw WriterException");
         }
         catch (WriterException e) {}
@@ -140,16 +144,18 @@ 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"));
         expected.details.add(new PosixDetails(123l, 456l, "foo"));
 
         StringBuilder json = new StringBuilder();
-        UserWriter.write(expected, json);
+        JsonUserWriter writer = new JsonUserWriter();
+        writer.write(expected, json);
         assertFalse(json.toString().isEmpty());
-        
-        User<? extends Principal> actual = UserReader.read(json.toString());
+
+        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/JsonUserRequestReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserRequestReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd4f6e715dd2a66723ed7a7d612806813b2dea00
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserRequestReaderWriterTest.java
@@ -0,0 +1,157 @@
+/*
+ ************************************************************************
+ *******************  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.PersonalDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+
+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 JsonUserRequestReaderWriterTest
+{
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            JsonUserRequestReader reader = new JsonUserRequestReader();
+            UserRequest u = reader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+
+        try
+        {
+            InputStream in = null;
+            JsonUserRequestReader reader = new JsonUserRequestReader();
+            UserRequest u = reader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+
+        try
+        {
+            Reader r = null;
+            JsonUserRequestReader reader = new JsonUserRequestReader();
+            UserRequest u = reader.read(r);
+            fail("null Reader should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            JsonUserRequestWriter writer = new JsonUserRequestWriter();
+            writer.write(null, new StringBuilder());
+            fail("null User should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+
+    @Test
+    public void testReadWrite()
+        throws Exception
+    {
+        User<Principal> expectedUser =
+            new User<Principal>(new HttpPrincipal("CADCtest"));
+        expectedUser.details.add(new PersonalDetails("CADCtest", "User"));
+
+        UserRequest<Principal> expected =
+            new UserRequest<Principal>(expectedUser, "MYPASSWORD".toCharArray());
+
+        StringBuilder json = new StringBuilder();
+        JsonUserRequestWriter writer = new JsonUserRequestWriter();
+        writer.write(expected, json);
+        assertFalse(json.toString().isEmpty());
+
+        JsonUserRequestReader reader = new JsonUserRequestReader();
+        UserRequest<Principal> actual = reader.read(json.toString());
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+    }
+
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupReaderWriterTest.java
index 12f3f5093d5f35c4b98aa0d6bcdaea8b31125477..6aab545506a02e5be27e23a648289c284eef17a5 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupReaderWriterTest.java
@@ -70,6 +70,8 @@ package ca.nrc.cadc.ac.xml;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupProperty;
+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;
@@ -104,7 +106,8 @@ public class GroupReaderWriterTest
         try
         {
             String s = null;
-            Group g = GroupReader.read(s);
+            GroupReader groupReader = new GroupReader();
+            Group g = groupReader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -112,7 +115,8 @@ public class GroupReaderWriterTest
         try
         {
             InputStream in = null;
-            Group g = GroupReader.read(in);
+            GroupReader groupReader = new GroupReader();
+            Group g = groupReader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -120,7 +124,8 @@ public class GroupReaderWriterTest
         try
         {
             Reader r = null;
-            Group g = GroupReader.read(r);
+            GroupReader groupReader = new GroupReader();
+            Group g = groupReader.read(r);
             fail("null element should throw ReaderException");
         }
         catch (IllegalArgumentException e) {}
@@ -132,7 +137,8 @@ public class GroupReaderWriterTest
     {
         try
         {
-            GroupWriter.write(null, new StringBuilder());
+            GroupWriter groupWriter = new GroupWriter();
+            groupWriter.write(null, new StringBuilder());
             fail("null Group should throw WriterException");
         }
         catch (WriterException e) {}
@@ -145,10 +151,12 @@ public class GroupReaderWriterTest
         Group expected = new Group("groupID", null);
                 
         StringBuilder xml = new StringBuilder();
-        GroupWriter.write(expected, xml);
+        GroupWriter groupWriter = new GroupWriter();
+        groupWriter.write(expected, xml);
         assertFalse(xml.toString().isEmpty());
-        
-        Group actual = GroupReader.read(xml.toString());
+
+        GroupReader groupReader = new GroupReader();
+        Group actual = groupReader.read(xml.toString());
         assertNotNull(actual);
         assertEquals(expected, actual);
     }
@@ -157,10 +165,24 @@ public class GroupReaderWriterTest
     public void testMaximalReadWrite()
         throws Exception
     {
-        Group expected = new Group("groupID", new User<Principal>(new HttpPrincipal("foo")));
+        User<Principal> owner = new User<Principal>(new HttpPrincipal("foo"));
+        X500Principal x500Principal = new X500Principal("cn=foo,o=bar");
+        owner.getIdentities().add(x500Principal);
+        PersonalDetails personalDetails = new PersonalDetails("foo", "bar");
+        personalDetails.address = "address";
+        personalDetails.email = "email";
+        personalDetails.institute = "institute";
+        personalDetails.city = "city";
+        personalDetails.country = "country";
+        owner.details.add(personalDetails);
+        PosixDetails posixDetails = new PosixDetails(123L, 456L, "/dev/null");
+        owner.details.add(posixDetails);
+
+        Group expected = new Group("groupID", owner);
         expected.description = "description";
         expected.lastModified = new Date();
-        expected.getProperties().add(new GroupProperty("key", "value", true));
+        expected.getProperties().add(new GroupProperty("key1", "value1", true));
+        expected.getProperties().add(new GroupProperty("key2", "value2", false));
         
         Group groupMember = new Group("member", new User<Principal>(new OpenIdPrincipal("bar")));
         User<Principal> userMember = new User<Principal>(new HttpPrincipal("baz"));
@@ -173,10 +195,12 @@ public class GroupReaderWriterTest
         expected.getUserAdmins().add(userAdmin);
         
         StringBuilder xml = new StringBuilder();
-        GroupWriter.write(expected, xml);
+        GroupWriter groupWriter = new GroupWriter();
+        groupWriter.write(expected, xml);
         assertFalse(xml.toString().isEmpty());
-        
-        Group actual = GroupReader.read(xml.toString());
+
+        GroupReader groupReader = new GroupReader();
+        Group actual = groupReader.read(xml.toString());
         assertNotNull(actual);
         assertEquals(expected, actual);
         assertEquals(expected.description, actual.description);
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java
index 1d4d9f3052d68324ad7db6e332215c985108c9aa..0281eaf87cf1456c0d5781fc187012d3d5c7caa2 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java
@@ -99,7 +99,8 @@ public class GroupsReaderWriterTest
         try
         {
             String s = null;
-            List<Group> g = GroupsReader.read(s);
+            GroupListReader groupListReader = new GroupListReader();
+            List<Group> g = groupListReader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -107,7 +108,8 @@ public class GroupsReaderWriterTest
         try
         {
             InputStream in = null;
-            List<Group> g = GroupsReader.read(in);
+            GroupListReader groupListReader = new GroupListReader();
+            List<Group> g = groupListReader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -115,7 +117,8 @@ public class GroupsReaderWriterTest
         try
         {
             Reader r = null;
-            List<Group> g = GroupsReader.read(r);
+            GroupListReader groupListReader = new GroupListReader();
+            List<Group> g = groupListReader.read(r);
             fail("null element should throw ReaderException");
         }
         catch (IllegalArgumentException e) {}
@@ -127,7 +130,8 @@ public class GroupsReaderWriterTest
     {
         try
         {
-            GroupsWriter.write(null, new StringBuilder());
+            GroupListWriter groupListWriter = new GroupListWriter();
+            groupListWriter.write(null, new StringBuilder());
             fail("null Group should throw WriterException");
         }
         catch (WriterException e) {}
@@ -142,10 +146,12 @@ public class GroupsReaderWriterTest
         expected.add(new Group("group2", null));
         
         StringBuilder xml = new StringBuilder();
-        GroupsWriter.write(expected, xml);
+        GroupListWriter groupListWriter = new GroupListWriter();
+        groupListWriter.write(expected, xml);
         assertFalse(xml.toString().isEmpty());
-        
-        List<Group> actual = GroupsReader.read(xml.toString());
+
+        GroupListReader groupListReader = new GroupListReader();
+        List<Group> actual = groupListReader.read(xml.toString());
         assertNotNull(actual);
         assertEquals(expected.size(), actual.size());
         assertEquals(expected.get(0), actual.get(0));
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 a0e3d54881d83f310dbc616884dcf37277066cd6..4e8d9f2b066ef053fb815854ee2a3ce03c66e30c 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
@@ -101,7 +101,8 @@ public class UserReaderWriterTest
         try
         {
             String s = null;
-            User<? extends Principal> u = UserReader.read(s);
+            UserReader userReader = new UserReader();
+            User<? extends Principal> u = userReader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -109,7 +110,8 @@ public class UserReaderWriterTest
         try
         {
             InputStream in = null;
-            User<? extends Principal> u = UserReader.read(in);
+            UserReader userReader = new UserReader();
+            User<? extends Principal> u = userReader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -117,7 +119,8 @@ public class UserReaderWriterTest
         try
         {
             Reader r = null;
-            User<? extends Principal> u = UserReader.read(r);
+            UserReader userReader = new UserReader();
+            User<? extends Principal> u = userReader.read(r);
             fail("null Reader should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -129,7 +132,8 @@ public class UserReaderWriterTest
     {
         try
         {
-            ca.nrc.cadc.ac.xml.UserWriter.write(null, new StringBuilder());
+            UserWriter userWriter = new UserWriter();
+            userWriter.write(null, new StringBuilder());
             fail("null User should throw WriterException");
         }
         catch (WriterException e) {}
@@ -144,10 +148,12 @@ public class UserReaderWriterTest
         expected.details.add(new PersonalDetails("firstname", "lastname"));
         
         StringBuilder xml = new StringBuilder();
-        UserWriter.write(expected, xml);
+        UserWriter userWriter = new UserWriter();
+        userWriter.write(expected, xml);
         assertFalse(xml.toString().isEmpty());
-        
-        User<? extends Principal> actual = UserReader.read(xml.toString());
+
+        UserReader userReader = new UserReader();
+        User<? extends Principal> actual = userReader.read(xml.toString());
         assertNotNull(actual);
         assertEquals(expected, actual);
     }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserRequestReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserRequestReaderWriterTest.java
index 191fb9b603131b691659c843aa11b25303309d14..30dec9315f46556d62e8193380f68452e29b4a88 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserRequestReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserRequestReaderWriterTest.java
@@ -99,7 +99,8 @@ public class UserRequestReaderWriterTest
         try
         {
             String s = null;
-            UserRequest u = UserRequestReader.read(s);
+            UserRequestReader userRequestReader = new UserRequestReader();
+            UserRequest u = userRequestReader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -107,7 +108,8 @@ public class UserRequestReaderWriterTest
         try
         {
             InputStream in = null;
-            UserRequest u = UserRequestReader.read(in);
+            UserRequestReader userRequestReader = new UserRequestReader();
+            UserRequest u = userRequestReader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -115,7 +117,8 @@ public class UserRequestReaderWriterTest
         try
         {
             Reader r = null;
-            UserRequest u = UserRequestReader.read(r);
+            UserRequestReader userRequestReader = new UserRequestReader();
+            UserRequest u = userRequestReader.read(r);
             fail("null Reader should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -127,7 +130,8 @@ public class UserRequestReaderWriterTest
     {
         try
         {
-            UserRequestWriter.write(null, new StringBuilder());
+            UserRequestWriter userRequestWriter = new UserRequestWriter();
+            userRequestWriter.write(null, new StringBuilder());
             fail("null UserRequest should throw WriterException");
         }
         catch (WriterException e) {}
@@ -137,22 +141,26 @@ public class UserRequestReaderWriterTest
     public void testReadWrite()
         throws Exception
     {
-        User<? extends Principal> expectedUser = new User<Principal>(new HttpPrincipal("foo"));
+        User<HttpPrincipal> expectedUser = new User<HttpPrincipal>(new HttpPrincipal("foo"));
         expectedUser.getIdentities().add(new NumericPrincipal(123l));
         expectedUser.details.add(new PersonalDetails("firstname", "lastname"));
 
-        String expectedPassword = "123456";
+        char[] expectedPassword = "123456".toCharArray();
 
-        UserRequest expected = new UserRequest(expectedUser, expectedPassword);
+        UserRequest<HttpPrincipal> expected =
+            new UserRequest<HttpPrincipal>(expectedUser, expectedPassword);
 
         StringBuilder xml = new StringBuilder();
-        UserRequestWriter.write(expected, xml);
+        UserRequestWriter userRequestWriter = new UserRequestWriter();
+        userRequestWriter.write(expected, xml);
         assertFalse(xml.toString().isEmpty());
-        
-        UserRequest actual = UserRequestReader.read(xml.toString());
+
+        UserRequestReader userRequestReader = new UserRequestReader();
+        UserRequest actual = userRequestReader.read(xml.toString());
         assertNotNull(actual);
         assertEquals(expected.getUser(), actual.getUser());
-        assertEquals(expected.getPassword(), actual.getPassword());
+        assertEquals(String.valueOf(expected.getPassword()),
+                     String.valueOf(actual.getPassword()));
     }
     
 }