diff --git a/projects/cadcAccessControl-Server/build.xml b/projects/cadcAccessControl-Server/build.xml
index 8a87c3bca324f82a7393879f24f870c304c20d78..b5ad9f91fe28e1e3606f3b6ab652d333b722271e 100644
--- a/projects/cadcAccessControl-Server/build.xml
+++ b/projects/cadcAccessControl-Server/build.xml
@@ -148,7 +148,7 @@
         <pathelement path="${jars}:${testingJars}"/>
       </classpath>
       <sysproperty key="ca.nrc.cadc.util.PropertiesReader.dir" value="test"/>
-      <test name="ca.nrc.cadc.ac.server.web.users.GetUserActionTest" />
+      <test name="ca.nrc.cadc.ac.server.web.users.UserActionFactoryTest" />
       <formatter type="plain" usefile="false" />
     </junit>
   </target>
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
index f88faffeb2851833b99c606de1f63e317703960f..070080806602418dc3d0b5cacfc8849e375b0869 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
@@ -68,13 +68,15 @@
  */
 package ca.nrc.cadc.ac.server;
 
-import java.security.AccessControlException;
-import java.security.Principal;
-import java.util.Map;
-
-import ca.nrc.cadc.ac.*;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserAlreadyExistsException;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.net.TransientException;
 
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.Collection;
 
 public interface UserPersistence<T extends Principal>
 {
@@ -85,7 +87,7 @@ public interface UserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    Map<String, PersonalDetails> getUsers()
+    Collection<User<Principal>> getUsers()
             throws TransientException, AccessControlException;
     
     /**
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
index 219728d8a32b0ae1eb1e05296c04bb87ae825d2c..5c09a99f4af62927120a657d5a612a11c2b6b4e6 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
@@ -150,8 +150,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
     private String[] userAttribs = new String[]
             {
                     LDAP_FIRST_NAME, LDAP_LAST_NAME, LDAP_ADDRESS, LDAP_CITY,
-                    LDAP_COUNTRY,
-                    LDAP_EMAIL, LDAP_INSTITUTE
+                    LDAP_COUNTRY, LDAP_EMAIL, LDAP_INSTITUTE
             };
     private String[] memberAttribs = new String[]
             {
@@ -539,11 +538,10 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @return A map of string keys to string values.
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
-    public Map<String, PersonalDetails> getUsers()
+    public Collection<User<Principal>> getUsers()
             throws TransientException
     {
-        final Map<String, PersonalDetails> users =
-                new HashMap<String, PersonalDetails>();
+        final Collection<User<Principal>> users = new ArrayList<User<Principal>>();
 
         try
         {
@@ -566,16 +564,15 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
                 {
                     if (!next.hasAttribute(LDAP_NSACCOUNTLOCK))
                     {
-                        final String trimmedFirstName =
+                        final String firstName =
                                 next.getAttributeValue(LDAP_FIRST_NAME).trim();
-                        final String trimmedLastName =
+                        final String lastName =
                                 next.getAttributeValue(LDAP_LAST_NAME).trim();
-                        final String trimmedUID =
-                                next.getAttributeValue(LDAP_UID).trim();
-
-                        users.put(trimmedUID,
-                                  new PersonalDetails(trimmedFirstName,
-                                                      trimmedLastName));
+                        final String uid =  next.getAttributeValue(LDAP_UID).trim();
+                        User<Principal> user = new User<Principal>(new HttpPrincipal(uid));
+                        PersonalDetails pd = new PersonalDetails(firstName, lastName);
+                        user.details.add(pd);
+                        users.add(user);
                     }
                 }
             }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
index 6a042cc3785193c354f28626a785e803c8190f88..fd8b41ec1abb427b629fc026f53eda34b767937a 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
@@ -75,6 +75,7 @@ import com.unboundid.ldap.sdk.DN;
 import java.security.AccessControlException;
 import java.security.Principal;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.log4j.Logger;
@@ -96,8 +97,8 @@ public class LdapUserPersistence<T extends Principal>
             logger.error("test/config/LdapConfig.properties file required.", e);
         }
     }
-    
-    public Map<String, PersonalDetails> getUsers()
+
+    public Collection<User<Principal>> getUsers()
         throws TransientException, AccessControlException
     {
         LdapUserDAO<T> userDAO = null;
@@ -125,7 +126,6 @@ public class LdapUserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    @Override
     public User<T> addUser(UserRequest<T> user)
         throws TransientException, AccessControlException,
                UserAlreadyExistsException
@@ -183,7 +183,6 @@ public class LdapUserPersistence<T extends Principal>
     * @throws TransientException     If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     */
-    @Override
     public User<T> getPendingUser(final T userID) throws UserNotFoundException,
                                                          TransientException,
                                                          AccessControlException
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
index 2805eda54b4e7bebfe5f0fc719c84085651f6dfd..9a93cfecf3ead83a4ac28fe764ce58c7823470fc 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AbstractGroupAction.java
@@ -68,19 +68,6 @@
  */
 package ca.nrc.cadc.ac.server.web.groups;
 
-import java.io.IOException;
-import java.security.AccessControlException;
-import java.security.Principal;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.List;
-
-import javax.security.auth.Subject;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.log4j.Logger;
-
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
 import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.MemberAlreadyExistsException;
@@ -91,6 +78,15 @@ import ca.nrc.cadc.ac.server.PluginFactory;
 import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.ac.server.web.SyncOutput;
 import ca.nrc.cadc.net.TransientException;
+import org.apache.log4j.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.List;
 
 public abstract class AbstractGroupAction implements PrivilegedExceptionAction<Object>
 {
@@ -208,18 +204,18 @@ public abstract class AbstractGroupAction implements PrivilegedExceptionAction<O
     private void sendError(int responseCode, String message)
     {
         syncOut.setHeader("Content-Type", "text/plain");
+        syncOut.setCode(responseCode);
         if (message != null)
         {
             try
             {
-                syncOut.getWriter() .write(message);
+                syncOut.getWriter().write(message);
             }
             catch (IOException e)
             {
                 log.warn("Could not write error message to output stream");
             }
         }
-        syncOut.setCode(responseCode);
     }
 
     <T extends Principal> GroupPersistence<T> getGroupPersistence()
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
index a4341c40aa82975b1aebf363794f30dc1a00d95c..5afabdb07782eeda62a83d49f68efb1ab6595ff6 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
@@ -99,7 +99,8 @@ public class CreateGroupAction extends AbstractGroupAction
         groupWriter.write(newGroup, syncOut.getWriter());
 
         List<String> addedMembers = null;
-        if ((newGroup.getUserMembers().size() > 0) || (newGroup.getGroupMembers().size() > 0))
+        if ((newGroup.getUserMembers().size() > 0) ||
+            (newGroup.getGroupMembers().size() > 0))
         {
             addedMembers = new ArrayList<String>();
             for (Group gr : newGroup.getGroupMembers())
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
index 07a83782a467cf199f1106d3a21df5481fa67561..0a7266ca4ac83cee0232619b1ef5500b7d99051e 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/AbstractUserAction.java
@@ -94,7 +94,9 @@ import java.io.Writer;
 import java.security.AccessControlException;
 import java.security.Principal;
 import java.security.PrivilegedExceptionAction;
+import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -324,7 +326,7 @@ public abstract class AbstractUserAction implements PrivilegedExceptionAction<Ob
      *
      * @param users         The Map of user IDs to names.
      */
-    protected final void writeUsers(final Map<String, PersonalDetails> users)
+    protected final <T extends Principal> void writeUsers(final Collection<User<T>> users)
             throws IOException
     {
         syncOut.setHeader("Content-Type", acceptedContentType);
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
index 8e3a6db4ccc8259169fbbb50232317d7e0fa9e86..0624272f497c63f2e8d502f43d482d43dc67c614 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUserListActionTest.java
@@ -70,6 +70,7 @@ package ca.nrc.cadc.ac.server.web.users;
 
 
 import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.json.JsonUserListWriter;
 import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.ac.server.web.SyncOutput;
@@ -85,7 +86,10 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.io.Writer;
+import java.security.Principal;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.easymock.EasyMock.*;
@@ -115,13 +119,14 @@ public class GetUserListActionTest
                 createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
-        final Map<String, PersonalDetails> userEntries =
-                new HashMap<String, PersonalDetails>();
+        List<User<Principal>> expectedUsers = new ArrayList<User<Principal>>();
 
         for (int i = 1; i <= 5; i++)
         {
-            userEntries.put("USER_" + i,
-                            new PersonalDetails("USER", Integer.toString(i)));
+            User<Principal> user = new User<Principal>(new HttpPrincipal("USER_" + i));
+            PersonalDetails pd = new PersonalDetails("USER", Integer.toString(i));
+            user.details.add(pd);
+            expectedUsers.add(user);
         }
 
         final GetUserListAction testSubject = new GetUserListAction()
@@ -138,8 +143,7 @@ public class GetUserListActionTest
         final Writer actualWriter = new StringWriter();
         final PrintWriter actualPrintWriter = new PrintWriter(actualWriter);
 
-        expect(mockUserPersistence.getUsers()).andReturn(
-                userEntries).once();
+        expect(mockUserPersistence.getUsers()).andReturn(expectedUsers).once();
         expect(mockSyncOut.getWriter()).andReturn(actualPrintWriter).once();
         mockSyncOut.setHeader("Content-Type", "application/json");
         expectLastCall().once();
@@ -153,7 +157,7 @@ public class GetUserListActionTest
         final Writer expectedWriter = new StringWriter();
         final PrintWriter expectedPrintWriter = new PrintWriter(expectedWriter);
         JsonUserListWriter userListWriter = new JsonUserListWriter();
-        userListWriter.write(userEntries, expectedPrintWriter);
+        userListWriter.write(expectedUsers, expectedPrintWriter);
         JSONAssert.assertEquals(expectedWriter.toString(), actualWriter.toString(), false);
 
         verify(mockSyncOut, mockUserPersistence);
@@ -167,13 +171,14 @@ public class GetUserListActionTest
                 createMock(SyncOutput.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
-        final Map<String, PersonalDetails> userEntries =
-                new HashMap<String, PersonalDetails>();
+        List<User<Principal>> expectedUsers = new ArrayList<User<Principal>>();
 
         for (int i = 1; i <= 5; i++)
         {
-            userEntries.put("USER_" + i,
-                            new PersonalDetails("USER", Integer.toString(i)));
+            User<Principal> user = new User<Principal>(new HttpPrincipal("USER_" + i));
+            PersonalDetails pd = new PersonalDetails("USER", Integer.toString(i));
+            user.details.add(pd);
+            expectedUsers.add(user);
         }
 
         final GetUserListAction testSubject = new GetUserListAction()
@@ -188,8 +193,7 @@ public class GetUserListActionTest
         final Writer actualWriter = new StringWriter();
         final PrintWriter actualPrintWriter = new PrintWriter(actualWriter);
 
-        expect(mockUserPersistence.getUsers()).andReturn(
-                userEntries).once();
+        expect(mockUserPersistence.getUsers()).andReturn(expectedUsers).once();
         expect(mockSyncOut.getWriter()).andReturn(actualPrintWriter).once();
         mockSyncOut.setHeader("Content-Type", "text/xml");
         expectLastCall().once();
@@ -203,7 +207,7 @@ public class GetUserListActionTest
         final Writer expectedWriter = new StringWriter();
         final PrintWriter expectedPrintWriter = new PrintWriter(expectedWriter);
         UserListWriter userListWriter = new UserListWriter();
-        userListWriter.write(userEntries, expectedPrintWriter);
+        userListWriter.write(expectedUsers, expectedPrintWriter);
         assertEquals("Wrong XML", expectedWriter.toString(), actualWriter.toString());
 
         verify(mockSyncOut, mockUserPersistence);
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
index bad17d1f5f80cee819082265ca7f344940ac14b1..d2d2661a16a8d2327f63310f168ab8e0b9434a8b 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
@@ -105,6 +105,7 @@ import java.security.AccessControlException;
 import java.security.AccessController;
 import java.security.Principal;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
index a240184be0794193897d8924ce1002747b5ecb4c..e5997162863ce13b0f560bb579d4b7e492d8b851 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupReader.java
@@ -76,37 +76,15 @@ import org.jdom2.Document;
 import org.json.JSONException;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.Reader;
 import java.util.Scanner;
 
+/**
+ * Class to read an JSON representation of a list of Groups
+ * into a list of Group objects.
+ */
 public class JsonGroupReader extends GroupReader
 {
-    /**
-     * Construct a Group from a InputStream.
-     *
-     * @param in InputStream.
-     * @return Group Group.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public Group read(InputStream in)
-        throws ReaderException, IOException
-    {
-        if (in == null)
-        {
-            throw new IOException("stream closed");
-        }
-        InputStreamReader reader;
-
-        Scanner s = new Scanner(in).useDelimiter("\\A");
-        String json = s.hasNext() ? s.next() : "";
-
-        return read(json);
-    }
-
     /**
      * Construct a Group from a Reader.
      *
@@ -127,27 +105,6 @@ public class JsonGroupReader extends GroupReader
         Scanner s = new Scanner(reader).useDelimiter("\\A");
         String json = s.hasNext() ? s.next() : "";
 
-        return read(json);
-    }
-
-    /**
-     * Construct a Group from an JSON String source.
-     *
-     * @param json String of JSON.
-     * @return Group Group.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public Group read(String json)
-        throws ReaderException, IOException
-    {
-        if (json == null)
-        {
-            throw new IllegalArgumentException("JSON must not be null");
-        }
-
-        // Create a JSONObject from the JSON
         try
         {
             JsonInputter jsonInputter = new JsonInputter();
@@ -160,12 +117,12 @@ public class JsonGroupReader extends GroupReader
             jsonInputter.getListElementMap().put("userAdmins", "user");
 
             Document document = jsonInputter.input(json);
-            return GroupReader.parseGroup(document.getRootElement());
+            return getGroup(document.getRootElement());
         }
         catch (JSONException e)
         {
             String error = "Unable to parse JSON to Group because " +
-                           e.getMessage();
+                e.getMessage();
             throw new ReaderException(error, e);
         }
     }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java
index 2db229327ce52306744846b95f0a4ffb19f310ce..864e14df31befef998f29f0cfaafaca5ecc4216b 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonGroupWriter.java
@@ -71,57 +71,18 @@ package ca.nrc.cadc.ac.json;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.ac.xml.GroupWriter;
-import ca.nrc.cadc.util.StringBuilderWriter;
 import ca.nrc.cadc.xml.JsonOutputter;
 import org.jdom2.Document;
 import org.jdom2.Element;
 
-import java.io.BufferedWriter;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 
+/**
+ * Class to write a JSON representation of a Group object.
+ */
 public class JsonGroupWriter extends GroupWriter
 {
-    /**
-     * Write a Group to a StringBuilder.
-     * @param group
-     * @param builder
-     * @throws IOException
-     * @throws WriterException
-     */
-    public void write(Group group, StringBuilder builder)
-        throws IOException, WriterException
-    {
-        write(group, new StringBuilderWriter(builder));
-    }
-
-    /**
-     * Write a Group to an OutputStream.
-     *
-     * @param group Group to write.
-     * @param out OutputStream to write to.
-     * @throws IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    @Override
-    public void write(Group group, OutputStream out)
-        throws IOException, WriterException
-    {
-        OutputStreamWriter outWriter;
-        try
-        {
-            outWriter = new OutputStreamWriter(out, "UTF-8");
-        }
-        catch (UnsupportedEncodingException e)
-        {
-            throw new RuntimeException("UTF-8 encoding not supported", e);
-        }
-        write(group, new BufferedWriter(outWriter));
-    }
-
     /**
      * Write a Group to a Writer.
      *
@@ -139,7 +100,7 @@ public class JsonGroupWriter extends GroupWriter
             throw new WriterException("null group");
         }
 
-        Element children = GroupWriter.getGroupElement(group);
+        Element children = getElement(group);
         Element groupElement = new Element("group");
         groupElement.addContent(children);
         Document document = new Document();
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e63adedabde2c2c9a704f5b05dc5c2c1f791a2f
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListReader.java
@@ -0,0 +1,127 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2015.                            (c) 2015.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.json;
+
+import ca.nrc.cadc.ac.ReaderException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.xml.UserListReader;
+import ca.nrc.cadc.xml.JsonInputter;
+import org.jdom2.Document;
+import org.json.JSONException;
+
+import java.io.Reader;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * Class to read an JSON representation of a Collection of Users
+ * into a list of User objects.
+ */
+public class JsonUserListReader extends UserListReader
+{
+    /**
+     * Construct a list of Users from a Reader.
+     *
+     * @param reader Reader.
+     * @return users List of Users.
+     * @throws ReaderException
+     * @throws URISyntaxException
+     */
+    @Override
+    public List<User<Principal>> read(Reader reader)
+        throws URISyntaxException, ReaderException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        Scanner s = new Scanner(reader).useDelimiter("\\A");
+        String json = s.hasNext() ? s.next() : "";
+
+        try
+        {
+            JsonInputter jsonInputter = new JsonInputter();
+            jsonInputter.getListElementMap().put("identities", "identity");
+            jsonInputter.getListElementMap().put("details", "userDetails");
+
+            Document document = jsonInputter.input(json);
+            return getUserList(document.getRootElement());
+        }
+        catch (JSONException e)
+        {
+            String error = "Unable to parse JSON to list of Users because " +
+                e.getMessage();
+            throw new ReaderException(error, e);
+        }
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
index 5bf9317a44b6c30c61b50f569471f652dc875daa..9a8acad66057c4c6c16e8ab52c5baf29de49f175 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserListWriter.java
@@ -68,58 +68,54 @@
 
 package ca.nrc.cadc.ac.json;
 
-import ca.nrc.cadc.ac.PersonalDetails;
-import org.json.JSONException;
-import org.json.JSONWriter;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.ac.xml.UserListWriter;
+import ca.nrc.cadc.xml.JsonOutputter;
+import org.jdom2.Document;
+import org.jdom2.Element;
 
 import java.io.IOException;
 import java.io.Writer;
-import java.util.Map;
-
+import java.security.Principal;
+import java.util.Collection;
 
 /**
  * Class to write out, as JSON, a list of user entries.
  */
-public class JsonUserListWriter
+public class JsonUserListWriter extends UserListWriter
 {
-    public static void write(final Map<String, PersonalDetails> users,
-                             final Writer writer) throws IOException
+    /**
+     * Write a Collection of Users to a Writer.
+     *
+     * @param users  Users to write.
+     * @param writer Writer to write to.
+     * @throws IOException     if the writer fails to write.
+     * @throws WriterException
+     */
+    @Override
+    public <T extends Principal> void write(Collection<User<T>> users, Writer writer)
+        throws IOException, WriterException
     {
-        final JSONWriter jsonWriter = new JSONWriter(writer);
-
-        try
+        if (users == null)
         {
-            jsonWriter.array();
-
-            for (final Map.Entry<String, PersonalDetails> entry
-                    : users.entrySet())
-            {
-                jsonWriter.object();
-
-                jsonWriter.key("id").value(entry.getKey());
-                jsonWriter.key("firstName").value(entry.getValue().
-                        getFirstName());
-                jsonWriter.key("lastName").value(entry.getValue().
-                        getLastName());
-
-                jsonWriter.endObject();
-                writer.write("\n");
-            }
+            throw new WriterException("null users");
         }
-        catch (JSONException e)
-        {
-            throw new IOException(e);
-        }
-        finally
+
+        Element usersElement = new Element("users");
+        for (User<? extends Principal> user : users)
         {
-            try
-            {
-                jsonWriter.endArray();
-            }
-            catch (JSONException e)
-            {
-                // Do nothing.
-            }
+            Element userElement = new Element(("user"));
+            userElement.addContent(getElement(user));
         }
+        Document document = new Document();
+        document.setRootElement(usersElement);
+
+        JsonOutputter jsonOutputter = new JsonOutputter();
+        jsonOutputter.getListElementNames().add("identities");
+        jsonOutputter.getListElementNames().add("details");
+
+        jsonOutputter.output(document, writer);
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
index 2c4a8e376ec338c5fa2157d08767ca79f8999d5d..4ef9ee9afa59ff7ea34631b2f569fcc5c718644f 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserReader.java
@@ -76,36 +76,15 @@ import org.jdom2.Document;
 import org.json.JSONException;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.Reader;
 import java.security.Principal;
 import java.util.Scanner;
 
+/**
+ * Class to read a JSON representation of a User to a User object.
+ */
 public class JsonUserReader extends UserReader
 {
-    /**
-     * Construct a User from a InputStream.
-     *
-     * @param in InputStream.
-     * @return User User.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public User<Principal> read(InputStream in)
-        throws IOException
-    {
-        if (in == null)
-        {
-            throw new IOException("stream closed");
-        }
-
-        Scanner s = new Scanner(in).useDelimiter("\\A");
-        String json = s.hasNext() ? s.next() : "";
-
-        return read(json);
-    }
-
     /**
      * Construct a User from a Reader.
      *
@@ -126,27 +105,6 @@ public class JsonUserReader extends UserReader
         Scanner s = new Scanner(reader).useDelimiter("\\A");
         String json = s.hasNext() ? s.next() : "";
 
-        return read(json);
-    }
-
-    /**
-     * Construct a User from an JSON String source.
-     *
-     * @param json String of JSON.
-     * @return User User.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public User<Principal> read(String json)
-        throws IOException
-    {
-        if (json == null || json.isEmpty())
-        {
-            throw new IllegalArgumentException("JSON must not be null or empty");
-        }
-
-        // Create a JSONObject from the JSON
         try
         {
             JsonInputter jsonInputter = new JsonInputter();
@@ -154,7 +112,7 @@ public class JsonUserReader extends UserReader
             jsonInputter.getListElementMap().put("details", "userDetails");
 
             Document document = jsonInputter.input(json);
-            return UserReader.parseUser(document.getRootElement());
+            return getUser(document.getRootElement());
         }
         catch (JSONException e)
         {
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
index b8d61e8cea9567a2caac1beb562766a5e05b1bfa..eb03dbb01e2655358798886e44be0d896b867d9e 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestReader.java
@@ -76,36 +76,15 @@ import org.jdom2.Document;
 import org.json.JSONException;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.Reader;
 import java.security.Principal;
 import java.util.Scanner;
 
+/**
+ * Class to read a JSON representation of a UserRequest to a UserRequest object.
+ */
 public class JsonUserRequestReader extends UserRequestReader
 {
-    /**
-     * Construct a User from a InputStream.
-     *
-     * @param in InputStream.
-     * @return User User.
-     * @throws ReaderException
-     * @throws IOException
-     */
-    @Override
-    public UserRequest<Principal> read(InputStream in)
-            throws IOException
-    {
-        if (in == null)
-        {
-            throw new IOException("stream closed");
-        }
-
-        Scanner s = new Scanner(in).useDelimiter("\\A");
-        String json = s.hasNext() ? s.next() : "";
-
-        return read(json);
-    }
-
     /**
      * Construct a User from a Reader.
      *
@@ -126,41 +105,20 @@ public class JsonUserRequestReader extends UserRequestReader
         Scanner s = new Scanner(reader).useDelimiter("\\A");
         String json = s.hasNext() ? s.next() : "";
 
-        return read(json);
-    }
-
-    /**
-     * Construct a UserRequest from an JSON String source.
-     *
-     * @param json String of the JSON.
-     * @return UserRequest UserRequest.
-     * @throws IOException
-     */
-    @Override
-    public UserRequest<Principal> read(String json)
-        throws IOException
-    {
-        if (json == null)
+        try
         {
-            throw new IllegalArgumentException("JSON must not be null");
+            JsonInputter jsonInputter = new JsonInputter();
+            jsonInputter.getListElementMap().put("identities", "identity");
+            jsonInputter.getListElementMap().put("details", "userDetails");
+
+            Document document = jsonInputter.input(json);
+            return getUserRequest(document.getRootElement());
         }
-        else
+        catch (JSONException e)
         {
-            try
-            {
-                JsonInputter jsonInputter = new JsonInputter();
-                jsonInputter.getListElementMap().put("identities", "identity");
-                jsonInputter.getListElementMap().put("details", "userDetails");
-
-                Document document = jsonInputter.input(json);
-                return UserRequestReader.parseUserRequest(document.getRootElement());
-            }
-            catch (JSONException e)
-            {
-                String error = "Unable to parse JSON to User because " +
-                    e.getMessage();
-                throw new ReaderException(error, e);
-            }
+            String error = "Unable to parse JSON to User because " +
+                e.getMessage();
+            throw new ReaderException(error, e);
         }
     }
 
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
index 7ebb07a652a9b972252d528e869ac86938d33c12..6cf6e6f742b009f319f25371126cbe777de768da 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserRequestWriter.java
@@ -1,9 +1,77 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  Government of Canada                 Gouvernement du Canada
+*  National Research Council            Conseil national de recherches
+*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+*  All rights reserved                  Tous droits réservés
+*
+*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+*  expressed, implied, or               énoncée, implicite ou légale,
+*  statutory, of any kind with          de quelque nature que ce
+*  respect to the software,             soit, concernant le logiciel,
+*  including without limitation         y compris sans restriction
+*  any warranty of merchantability      toute garantie de valeur
+*  or fitness for a particular          marchande ou de pertinence
+*  purpose. NRC shall not be            pour un usage particulier.
+*  liable in any event for any          Le CNRC ne pourra en aucun cas
+*  damages, whether direct or           être tenu responsable de tout
+*  indirect, special or general,        dommage, direct ou indirect,
+*  consequential or incidental,         particulier ou général,
+*  arising from the use of the          accessoire ou fortuit, résultant
+*  software.  Neither the name          de l'utilisation du logiciel. Ni
+*  of the National Research             le nom du Conseil National de
+*  Council of Canada nor the            Recherches du Canada ni les noms
+*  names of its contributors may        de ses  participants ne peuvent
+*  be used to endorse or promote        être utilisés pour approuver ou
+*  products derived from this           promouvoir les produits dérivés
+*  software without specific prior      de ce logiciel sans autorisation
+*  written permission.                  préalable et particulière
+*                                       par écrit.
+*
+*  This file is part of the             Ce fichier fait partie du projet
+*  OpenCADC project.                    OpenCADC.
+*
+*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+*  you can redistribute it and/or       vous pouvez le redistribuer ou le
+*  modify it under the terms of         modifier suivant les termes de
+*  the GNU Affero General Public        la “GNU Affero General Public
+*  License as published by the          License” telle que publiée
+*  Free Software Foundation,            par la Free Software Foundation
+*  either version 3 of the              : soit la version 3 de cette
+*  License, or (at your option)         licence, soit (à votre gré)
+*  any later version.                   toute version ultérieure.
+*
+*  OpenCADC is distributed in the       OpenCADC est distribué
+*  hope that it will be useful,         dans l’espoir qu’il vous
+*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+*  without even the implied             GARANTIE : sans même la garantie
+*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+*  General Public License for           Générale Publique GNU Affero
+*  more details.                        pour plus de détails.
+*
+*  You should have received             Vous devriez avoir reçu une
+*  a copy of the GNU Affero             copie de la Licence Générale
+*  General Public License along         Publique GNU Affero avec
+*  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+*                                       <http://www.gnu.org/licenses/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
 package ca.nrc.cadc.ac.json;
 
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.ac.xml.UserRequestWriter;
-import ca.nrc.cadc.util.StringBuilderWriter;
 import ca.nrc.cadc.xml.JsonOutputter;
 import org.jdom2.Document;
 import org.jdom2.Element;
@@ -17,20 +85,6 @@ import java.security.Principal;
  */
 public class JsonUserRequestWriter extends UserRequestWriter
 {
-    /**
-     * Write a UserRequest to a StringBuilder.
-     *
-     * @param userRequest UserRequest to write.
-     * @param builder StringBuilder to write to.
-     * @throws java.io.IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    public void write(UserRequest<? extends Principal> userRequest, StringBuilder builder)
-        throws IOException, WriterException
-    {
-        write(userRequest, new StringBuilderWriter(builder));
-    }
-
     /**
      * Write a UserRequest to a Writer.
      *
@@ -39,7 +93,8 @@ public class JsonUserRequestWriter extends UserRequestWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(UserRequest<? extends Principal> userRequest, Writer writer)
+    @Override
+    public <T extends Principal> void write(UserRequest<T> userRequest, Writer writer)
         throws IOException, WriterException
     {
         if (userRequest == null)
@@ -47,7 +102,7 @@ public class JsonUserRequestWriter extends UserRequestWriter
             throw new WriterException("null UserRequest");
         }
 
-        Element children = UserRequestWriter.getUserRequestElement(userRequest);
+        Element children = getElement(userRequest);
         Element userRequestElement = new Element("userRequest");
         userRequestElement.addContent(children);
         Document document = new Document();
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
index 444a670a4453e0bd2e6b7f481fb9247e73df0525..3b9004ff24ae028ccd7b6027e2ce18b77394c902 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/JsonUserWriter.java
@@ -69,68 +69,21 @@
 package ca.nrc.cadc.ac.json;
 
 import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserDetails;
 import ca.nrc.cadc.ac.WriterException;
-import ca.nrc.cadc.ac.xml.GroupWriter;
 import ca.nrc.cadc.ac.xml.UserWriter;
-import ca.nrc.cadc.util.StringBuilderWriter;
 import ca.nrc.cadc.xml.JsonOutputter;
 import org.jdom2.Document;
 import org.jdom2.Element;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
 
-import java.io.BufferedWriter;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.security.Principal;
-import java.util.Set;
 
+/**
+ * Class to write a JSON representation of a User object.
+ */
 public class JsonUserWriter extends UserWriter
 {
-    /**
-     * Write a User as a JSON string to a StringBuilder.
-     * 
-     * @param user User to write.
-     * @param builder StringBuilder to write to.
-     * @throws IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    @Override
-    public void write(User<? extends Principal> user, StringBuilder builder)
-        throws IOException, WriterException
-    {
-        write(user, new StringBuilderWriter(builder));
-    }
-
-    /**
-     * Write a User as a JSON string to an OutputStream.
-     *
-     * @param user User to write.
-     * @param out OutputStream to write to.
-     * @throws IOException if the writer fails to write.
-     * @throws WriterException
-     */
-    @Override
-    public void write(User<? extends Principal> user, OutputStream out)
-        throws IOException, WriterException
-    {                
-        OutputStreamWriter outWriter;
-        try
-        {
-            outWriter = new OutputStreamWriter(out, "UTF-8");
-        }
-        catch (UnsupportedEncodingException e)
-        {
-            throw new RuntimeException("UTF-8 encoding not supported", e);
-        }
-        write(user, new BufferedWriter(outWriter));
-    }
-
     /**
      * Write a User as a JSON string to a Writer.
      *
@@ -140,7 +93,7 @@ public class JsonUserWriter extends UserWriter
      * @throws WriterException
      */
     @Override
-    public void write(User<? extends Principal> user, Writer writer)
+    public<T extends Principal> void write(User<T> user, Writer writer)
         throws IOException, WriterException
     {
         if (user == null)
@@ -148,7 +101,7 @@ public class JsonUserWriter extends UserWriter
             throw new WriterException("null User");
         }
 
-        Element children = UserWriter.getUserElement(user);
+        Element children = getElement(user);
         Element userElement = new Element("user");
         userElement.addContent(children);
         Document document = new Document();
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/AbstractXML.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/AbstractXML.java
new file mode 100644
index 0000000000000000000000000000000000000000..db221a9de08a4b7b7a8d7d81c02f4b98b8bced97
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/AbstractXML.java
@@ -0,0 +1,1018 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  Government of Canada                 Gouvernement du Canada
+*  National Research Council            Conseil national de recherches
+*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+*  All rights reserved                  Tous droits réservés
+*
+*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+*  expressed, implied, or               énoncée, implicite ou légale,
+*  statutory, of any kind with          de quelque nature que ce
+*  respect to the software,             soit, concernant le logiciel,
+*  including without limitation         y compris sans restriction
+*  any warranty of merchantability      toute garantie de valeur
+*  or fitness for a particular          marchande ou de pertinence
+*  purpose. NRC shall not be            pour un usage particulier.
+*  liable in any event for any          Le CNRC ne pourra en aucun cas
+*  damages, whether direct or           être tenu responsable de tout
+*  indirect, special or general,        dommage, direct ou indirect,
+*  consequential or incidental,         particulier ou général,
+*  arising from the use of the          accessoire ou fortuit, résultant
+*  software.  Neither the name          de l'utilisation du logiciel. Ni
+*  of the National Research             le nom du Conseil National de
+*  Council of Canada nor the            Recherches du Canada ni les noms
+*  names of its contributors may        de ses  participants ne peuvent
+*  be used to endorse or promote        être utilisés pour approuver ou
+*  products derived from this           promouvoir les produits dérivés
+*  software without specific prior      de ce logiciel sans autorisation
+*  written permission.                  préalable et particulière
+*                                       par écrit.
+*
+*  This file is part of the             Ce fichier fait partie du projet
+*  OpenCADC project.                    OpenCADC.
+*
+*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+*  you can redistribute it and/or       vous pouvez le redistribuer ou le
+*  modify it under the terms of         modifier suivant les termes de
+*  the GNU Affero General Public        la “GNU Affero General Public
+*  License as published by the          License” telle que publiée
+*  Free Software Foundation,            par la Free Software Foundation
+*  either version 3 of the              : soit la version 3 de cette
+*  License, or (at your option)         licence, soit (à votre gré)
+*  any later version.                   toute version ultérieure.
+*
+*  OpenCADC is distributed in the       OpenCADC est distribué
+*  hope that it will be useful,         dans l’espoir qu’il vous
+*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+*  without even the implied             GARANTIE : sans même la garantie
+*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+*  General Public License for           Générale Publique GNU Affero
+*  more details.                        pour plus de détails.
+*
+*  You should have received             Vous devriez avoir reçu une
+*  a copy of the GNU Affero             copie de la Licence Générale
+*  General Public License along         Publique GNU Affero avec
+*  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+*                                       <http://www.gnu.org/licenses/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
+package ca.nrc.cadc.ac.xml;
+
+import ca.nrc.cadc.ac.AC;
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupProperty;
+import ca.nrc.cadc.ac.IdentityType;
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.PosixDetails;
+import ca.nrc.cadc.ac.ReaderException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserDetails;
+import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+import ca.nrc.cadc.auth.OpenIdPrincipal;
+import ca.nrc.cadc.date.DateUtil;
+import org.jdom2.Attribute;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.Writer;
+import java.security.Principal;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * AbstractXML TODO describe class
+ */
+public abstract class AbstractXML
+{
+    /**
+     * Write to root Element to a writer.
+     *
+     * @param root Root Element to write.
+     * @param writer Writer to write to.
+     * @throws IOException if the writer fails to write.
+     */
+    protected void write(Element root, Writer writer)
+        throws IOException
+    {
+        XMLOutputter outputter = new XMLOutputter();
+        outputter.setFormat(Format.getPrettyFormat());
+        outputter.output(new Document(root), writer);
+    }
+
+    /**
+     * Get a UserRequest object from a JDOM element.
+     *
+     * @param element The UserRequest JDOM element.
+     * @return A UserRequest object.
+     * @throws ReaderException
+     */
+    protected final Group getGroup(Element element)
+        throws ReaderException
+    {
+        String uri = element.getAttributeValue("uri");
+        if (uri == null)
+        {
+            String error = "group missing required uri attribute";
+            throw new ReaderException(error);
+        }
+
+        // Group groupID
+        int index = uri.indexOf(AC.GROUP_URI);
+        if (index == -1)
+        {
+            String error = "group uri attribute malformed: " + uri;
+            throw new ReaderException(error);
+        }
+        String groupID = uri.substring(AC.GROUP_URI.length());
+
+        // Group owner
+        User<? extends Principal> user = null;
+        Element ownerElement = element.getChild("owner");
+        if (ownerElement != null)
+        {
+            // Owner user
+            Element userElement = ownerElement.getChild("user");
+            if (userElement == null)
+            {
+                String error = "owner missing required user element";
+                throw new ReaderException(error);
+            }
+            user = getUser(userElement);
+        }
+
+        Group group = new Group(groupID, user);
+
+        // description
+        Element descriptionElement = element.getChild("description");
+        if (descriptionElement != null)
+        {
+            group.description = descriptionElement.getText();
+        }
+
+        // lastModified
+        Element lastModifiedElement = element.getChild("lastModified");
+        if (lastModifiedElement != null)
+        {
+            try
+            {
+                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
+                group.lastModified = df.parse(lastModifiedElement.getText());
+            }
+            catch (ParseException e)
+            {
+                String error = "Unable to parse group lastModified because " + e.getMessage();
+
+                throw new ReaderException(error);
+            }
+        }
+
+        // properties
+        Element propertiesElement = element.getChild("properties");
+        if (propertiesElement != null)
+        {
+            List<Element> propertyElements = propertiesElement.getChildren("property");
+            for (Element propertyElement : propertyElements)
+            {
+                group.getProperties().add(getGroupProperty(propertyElement));
+            }
+        }
+
+        // groupMembers
+        Element groupMembersElement = element.getChild("groupMembers");
+        if (groupMembersElement != null)
+        {
+            List<Element> groupElements = groupMembersElement.getChildren("group");
+            for (Element groupMember : groupElements)
+            {
+                group.getGroupMembers().add(getGroup(groupMember));
+            }
+        }
+
+        // userMembers
+        Element userMembersElement = element.getChild("userMembers");
+        if (userMembersElement != null)
+        {
+            List<Element> userElements = userMembersElement.getChildren("user");
+            for (Element userMember : userElements)
+            {
+                group.getUserMembers().add(getUser(userMember));
+            }
+        }
+
+        // groupAdmins
+        Element groupAdminsElement = element.getChild("groupAdmins");
+        if (groupAdminsElement != null)
+        {
+            List<Element> groupElements = groupAdminsElement.getChildren("group");
+            for (Element groupMember : groupElements)
+            {
+                group.getGroupAdmins().add(getGroup(groupMember));
+            }
+        }
+
+        // userAdmins
+        Element userAdminsElement = element.getChild("userAdmins");
+        if (userAdminsElement != null)
+        {
+            List<Element> userElements = userAdminsElement.getChildren("user");
+            for (Element userMember : userElements)
+            {
+                group.getUserAdmins().add(getUser(userMember));
+            }
+        }
+
+        return group;
+    }
+
+    /**
+     * Get a JDOM element from a Group object.
+     *
+     * @param group The UserRequest.
+     * @return A JDOM Group representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(Group group)
+        throws WriterException
+    {
+        return getElement(group, true);
+    }
+
+    /**
+     * Get a JDOM element from a Group object.
+     *
+     * @param group The UserRequest.
+     * @param deepCopy Return all Group elements.
+     * @return A JDOM Group representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(Group group, boolean deepCopy)
+        throws WriterException
+    {
+        // Create the root group element.
+        Element groupElement = new Element("group");
+        String groupURI = AC.GROUP_URI + group.getID();
+        groupElement.setAttribute(new Attribute("uri", groupURI));
+
+        // Group owner
+        if (group.getOwner() != null)
+        {
+            Element ownerElement = new Element("owner");
+            Element userElement = getElement(group.getOwner());
+            ownerElement.addContent(userElement);
+            groupElement.addContent(ownerElement);
+        }
+
+        if (deepCopy)
+        {
+            // Group description
+            if (group.description != null)
+            {
+                Element descriptionElement = new Element("description");
+                descriptionElement.setText(group.description);
+                groupElement.addContent(descriptionElement);
+            }
+
+            // lastModified
+            if (group.lastModified != null)
+            {
+                Element lastModifiedElement = new Element("lastModified");
+                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
+                lastModifiedElement.setText(df.format(group.lastModified));
+                groupElement.addContent(lastModifiedElement);
+            }
+
+            // Group properties
+            if (!group.getProperties().isEmpty())
+            {
+                Element propertiesElement = new Element("properties");
+                for (GroupProperty property : group.getProperties())
+                {
+                    propertiesElement.addContent(getElement(property));
+                }
+                groupElement.addContent(propertiesElement);
+            }
+
+            // Group groupMembers.
+            if ((group.getGroupMembers() != null) && (!group.getGroupMembers().isEmpty()))
+            {
+                Element groupMembersElement = new Element("groupMembers");
+                for (Group groupMember : group.getGroupMembers())
+                {
+                    groupMembersElement.addContent(getElement(groupMember, false));
+                }
+                groupElement.addContent(groupMembersElement);
+            }
+
+            // Group userMembers
+            if ((group.getUserMembers() != null) && (!group.getUserMembers().isEmpty()))
+            {
+                Element userMembersElement = new Element("userMembers");
+                for (User<? extends Principal> userMember : group.getUserMembers())
+                {
+                    userMembersElement.addContent(getElement(userMember));
+                }
+                groupElement.addContent(userMembersElement);
+            }
+
+            // Group groupAdmins.
+            if ((group.getGroupAdmins() != null) && (!group.getGroupAdmins().isEmpty()))
+            {
+                Element groupAdminsElement = new Element("groupAdmins");
+                for (Group groupMember : group.getGroupAdmins())
+                {
+                    groupAdminsElement.addContent(getElement(groupMember, false));
+                }
+                groupElement.addContent(groupAdminsElement);
+            }
+
+            // Group userAdmins
+            if ((group.getUserAdmins() != null) && (!group.getUserAdmins().isEmpty()))
+            {
+                Element userAdminsElement = new Element("userAdmins");
+                for (User<? extends Principal> userMember : group.getUserAdmins())
+                {
+                    userAdminsElement.addContent(getElement(userMember));
+                }
+                groupElement.addContent(userAdminsElement);
+            }
+        }
+
+        return groupElement;
+    }
+
+    /**
+     * Get a UserRequest object from a JDOM element.
+     *
+     * @param element The UserRequest JDOM element.
+     * @return A UserRequest object.
+     * @throws ReaderException
+     */
+    protected final UserRequest<Principal> getUserRequest(Element element)
+        throws ReaderException
+    {
+        // user element of the UserRequest element
+        Element userElement = element.getChild("user");
+        if (userElement == null)
+        {
+            String error = "user element not found in userRequest element";
+            throw new ReaderException(error);
+        }
+        User<Principal> user = getUser(userElement);
+
+        // password element of the userRequest element
+        Element passwordElement = element.getChild("password");
+        if (passwordElement == null)
+        {
+            String error = "password element not found in userRequest element";
+            throw new ReaderException(error);
+        }
+        String password = passwordElement.getText();
+
+        return new UserRequest<Principal>(user, password.toCharArray());
+    }
+
+    /**
+     * Get a JDOM element from a UserRequest object.
+     *
+     * @param userRequest The UserRequest.
+     * @return A JDOM UserRequest representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(UserRequest<? extends Principal> userRequest)
+        throws WriterException
+    {
+        // Create the userRequest Element.
+        Element userRequestElement = new Element("userRequest");
+
+        // user element
+        Element userElement = getElement(userRequest.getUser());
+        userRequestElement.addContent(userElement);
+
+        // password element
+        Element passwordElement = new Element("password");
+        passwordElement.setText(String.valueOf(userRequest.getPassword()));
+        userRequestElement.addContent(passwordElement);
+
+        return userRequestElement;
+    }
+
+    /**
+     * Get a User object from a JDOM element.
+     *
+     * @param element The User JDOM element.
+     * @return A User object.
+     * @throws ReaderException
+     */
+    protected final User<Principal> getUser(Element element)
+        throws ReaderException
+    {
+        // userID element of the User element
+        Element userIDElement = element.getChild("userID");
+        if (userIDElement == null)
+        {
+            String error = "userID element not found in user element";
+            throw new ReaderException(error);
+        }
+
+        // identity element of the userID element
+        Element userIDIdentityElement = userIDElement.getChild("identity");
+        if (userIDIdentityElement == null)
+        {
+            String error = "identity element not found in userID element";
+            throw new ReaderException(error);
+        }
+
+        Principal userID = getPrincipal(userIDIdentityElement);
+        User<Principal> user = new User<Principal>(userID);
+
+        // identities
+        Element identitiesElement = element.getChild("identities");
+        if (identitiesElement != null)
+        {
+            List<Element> identityElements = identitiesElement.getChildren("identity");
+            for (Element identityElement : identityElements)
+            {
+                user.getIdentities().add(getPrincipal(identityElement));
+            }
+
+        }
+
+        // details
+        Element detailsElement = element.getChild("details");
+        if (detailsElement != null)
+        {
+            List<Element> userDetailsElements = detailsElement.getChildren("userDetails");
+            for (Element userDetailsElement : userDetailsElements)
+            {
+                user.details.add(getUserDetails(userDetailsElement));
+            }
+        }
+
+        return user;
+    }
+
+    /**
+     * Get a JDOM element from a User object.
+     *
+     * @param user The User.
+     * @return A JDOM User representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(User<? extends Principal> user)
+        throws WriterException
+    {
+        // Create the user Element.
+        Element userElement = new Element("user");
+
+        // userID element
+        Element userIDElement = new Element("userID");
+        userIDElement.addContent(getElement(user.getUserID()));
+        userElement.addContent(userIDElement);
+
+        // identities
+        Set<Principal> identities = user.getIdentities();
+        if (!identities.isEmpty())
+        {
+            Element identitiesElement = new Element("identities");
+            for (Principal identity : identities)
+            {
+                identitiesElement.addContent(getElement(identity));
+            }
+            userElement.addContent(identitiesElement);
+        }
+
+        // details
+        if (!user.details.isEmpty())
+        {
+            Element detailsElement = new Element("details");
+            Set<UserDetails> userDetails = user.details;
+            for (UserDetails userDetail : userDetails)
+            {
+                detailsElement.addContent(getElement(userDetail));
+            }
+            userElement.addContent(detailsElement);
+        }
+
+        return userElement;
+    }
+
+    /**
+     * Get a GroupProperty object from a JDOM element.
+     *
+     * @param element The GroupProperty JDOM element.
+     * @return A GroupProperty object.
+     * @throws ReaderException
+     */
+    protected final GroupProperty getGroupProperty(Element element)
+        throws ReaderException
+    {
+        if (element == null)
+        {
+            String error = "null property element";
+            throw new ReaderException(error);
+        }
+
+        if (!element.getName().equals(GroupProperty.NAME))
+        {
+            String error = "expected property element name, found " +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String key = element.getAttributeValue(GroupProperty.KEY_ATTRIBUTE);
+        if (key == null)
+        {
+            String error = "required key attribute not found";
+            throw new ReaderException(error);
+        }
+
+        String type = element.getAttributeValue(GroupProperty.TYPE_ATTRIBUTE);
+        if (type == null)
+        {
+            String error = "required type attribute not found";
+            throw new ReaderException(error);
+        }
+        Object value;
+        if (type.equals(GroupProperty.STRING_TYPE))
+        {
+            value = String.valueOf(element.getText());
+        }
+        else
+        {
+            if (type.equals(GroupProperty.INTEGER_TYPE))
+            {
+                value = Integer.valueOf(element.getText());
+            }
+            else
+            {
+                String error = "Unsupported GroupProperty type: " + type;
+                throw new ReaderException(error);
+            }
+        }
+        Boolean readOnly = Boolean.valueOf(element.getAttributeValue(GroupProperty.READONLY_ATTRIBUTE));
+
+        return new GroupProperty(key, value, readOnly);
+    }
+
+    /**
+     * Get a JDOM element from a GroupProperty object.
+     *
+     * @param property The GroupProperty.
+     * @return A JDOM GroupProperty representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(GroupProperty property)
+        throws WriterException
+    {
+        if (property == null)
+        {
+            throw new WriterException("null GroupProperty");
+        }
+
+        Element propertyElement = new Element(GroupProperty.NAME);
+        propertyElement.setAttribute(GroupProperty.KEY_ATTRIBUTE,
+            property.getKey());
+        if (property.isReadOnly())
+        {
+            propertyElement.setAttribute(GroupProperty.READONLY_ATTRIBUTE,
+                "true");
+        }
+
+        Object value = property.getValue();
+        if ((value instanceof String))
+        {
+            propertyElement.setAttribute(GroupProperty.TYPE_ATTRIBUTE,
+                GroupProperty.STRING_TYPE);
+        }
+        else if ((value instanceof Integer))
+        {
+            propertyElement.setAttribute(GroupProperty.TYPE_ATTRIBUTE,
+                GroupProperty.INTEGER_TYPE);
+        }
+        else
+        {
+            String error = "Unsupported value type: " +
+                value.getClass().getSimpleName();
+            throw new IllegalArgumentException(error);
+        }
+        propertyElement.setText(String.valueOf(property.getValue()));
+
+        return propertyElement;
+    }
+
+    /**
+     * Get a Principal object from a JDOM element.
+     *
+     * @param element The Principal JDOM element.
+     * @return A Principal object.
+     * @throws ReaderException
+     */
+    protected final Principal getPrincipal(Element element)
+        throws ReaderException
+    {
+        if (element == null)
+        {
+            String error = "null identity element";
+            throw new ReaderException(error);
+        }
+
+        if (!element.getName().equals("identity"))
+        {
+            String error = "expected identity element name, found " +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String type = element.getAttributeValue("type");
+        if (type == null)
+        {
+            String error = "type attribute not found in identity element" +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String identity = element.getText();
+        Principal principal;
+        if (type.equals(IdentityType.OPENID.getValue()))
+        {
+            principal = new OpenIdPrincipal(identity);
+        }
+        else if (type.equals(IdentityType.UID.getValue()))
+        {
+            Integer cadcID;
+            try
+            {
+                cadcID = Integer.valueOf(identity);
+            }
+            catch (NumberFormatException e)
+            {
+                String error = "Non-integer cadcID: " + identity;
+                throw new ReaderException(error);
+            }
+            principal = new NumericPrincipal(cadcID);
+        }
+        else if (type.equals(IdentityType.USERNAME.getValue()))
+        {
+            principal = new HttpPrincipal(identity);
+        }
+        else if (type.equals(IdentityType.X500.getValue()))
+        {
+            principal = new X500Principal(identity);
+        }
+        else
+        {
+            String error = "Unknown type attribute: " + type;
+            throw new ReaderException(error);
+        }
+
+        return principal;
+    }
+
+    /**
+     * Get a JDOM element from a Principal object.
+     *
+     * @param identity The Principal.
+     * @return A JDOM UserDetails representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(Principal identity)
+        throws WriterException
+    {
+        if (identity == null)
+        {
+            String error = "null identity";
+            throw new WriterException(error);
+        }
+
+        Element identityElement = new Element("identity");
+        if ((identity instanceof HttpPrincipal))
+        {
+            identityElement.setAttribute("type", IdentityType.USERNAME.getValue());
+        }
+        else if ((identity instanceof NumericPrincipal))
+        {
+            identityElement.setAttribute("type", IdentityType.UID.getValue());
+        }
+        else if ((identity instanceof OpenIdPrincipal))
+        {
+            identityElement.setAttribute("type", IdentityType.OPENID.getValue());
+        }
+        else if ((identity instanceof X500Principal))
+        {
+            identityElement.setAttribute("type", IdentityType.X500.getValue());
+        }
+        else
+        {
+            String error = "Unsupported Principal type " +
+                identity.getClass().getSimpleName();
+            throw new IllegalArgumentException(error);
+        }
+        identityElement.setText(identity.getName());
+
+        return identityElement;
+    }
+
+    /**
+     * Get a UserDetails object from a JDOM element.
+     *
+     * @param element The UserDetails JDOM element.
+     * @return A UserDetails object.
+     * @throws ReaderException
+     */
+    protected final UserDetails getUserDetails(Element element)
+        throws ReaderException
+    {
+        if (element == null)
+        {
+            throw new ReaderException("null UserDetails");
+        }
+
+        if (!element.getName().equals(UserDetails.NAME))
+        {
+            String error = "expected element name userDetails, found " +
+                element.getName();
+            throw new ReaderException(error);
+        }
+
+        String type = element.getAttributeValue(UserDetails.TYPE_ATTRIBUTE);
+        if (type == null)
+        {
+            String error = "userDetails missing required attribute type";
+            throw new ReaderException(error);
+        }
+
+        if (type.equals(PosixDetails.NAME))
+        {
+            return getPosixDetails(element);
+        }
+        if (type.equals(PersonalDetails.NAME))
+        {
+            return getPersonalDetails(element);
+        }
+
+        String error = "Unknown UserDetails attribute type " + type;
+        throw new ReaderException(error);
+    }
+
+    /**
+     * Get a PosixDetails object from a JDOM element.
+     *
+     * @param element The PosixDetails JDOM element.
+     * @return A PosixDetails object.
+     * @throws ReaderException
+     */
+    protected final PosixDetails getPosixDetails(Element element)
+        throws ReaderException
+    {
+        // uid
+        Element uidElement = element.getChild(PosixDetails.UID);
+        if (uidElement == null)
+        {
+            String error = "posixDetails missing required element uid";
+            throw new ReaderException(error);
+        }
+        long uid;
+        try
+        {
+            uid = Long.valueOf(uidElement.getText());
+        }
+        catch (NumberFormatException e)
+        {
+            String error = "Cannot parse posixDetails uid to a long";
+            throw new ReaderException(error);
+        }
+
+        // gid
+        Element gidElement = element.getChild(PosixDetails.GID);
+        if (gidElement == null)
+        {
+            String error = "posixDetails missing required element gid";
+            throw new ReaderException(error);
+        }
+        long gid;
+        try
+        {
+            gid = Long.valueOf(gidElement.getText());
+        }
+        catch (NumberFormatException e)
+        {
+            String error = "Cannot parse posixDetails gid to a long";
+            throw new ReaderException(error);
+        }
+
+        // homeDirectory
+        Element homeDirElement = element.getChild(PosixDetails.HOME_DIRECTORY);
+        if (homeDirElement == null)
+        {
+            String error = "posixDetails missing required element homeDirectory";
+            throw new ReaderException(error);
+        }
+        String homeDirectory = homeDirElement.getText();
+
+        return new PosixDetails(uid, gid, homeDirectory);
+    }
+
+    /**
+     * Get a PersonalDetails object from a JDOM element.
+     *
+     * @param element The PersonalDetails JDOM element.
+     * @return A PersonalDetails object.
+     * @throws ReaderException
+     */
+    protected final PersonalDetails getPersonalDetails(Element element)
+        throws ReaderException
+    {
+        // firstName
+        Element firstNameElement = element.getChild(PersonalDetails.FIRSTNAME);
+        if (firstNameElement == null)
+        {
+            String error = "personalDetails missing required element firstName";
+            throw new ReaderException(error);
+        }
+        String firstName = firstNameElement.getText();
+
+        // lastName
+        Element lastNameElement = element.getChild(PersonalDetails.LASTNAME);
+        if (lastNameElement == null)
+        {
+            String error = "personalDetails missing required element lastName";
+            throw new ReaderException(error);
+        }
+        String lastName = lastNameElement.getText();
+
+        PersonalDetails details = new PersonalDetails(firstName, lastName);
+
+        // email
+        Element emailElement = element.getChild(PersonalDetails.EMAIL);
+        if (emailElement != null)
+        {
+            details.email = emailElement.getText();
+        }
+
+        // address
+        Element addressElement = element.getChild(PersonalDetails.ADDRESS);
+        if (addressElement != null)
+        {
+            details.address = addressElement.getText();
+        }
+
+        // institute
+        Element instituteElement = element.getChild(PersonalDetails.INSTITUTE);
+        if (instituteElement != null)
+        {
+            details.institute = instituteElement.getText();
+        }
+
+        // city
+        Element cityElement = element.getChild(PersonalDetails.CITY);
+        if (cityElement != null)
+        {
+            details.city = cityElement.getText();
+        }
+
+        // country
+        Element countryElement = element.getChild(PersonalDetails.COUNTRY);
+        if (countryElement != null)
+        {
+            details.country = countryElement.getText();
+        }
+
+        return details;
+    }
+
+    /**
+     * Get a JDOM element from a UserDetails object.
+     *
+     * @param details The UserDetails.
+     * @return A JDOM UserDetails representation.
+     * @throws WriterException
+     */
+    protected final Element getElement(UserDetails details)
+        throws WriterException
+    {
+        if (details == null)
+        {
+            throw new WriterException("null UserDetails");
+        }
+
+        if ((details instanceof PosixDetails))
+        {
+            return getElement((PosixDetails) details);
+        }
+        if ((details instanceof PersonalDetails))
+        {
+            return getElement((PersonalDetails) details);
+        }
+
+        String error = "Unknown UserDetails implementation: " +
+            details.getClass().getName();
+        throw new WriterException(error);
+    }
+
+    /**
+     * Get a JDOM element from a PosixDetails object.
+     *
+     * @param details The PosixDetails.
+     * @return A JDOM PosixDetails representation.
+     */
+    protected final Element getElement(PosixDetails details)
+    {
+        Element detailsElement = new Element(UserDetails.NAME);
+        detailsElement.setAttribute(UserDetails.TYPE_ATTRIBUTE,
+            PosixDetails.NAME);
+
+        Element uidElement = new Element(PosixDetails.UID);
+        uidElement.setText(String.valueOf(details.getUid()));
+        detailsElement.addContent(uidElement);
+
+        Element gidElement = new Element(PosixDetails.GID);
+        gidElement.setText(String.valueOf(details.getGid()));
+        detailsElement.addContent(gidElement);
+
+        Element homeDirElement = new Element(PosixDetails.HOME_DIRECTORY);
+        homeDirElement.setText(details.getHomeDirectory());
+        detailsElement.addContent(homeDirElement);
+
+        return detailsElement;
+    }
+
+    /**
+     * Get a JDOM element from a PersonalDetails object.
+     *
+     * @param details The PersonalDetails.
+     * @return JDOM PersonalDetails representation.
+     */
+    protected final Element getElement(PersonalDetails details)
+    {
+        Element detailsElement = new Element(UserDetails.NAME);
+        detailsElement.setAttribute(UserDetails.TYPE_ATTRIBUTE,
+            PersonalDetails.NAME);
+
+        Element firstNameElement = new Element(PersonalDetails.FIRSTNAME);
+        firstNameElement.setText(details.getFirstName());
+        detailsElement.addContent(firstNameElement);
+
+        Element lastNameElement = new Element(PersonalDetails.LASTNAME);
+        lastNameElement.setText(details.getLastName());
+        detailsElement.addContent(lastNameElement);
+
+        if (details.email != null)
+        {
+            Element emailElement = new Element(PersonalDetails.EMAIL);
+            emailElement.setText(details.email);
+            detailsElement.addContent(emailElement);
+        }
+
+        if (details.address != null)
+        {
+            Element addressElement = new Element(PersonalDetails.ADDRESS);
+            addressElement.setText(details.address);
+            detailsElement.addContent(addressElement);
+        }
+
+        if (details.institute != null)
+        {
+            Element instituteElement = new Element(PersonalDetails.INSTITUTE);
+            instituteElement.setText(details.institute);
+            detailsElement.addContent(instituteElement);
+        }
+
+        if (details.city != null)
+        {
+            Element cityElement = new Element(PersonalDetails.CITY);
+            cityElement.setText(details.city);
+            detailsElement.addContent(cityElement);
+        }
+
+        if (details.country != null)
+        {
+            Element countryElement = new Element(PersonalDetails.COUNTRY);
+            countryElement.setText(details.country);
+            detailsElement.addContent(countryElement);
+        }
+
+        return detailsElement;
+    }
+
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
index cd962918e443f8d3db33cd7eead08d475285543d..10d46bbea732562d47648a2cd81a691d904b9a47 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListReader.java
@@ -71,6 +71,10 @@ package ca.nrc.cadc.ac.xml;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.ReaderException;
 import ca.nrc.cadc.xml.XmlUtil;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -80,15 +84,12 @@ import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.JDOMException;
 
 /**
- * Class to read an XML representation of a list of Groups
- * into a List of Group objects.
+ * Class to read an XML representation of a List of Groups
+ * into a Collection of Group objects.
  */
-public class GroupListReader
+public class GroupListReader extends AbstractXML
 {
     /**
      * Construct a list of Group's from an XML String source.
@@ -110,7 +111,7 @@ public class GroupListReader
     }
 
     /**
-     * Construct a list of Group's from a InputStream.
+     * Construct a List of Group's from a InputStream.
      * 
      * @param in InputStream.
      * @return Groups List of Group.
@@ -175,20 +176,29 @@ public class GroupListReader
             throw new ReaderException(error);
         }
 
-        return parseGroups(root);
+        return getGroupList(root);
     }
 
-    protected static List<Group> parseGroups(Element groupsElement)
-            throws URISyntaxException, ReaderException
-    {
+    /**
+     * Get a List of Groups from a JDOM element.
+     *
+     * @param element The Group's JDOM element.
+     * @return A List of Group objects.
+     * @throws URISyntaxException
+     * @throws ReaderException
+     */
+    protected final List<Group> getGroupList(Element element)
+        throws URISyntaxException, ReaderException
+    {;
         List<Group> groups = new ArrayList<Group>();
 
-        List<Element> groupElements = groupsElement.getChildren("group");
+        List<Element> groupElements = element.getChildren("group");
         for (Element groupElement : groupElements)
         {
-            groups.add(GroupReader.parseGroup(groupElement));
+            groups.add(getGroup(groupElement));
         }
 
         return groups;
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
index b3707fb3d7ce6bb3e2f3c16a6e9f59c01606b786..e406a4cd61a3c66c4d433879d5b7836f646ecc11 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupListWriter.java
@@ -1,8 +1,79 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  Government of Canada                 Gouvernement du Canada
+*  National Research Council            Conseil national de recherches
+*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+*  All rights reserved                  Tous droits réservés
+*
+*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+*  expressed, implied, or               énoncée, implicite ou légale,
+*  statutory, of any kind with          de quelque nature que ce
+*  respect to the software,             soit, concernant le logiciel,
+*  including without limitation         y compris sans restriction
+*  any warranty of merchantability      toute garantie de valeur
+*  or fitness for a particular          marchande ou de pertinence
+*  purpose. NRC shall not be            pour un usage particulier.
+*  liable in any event for any          Le CNRC ne pourra en aucun cas
+*  damages, whether direct or           être tenu responsable de tout
+*  indirect, special or general,        dommage, direct ou indirect,
+*  consequential or incidental,         particulier ou général,
+*  arising from the use of the          accessoire ou fortuit, résultant
+*  software.  Neither the name          de l'utilisation du logiciel. Ni
+*  of the National Research             le nom du Conseil National de
+*  Council of Canada nor the            Recherches du Canada ni les noms
+*  names of its contributors may        de ses  participants ne peuvent
+*  be used to endorse or promote        être utilisés pour approuver ou
+*  products derived from this           promouvoir les produits dérivés
+*  software without specific prior      de ce logiciel sans autorisation
+*  written permission.                  préalable et particulière
+*                                       par écrit.
+*
+*  This file is part of the             Ce fichier fait partie du projet
+*  OpenCADC project.                    OpenCADC.
+*
+*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+*  you can redistribute it and/or       vous pouvez le redistribuer ou le
+*  modify it under the terms of         modifier suivant les termes de
+*  the GNU Affero General Public        la “GNU Affero General Public
+*  License as published by the          License” telle que publiée
+*  Free Software Foundation,            par la Free Software Foundation
+*  either version 3 of the              : soit la version 3 de cette
+*  License, or (at your option)         licence, soit (à votre gré)
+*  any later version.                   toute version ultérieure.
+*
+*  OpenCADC is distributed in the       OpenCADC est distribué
+*  hope that it will be useful,         dans l’espoir qu’il vous
+*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+*  without even the implied             GARANTIE : sans même la garantie
+*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+*  General Public License for           Générale Publique GNU Affero
+*  more details.                        pour plus de détails.
+*
+*  You should have received             Vous devriez avoir reçu une
+*  a copy of the GNU Affero             copie de la Licence Générale
+*  General Public License along         Publique GNU Affero avec
+*  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+*                                       <http://www.gnu.org/licenses/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
 package ca.nrc.cadc.ac.xml;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.util.StringBuilderWriter;
+import org.jdom2.Element;
+
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -10,19 +81,15 @@ import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.util.Collection;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 /**
  * Class to write a XML representation from a Collection of Groups objects.
  */
-public class GroupListWriter
+public class GroupListWriter extends AbstractXML
 {
     /**
-     * Write a List of Group's to a StringBuilder.
-     * @param groups List of Group's to write.
+     * Write a Collection of Group's to a StringBuilder.
+     * @param groups Collection of Group's to write.
      * @param builder
      * @throws java.io.IOException
      * @throws WriterException
@@ -34,9 +101,9 @@ public class GroupListWriter
     }
 
     /**
-     * Write a List of Group's to an OutputStream.
+     * Write a Collection of Group's to an OutputStream.
      * 
-     * @param groups List of Group's to write.
+     * @param groups Collection of Group's to write.
      * @param out OutputStream to write to.
      * @throws IOException if the writer fails to write.
      * @throws WriterException
@@ -57,9 +124,9 @@ public class GroupListWriter
     }
 
     /**
-     * Write a List of Group's to a Writer.
+     * Write a Collection of Group's to a Writer.
      * 
-     * @param groups List of Group's to write.
+     * @param groups Collection of Group's to write.
      * @param writer  Writer to write to.
      * @throws IOException if the writer fails to write.
      * @throws WriterException
@@ -72,41 +139,27 @@ public class GroupListWriter
         throw new WriterException("null groups");
         }
 
-        write(getGroupsElement(groups), writer);
+        write(getElement(groups), writer);
     }
 
     /**
-     * 
-     * @param groups List of Group's to write.
-     * @return Element of list of Group's.
+     * Get a JDOM element from a Collection of Group objects.
+     *
+     * @param groups Collection of Group's to write.
+     * @return A JDOM Group list representation.
      * @throws WriterException
      */
-    public static Element getGroupsElement(Collection<Group> groups)
+    protected final Element getElement(Collection<Group> groups)
         throws WriterException
     {
         Element groupsElement = new Element("groups");
 
         for (Group group : groups)
         {
-            groupsElement.addContent(GroupWriter.getGroupElement(group));
+            groupsElement.addContent(getElement(group));
         }
 
         return groupsElement;
     }
 
-    /**
-     * Write to root Element to a writer.
-     * 
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
-    }
-    
 }
\ No newline at end of file
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
index 0f0d1dca1233d54a01c9cbb0da2ba9685fffc18c..31de44fcb7aa99aefc085b97db6b98ec139be433 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupReader.java
@@ -68,11 +68,8 @@
  */
 package ca.nrc.cadc.ac.xml;
 
-import ca.nrc.cadc.ac.AC;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.ReaderException;
-import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.date.DateUtil;
 import ca.nrc.cadc.xml.XmlUtil;
 import org.jdom2.Document;
 import org.jdom2.Element;
@@ -85,15 +82,11 @@ import java.io.Reader;
 import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
-import java.security.Principal;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.util.List;
 
 /**
  * Class to read a XML representation of a Group to a Group object.
  */
-public class GroupReader
+public class GroupReader extends AbstractXML
 {
 
     /**
@@ -181,128 +174,7 @@ public class GroupReader
             throw new ReaderException(error);
         }
 
-        return parseGroup(root);
+        return getGroup(root);
     }
 
-    public static Group parseGroup(Element groupElement)
-        throws ReaderException
-    {
-        String uri = groupElement.getAttributeValue("uri");
-        if (uri == null)
-        {
-            String error = "group missing required uri attribute";
-            throw new ReaderException(error);
-        }
-
-        // Group groupID
-        int index = uri.indexOf(AC.GROUP_URI);
-        if (index == -1)
-        {
-            String error = "group uri attribute malformed: " + uri;
-            throw new ReaderException(error);
-        }
-        String groupID = uri.substring(AC.GROUP_URI.length());
-
-        // Group owner
-        User<? extends Principal> user = null;
-        Element ownerElement = groupElement.getChild("owner");
-        if (ownerElement != null)
-        {
-            // Owner user
-            Element userElement = ownerElement.getChild("user");
-            if (userElement == null)
-            {
-                String error = "owner missing required user element";
-                throw new ReaderException(error);
-            }
-            user = UserReader.parseUser(userElement);
-        }
-
-        Group group = new Group(groupID, user);
-
-        // description
-        Element descriptionElement = groupElement.getChild("description");
-        if (descriptionElement != null)
-        {
-            group.description = descriptionElement.getText();
-        }
-
-        // lastModified
-        Element lastModifiedElement = groupElement.getChild("lastModified");
-        if (lastModifiedElement != null)
-        {
-            try
-            {
-                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
-                group.lastModified = df.parse(lastModifiedElement.getText());
-            }
-            catch (ParseException e)
-            {
-                String error = "Unable to parse group lastModified because " + e.getMessage();
-
-                throw new ReaderException(error);
-            }
-
-        }
-        
-        // properties
-        Element propertiesElement = groupElement.getChild("properties");
-        if (propertiesElement != null)
-        {
-            List<Element> propertyElements = propertiesElement.getChildren("property");
-            for (Element propertyElement : propertyElements)
-            {
-                group.getProperties().add(ca.nrc.cadc.ac.xml.GroupPropertyReader.read(propertyElement));
-            }
-
-        }
-
-        // groupMembers
-        Element groupMembersElement = groupElement.getChild("groupMembers");
-        if (groupMembersElement != null)
-        {
-            List<Element> groupElements = groupMembersElement.getChildren("group");
-            for (Element groupMember : groupElements)
-            {
-                group.getGroupMembers().add(parseGroup(groupMember));
-            }
-
-        }
-
-        // userMembers
-        Element userMembersElement = groupElement.getChild("userMembers");
-        if (userMembersElement != null)
-        {
-            List<Element> userElements = userMembersElement.getChildren("user");
-            for (Element userMember : userElements)
-            {
-                group.getUserMembers().add(UserReader.parseUser(userMember));
-            }
-        }
-        
-        // groupAdmins
-        Element groupAdminsElement = groupElement.getChild("groupAdmins");
-        if (groupAdminsElement != null)
-        {
-            List<Element> groupElements = groupAdminsElement.getChildren("group");
-            for (Element groupMember : groupElements)
-            {
-                group.getGroupAdmins().add(parseGroup(groupMember));
-            }
-
-        }
-
-        // userAdmins
-        Element userAdminsElement = groupElement.getChild("userAdmins");
-        if (userAdminsElement != null)
-        {
-            List<Element> userElements = userAdminsElement.getChildren("user");
-            for (Element userMember : userElements)
-            {
-                group.getUserAdmins().add(UserReader.parseUser(userMember));
-            }
-        }
-
-        return group;
-    }
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
index 380ba79a8d7ec2fa8e203f8e7f6d8bafbb51ff0e..54eba4f2d390d170d0efd7c299b304cf5c3dc43b 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/GroupWriter.java
@@ -68,18 +68,9 @@
  */
 package ca.nrc.cadc.ac.xml;
 
-import ca.nrc.cadc.ac.AC;
 import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.GroupProperty;
-import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.WriterException;
-import ca.nrc.cadc.date.DateUtil;
 import ca.nrc.cadc.util.StringBuilderWriter;
-import org.jdom2.Attribute;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -87,13 +78,11 @@ import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
-import java.security.Principal;
-import java.text.DateFormat;
 
 /**
  * Class to write a XML representation of a Group object.
  */
-public class GroupWriter
+public class GroupWriter extends AbstractXML
 {
     /**
      * Write a Group to a StringBuilder.
@@ -147,129 +136,7 @@ public class GroupWriter
             throw new WriterException("null group");
         }
 
-        write(getGroupElement(group), writer);
+        write(getElement(group), writer);
     }
 
-    /**
-     * 
-     * @param group
-     * @return 
-     * @throws WriterException
-     */
-    public static Element getGroupElement(Group group)
-        throws WriterException
-    {
-        return getGroupElement(group, true);
-    }
-
-    public static Element getGroupElement(Group group, boolean deepCopy)
-        throws WriterException
-    {
-        // Create the root group element.
-        Element groupElement = new Element("group");
-        String groupURI = AC.GROUP_URI + group.getID();
-        groupElement.setAttribute(new Attribute("uri", groupURI));
-
-        // Group owner
-        if (group.getOwner() != null)
-        {
-            Element ownerElement = new Element("owner");
-            Element userElement = UserWriter.getUserElement(group.getOwner());
-            ownerElement.addContent(userElement);
-            groupElement.addContent(ownerElement);
-        }
-
-        if (deepCopy)
-        {
-            // Group description
-            if (group.description != null)
-            {
-                Element descriptionElement = new Element("description");
-                descriptionElement.setText(group.description);
-                groupElement.addContent(descriptionElement);
-            }
-
-            // lastModified
-            if (group.lastModified != null)
-            {
-                Element lastModifiedElement = new Element("lastModified");
-                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
-                lastModifiedElement.setText(df.format(group.lastModified));
-                groupElement.addContent(lastModifiedElement);
-            }
-
-            // Group properties
-            if (!group.getProperties().isEmpty())
-            {
-                Element propertiesElement = new Element("properties");
-                for (GroupProperty property : group.getProperties())
-                {
-                    propertiesElement.addContent(ca.nrc.cadc.ac.xml.GroupPropertyWriter.write(property));
-                }
-                groupElement.addContent(propertiesElement);
-            }
-
-            // Group groupMembers.
-            if ((group.getGroupMembers() != null) && (!group.getGroupMembers().isEmpty()))
-            {
-                Element groupMembersElement = new Element("groupMembers");
-                for (Group groupMember : group.getGroupMembers())
-                {
-                    groupMembersElement.addContent(getGroupElement(groupMember, false));
-                }
-                groupElement.addContent(groupMembersElement);
-            }
-
-            // Group userMembers
-            if ((group.getUserMembers() != null) && (!group.getUserMembers().isEmpty()))
-            {
-                Element userMembersElement = new Element("userMembers");
-                for (User<? extends Principal> userMember : group.getUserMembers())
-                {
-                    userMembersElement.addContent(UserWriter.getUserElement(userMember));
-                }
-                groupElement.addContent(userMembersElement);
-            }
-            
-            // Group groupAdmins.
-            if ((group.getGroupAdmins() != null) && (!group.getGroupAdmins().isEmpty()))
-            {
-                Element groupAdminsElement = new Element("groupAdmins");
-                for (Group groupMember : group.getGroupAdmins())
-                {
-                    groupAdminsElement.addContent(getGroupElement(groupMember, false));
-                }
-                groupElement.addContent(groupAdminsElement);
-            }
-
-            // Group userAdmins
-            if ((group.getUserAdmins() != null) && (!group.getUserAdmins().isEmpty()))
-            {
-                Element userAdminsElement = new Element("userAdmins");
-                for (User<? extends Principal> userMember : group.getUserAdmins())
-                {
-                    userAdminsElement.addContent(UserWriter.getUserElement(userMember));
-                }
-                groupElement.addContent(userAdminsElement);
-            }
-        }
-
-        return groupElement;
-    }
-
-    /**
-     * Write to root Element to a writer.
-     * 
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
-    }
-    
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..4877e7c2cc876f07e888ef2563548b01babda0f1
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListReader.java
@@ -0,0 +1,204 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2015.                            (c) 2015.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.xml;
+
+import ca.nrc.cadc.ac.ReaderException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.xml.XmlUtil;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class to read an XML representation of a List of Users
+ * into a List of User objects.
+ */
+public class UserListReader extends AbstractXML
+{
+    /**
+     * Construct a List of Users from an XML String source.
+     *
+     * @param xml String of the XML.
+     * @return List of users.
+     * @throws ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public List<User<Principal>> read(String xml)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (xml == null)
+        {
+            throw new IllegalArgumentException("XML must not be null");
+        }
+        return read(new StringReader(xml));
+    }
+
+    /**
+     * Construct a List of Users from a InputStream.
+     *
+     * @param in InputStream.
+     * @return List of Users.
+     * @throws ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public List<User<Principal>> read(InputStream in)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (in == null)
+        {
+            throw new IOException("stream closed");
+        }
+        InputStreamReader reader;
+        try
+        {
+            reader = new InputStreamReader(in, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported");
+        }
+        return read(reader);
+    }
+
+    /**
+     * Construct a List of Users from a Reader.
+     *
+     * @param reader Reader.
+     * @return List of Users.
+     * @throws ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public List<User<Principal>> read(Reader reader)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        Document document;
+        try
+        {
+            document = XmlUtil.buildDocument(reader);
+        }
+        catch (JDOMException jde)
+        {
+            String error = "XML failed validation: " + jde.getMessage();
+            throw new ReaderException(error, jde);
+        }
+
+        Element root = document.getRootElement();
+
+        String userElemName = root.getName();
+
+        if (!userElemName.equalsIgnoreCase("users"))
+        {
+            String error = "Expected users element, found " + userElemName;
+            throw new ReaderException(error);
+        }
+
+        return getUserList(root);
+    }
+
+    /**
+     * Get a List of Users from a JDOM element.
+     *
+     * @param element The Users JDOM element.
+     * @return A List of User objects.
+     * @throws URISyntaxException
+     * @throws ReaderException
+     */
+    protected final List<User<Principal>> getUserList(Element element)
+        throws URISyntaxException, ReaderException
+    {
+        List<User<Principal>> users = new ArrayList<User<Principal>>();
+
+        List<Element> userElements = element.getChildren("user");
+        for (Element userElement : userElements)
+        {
+            users.add(getUser(userElement));
+        }
+
+        return users;
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
index f08d4de4c4ba371da692b858c25e51198cb6abe0..92697e1f092aa52a71f85087350edcb41f834a89 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserListWriter.java
@@ -68,54 +68,101 @@
 
 package ca.nrc.cadc.ac.xml;
 
-import ca.nrc.cadc.ac.PersonalDetails;
-import org.jdom2.Document;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.util.StringBuilderWriter;
 import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
+import java.io.BufferedWriter;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
 import java.io.Writer;
-import java.util.Map;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.List;
 
 /**
- * Class to write a XML representation of a Collection of User's.
+ * Class to write a XML representation of a List of User's.
  */
-public class UserListWriter
+public class UserListWriter extends AbstractXML
 {
     /**
-     * Write the Map of User entries as XML.
+     * Write a Collection of User's to a StringBuilder.
      *
-     * @param users             The Map of User IDs to Names.
-     * @param writer            The Writer to output to.
-     * @throws IOException      Any writing errors.
+     * @param users   Collection of User's to write.
+     * @param builder The StringBuilder.
+     * @throws java.io.IOException
+     * @throws WriterException
      */
-    public void write(final Map<String, PersonalDetails> users,
-                      final Writer writer) throws IOException
+    public <T extends Principal> void write(Collection<User<T>> users, StringBuilder builder)
+        throws IOException, WriterException
     {
-        // Create the root users Element.
-        final Element usersElement = new Element("users");
+        write(users, new StringBuilderWriter(builder));
+    }
+
+    /**
+     * Write a Collection of User's to an OutputStream.
+     *
+     * @param users Collection of User's to write.
+     * @param out   OutputStream to write to.
+     * @throws IOException     if the writer fails to write.
+     * @throws WriterException
+     */
+    public <T extends Principal> void write(Collection<User<T>> users, OutputStream out)
+        throws IOException, WriterException
+    {
+        OutputStreamWriter outWriter;
+        try
+        {
+            outWriter = new OutputStreamWriter(out, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported", e);
+        }
+        write(users, new BufferedWriter(outWriter));
+    }
 
-        for (final Map.Entry<String, PersonalDetails> entry : users.entrySet())
+    /**
+     * Write a Collection of Users to a Writer.
+     *
+     * @param users  Users to write.
+     * @param writer Writer to write to.
+     * @throws IOException     if the writer fails to write.
+     * @throws WriterException
+     */
+    public <T extends Principal> void write(Collection<User<T>> users, Writer writer)
+        throws IOException, WriterException
+    {
+        if (users == null)
         {
-            final Element userEntryElement = new Element("user");
-            final Element firstNameElement = new Element("firstName");
-            final Element lastNameElement = new Element("lastName");
+            throw new WriterException("null users");
+        }
 
-            userEntryElement.setAttribute("id", entry.getKey());
+        write(getElement(users), writer);
+    }
 
-            firstNameElement.setText(entry.getValue().getFirstName());
-            userEntryElement.addContent(firstNameElement);
 
-            lastNameElement.setText(entry.getValue().getLastName());
-            userEntryElement.addContent(lastNameElement);
+    /**
+     * Get a JDOM element from a Collection of User objects.
+     *
+     * @param users Collection of User's to write.
+     * @return A JDOM Group list representation.
+     * @throws WriterException
+     */
+    protected final <T extends Principal> Element getElement(Collection<User<T>> users)
+        throws WriterException
+    {
+        Element usersElement = new Element("users");
 
-            usersElement.addContent(userEntryElement);
+        for (User<T> user : users)
+        {
+            usersElement.addContent(getElement(user));
         }
 
-        final XMLOutputter output = new XMLOutputter();
-
-        output.setFormat(Format.getPrettyFormat());
-        output.output(new Document(usersElement), writer);
+        return usersElement;
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
index 0accd9da28358bd855b9b73c0d10ce8265044872..b4181956db7610c795219c5e1fd103680479dbcf 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserReader.java
@@ -83,16 +83,15 @@ import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.security.Principal;
-import java.util.List;
 
 /**
  * Class to read a XML representation of a User to a User object.
  */
-public class UserReader
+public class UserReader extends AbstractXML
 {
     /**
      * Construct a User from an XML String source.
-     * 
+     *
      * @param xml String of the XML.
      * @return User User.
      * @throws ReaderException
@@ -111,7 +110,7 @@ public class UserReader
 
     /**
      * Construct a User from a InputStream.
-     * 
+     *
      * @param in InputStream.
      * @return User User.
      * @throws java.io.IOException
@@ -137,7 +136,7 @@ public class UserReader
 
     /**
      * Construct a User from a Reader.
-     * 
+     *
      * @param reader Reader.
      * @return User User.
      * @throws ReaderException
@@ -166,58 +165,7 @@ public class UserReader
         // Root element and namespace of the Document
         Element root = document.getRootElement();
 
-        return parseUser(root);
-    }
-
-    public static User<Principal> parseUser(Element userElement)
-        throws ReaderException
-    {
-        // userID element of the User element
-        Element userIDElement = userElement.getChild("userID");
-        if (userIDElement == null)
-        {
-            String error = "userID element not found in user element";
-            throw new ReaderException(error);
-        }
-
-        // identity element of the userID element
-        Element userIDIdentityElement = userIDElement.getChild("identity");
-        if (userIDIdentityElement == null)
-        {
-            String error = "identity element not found in userID element";
-            throw new ReaderException(error);
-        }
-
-        IdentityReader identityReader = new IdentityReader();
-        Principal userID = identityReader.read(userIDIdentityElement);
-
-        User<Principal> user = new User<Principal>(userID);
-
-        // identities
-        Element identitiesElement = userElement.getChild("identities");
-        if (identitiesElement != null)
-        {
-            List<Element> identityElements = identitiesElement.getChildren("identity");
-            for (Element identityElement : identityElements)
-            {
-                user.getIdentities().add(identityReader.read(identityElement));
-            }
-
-        }
-
-        // details
-        Element detailsElement = userElement.getChild("details");
-        if (detailsElement != null)
-        {
-            UserDetailsReader userDetailsReader = new UserDetailsReader();
-            List<Element> userDetailsElements = detailsElement.getChildren("userDetails");
-            for (Element userDetailsElement : userDetailsElements)
-            {
-                user.details.add(userDetailsReader.read(userDetailsElement));
-            }
-        }
-
-        return user;
+        return getUser(root);
     }
 
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
index e44eba1e4dc37fe8f9fa81b0f7b9629bf271e65e..317d753c4756ef2f585a4044be738fcabf3ab8c4 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestReader.java
@@ -69,9 +69,12 @@
 package ca.nrc.cadc.ac.xml;
 
 import ca.nrc.cadc.ac.ReaderException;
-import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.xml.XmlUtil;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -80,14 +83,10 @@ import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.security.Principal;
 
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.JDOMException;
-
 /**
  * Class to read a XML representation of a UserRequest to a UserRequest object.
  */
-public class UserRequestReader
+public class UserRequestReader extends AbstractXML
 {
     /**
      * Construct a UserRequest from an XML String source.
@@ -164,31 +163,7 @@ public class UserRequestReader
         // Root element and namespace of the Document
         Element root = document.getRootElement();
 
-        return parseUserRequest(root);
+        return getUserRequest(root);
     }
 
-    protected static UserRequest<Principal> parseUserRequest(
-            Element userRequestElement)
-        throws ReaderException
-    {
-        // user element of the UserRequest element
-        Element userElement = userRequestElement.getChild("user");
-        if (userElement == null)
-        {
-            String error = "user element not found in userRequest element";
-            throw new ReaderException(error);
-        }
-        User<Principal> user = ca.nrc.cadc.ac.xml.UserReader.parseUser(userElement);
-
-        // password element of the userRequest element
-        Element passwordElement = userRequestElement.getChild("password");
-        if (passwordElement == null)
-        {
-            String error = "password element not found in userRequest element";
-            throw new ReaderException(error);
-        }
-        String password = passwordElement.getText();
-
-        return new UserRequest<Principal>(user, password.toCharArray());
-    }
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
index fe0ffee96a6fbbd1a9e9a70f77d575dfbdf0a629..ef1006820b1bd5ad6cd50722e1a307ed9ceb17d6 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserRequestWriter.java
@@ -72,10 +72,6 @@ package ca.nrc.cadc.ac.xml;
 import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.util.StringBuilderWriter;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -84,7 +80,7 @@ import java.security.Principal;
 /**
  * Class to write a XML representation of a UserRequest object.
  */
-public class UserRequestWriter
+public class UserRequestWriter extends AbstractXML
 {
     /**
      * Write a UserRequest to a StringBuilder.
@@ -94,7 +90,7 @@ public class UserRequestWriter
      * @throws java.io.IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(UserRequest<? extends Principal> userRequest, StringBuilder builder)
+    public <T extends Principal> void write(UserRequest<T> userRequest, StringBuilder builder)
         throws IOException, WriterException
     {
         write(userRequest, new StringBuilderWriter(builder));
@@ -108,7 +104,7 @@ public class UserRequestWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public static void write(UserRequest<? extends Principal> userRequest, Writer writer)
+    public <T extends Principal> void write(UserRequest<T> userRequest, Writer writer)
         throws IOException, WriterException
     {
         if (userRequest == null)
@@ -116,46 +112,7 @@ public class UserRequestWriter
             throw new WriterException("null UserRequest");
         }
 
-        write(getUserRequestElement(userRequest), writer);
+        write(getElement(userRequest), writer);
     }
 
-    /**
-     * Build the UserRequest element.
-     *
-     * @param userRequest UserRequest.
-     * @return member Element.
-     * @throws WriterException
-     */
-    public static Element getUserRequestElement(UserRequest<? extends Principal> userRequest)
-        throws WriterException
-    {
-        // Create the userRequest Element.
-        Element userRequestElement = new Element("userRequest");
-
-        // user element
-        Element userElement = UserWriter.getUserElement(userRequest.getUser());
-        userRequestElement.addContent(userElement);
-
-        // password element
-        Element passwordElement = new Element("password");
-        passwordElement.setText(String.valueOf(userRequest.getPassword()));
-        userRequestElement.addContent(passwordElement);
-
-        return userRequestElement;
-    }
-
-    /**
-     * Write to root Element to a writer.
-     *
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
-    }
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
index 86327ad8f27e7e77bfaa4b17291fe4d8830e2fbf..e2fe3bf80cde9544841a5f9a3a244dae71cb70d0 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UserWriter.java
@@ -69,13 +69,8 @@
 package ca.nrc.cadc.ac.xml;
 
 import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserDetails;
 import ca.nrc.cadc.ac.WriterException;
 import ca.nrc.cadc.util.StringBuilderWriter;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.output.Format;
-import org.jdom2.output.XMLOutputter;
 
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -84,12 +79,11 @@ import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.security.Principal;
-import java.util.Set;
 
 /**
  * Class to write a XML representation of a User object.
  */
-public class UserWriter
+public class UserWriter extends AbstractXML
 {
     /**
      * Write a User to a StringBuilder.
@@ -99,7 +93,7 @@ public class UserWriter
      * @throws java.io.IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(User<? extends Principal> user, StringBuilder builder)
+    public <T extends Principal> void write(User<T> user, StringBuilder builder)
         throws IOException, WriterException
     {
         write(user, new StringBuilderWriter(builder));
@@ -113,7 +107,7 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(User<? extends Principal> user, OutputStream out)
+    public <T extends Principal> void write(User<T> user, OutputStream out)
         throws IOException, WriterException
     {                
         OutputStreamWriter outWriter;
@@ -136,7 +130,7 @@ public class UserWriter
      * @throws IOException if the writer fails to write.
      * @throws WriterException
      */
-    public void write(User<? extends Principal> user, Writer writer)
+    public <T extends Principal> void write(User<T> user, Writer writer)
         throws IOException, WriterException
     {
         if (user == null)
@@ -144,69 +138,7 @@ public class UserWriter
             throw new WriterException("null User");
         }
         
-        write(getUserElement(user), writer);
-    }
-
-    /**
-     * Build the member Element of a User.
-     *
-     * @param user User.
-     * @return member Element.
-     * @throws WriterException
-     */
-    public static Element getUserElement(User<? extends Principal> user)
-        throws WriterException
-    {
-        // Create the user Element.
-        Element userElement = new Element("user");
-
-        // userID element
-        IdentityWriter identityWriter = new IdentityWriter();
-        Element userIDElement = new Element("userID");
-        userIDElement.addContent(identityWriter.write(user.getUserID()));
-        userElement.addContent(userIDElement);
-
-        // identities
-        Set<Principal> identities = user.getIdentities();
-        if (!identities.isEmpty())
-        {
-            Element identitiesElement = new Element("identities");
-            for (Principal identity : identities)
-            {
-                identitiesElement.addContent(identityWriter.write(identity));
-            }
-            userElement.addContent(identitiesElement);
-        }
-
-        // details
-        if (!user.details.isEmpty())
-        {
-            UserDetailsWriter userDetailsWriter = new UserDetailsWriter();
-            Element detailsElement = new Element("details");
-            Set<UserDetails> userDetails = user.details;
-            for (UserDetails userDetail : userDetails)
-            {
-                detailsElement.addContent(userDetailsWriter.write(userDetail));
-            }
-            userElement.addContent(detailsElement);
-        }
-
-        return userElement;
-    }
-
-    /**
-     * Write to root Element to a writer.
-     *
-     * @param root Root Element to write.
-     * @param writer Writer to write to.
-     * @throws IOException if the writer fails to write.
-     */
-    private static void write(Element root, Writer writer)
-        throws IOException
-    {
-        XMLOutputter outputter = new XMLOutputter();
-        outputter.setFormat(Format.getPrettyFormat());
-        outputter.output(new Document(root), writer);
+        write(getElement(user), writer);
     }
 
 }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserListReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserListReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..44b3922a64b8ba20c6270cece1d1717832e52f8c
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserListReaderWriterTest.java
@@ -0,0 +1,95 @@
+package ca.nrc.cadc.ac.json;
+
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.PosixDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+import org.apache.log4j.Logger;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+/**
+ * JsonUserListReaderWriterTest TODO describe class
+ */
+public class JsonUserListReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(JsonUserListReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            JsonUserListReader reader = new JsonUserListReader();
+            List<User<Principal>> u = reader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+
+        try
+        {
+            InputStream in = null;
+            JsonUserListReader reader = new JsonUserListReader();
+            List<User<Principal>> u = reader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+
+        try
+        {
+            Reader r = null;
+            JsonUserListReader reader = new JsonUserListReader();
+            List<User<Principal>> u = reader.read(r);
+            fail("null Reader should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            JsonUserWriter writer = new JsonUserWriter();
+            writer.write(null, new StringBuilder());
+            fail("null User should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+
+    @Test
+    public void testReadWrite()
+        throws Exception
+    {
+        User<Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
+        expected.getIdentities().add(new NumericPrincipal(123l));
+        expected.details.add(new PersonalDetails("firstname", "lastname"));
+        expected.details.add(new PosixDetails(123l, 456l, "foo"));
+
+        StringBuilder json = new StringBuilder();
+        JsonUserWriter writer = new JsonUserWriter();
+        writer.write(expected, json);
+        assertFalse(json.toString().isEmpty());
+
+        JsonUserReader reader = new JsonUserReader();
+        User<Principal> actual = reader.read(json.toString());
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+    }
+
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
index dccefe894c2a5b2a3addf25e226cee910061ea74..7de85967189e9397195f80ab03f012b5c8c03d4c 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/json/JsonUserReaderWriterTest.java
@@ -103,7 +103,7 @@ public class JsonUserReaderWriterTest
         {
             String s = null;
             JsonUserReader reader = new JsonUserReader();
-            User<? extends Principal> u = reader.read(s);
+            User<Principal> u = reader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -112,7 +112,7 @@ public class JsonUserReaderWriterTest
         {
             InputStream in = null;
             JsonUserReader reader = new JsonUserReader();
-            User<? extends Principal> u = reader.read(in);
+            User<Principal> u = reader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -121,7 +121,7 @@ public class JsonUserReaderWriterTest
         {
             Reader r = null;
             JsonUserReader reader = new JsonUserReader();
-            User<? extends Principal> u = reader.read(r);
+            User<Principal> u = reader.read(r);
             fail("null Reader should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -144,7 +144,7 @@ public class JsonUserReaderWriterTest
     public void testReadWrite()
         throws Exception
     {
-        User<Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
+        User<? extends Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
         expected.getIdentities().add(new NumericPrincipal(123l));
         expected.details.add(new PersonalDetails("firstname", "lastname"));
         expected.details.add(new PosixDetails(123l, 456l, "foo"));
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupListReaderWriterTest.java
similarity index 98%
rename from projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java
rename to projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupListReaderWriterTest.java
index 0281eaf87cf1456c0d5781fc187012d3d5c7caa2..a18c13b3c093922e9da73774004296729eb0cf70 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupsReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupListReaderWriterTest.java
@@ -88,9 +88,9 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class GroupsReaderWriterTest
+public class GroupListReaderWriterTest
 {
-    private static Logger log = Logger.getLogger(GroupsReaderWriterTest.class);
+    private static Logger log = Logger.getLogger(GroupListReaderWriterTest.class);
 
     @Test
     public void testReaderExceptions()
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java
index 7075c9e1f649ca01beb98529bfba564f441582b1..172d211ec5ea4a2554c891db2c96f2ec1b55bc4b 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/GroupPropertyReaderWriterTest.java
@@ -83,7 +83,7 @@ import static org.junit.Assert.*;
  *
  * @author jburke
  */
-public class GroupPropertyReaderWriterTest
+public class GroupPropertyReaderWriterTest extends AbstractXML
 {
     private static Logger log = Logger.getLogger(GroupPropertyReaderWriterTest.class);
 
@@ -100,7 +100,7 @@ public class GroupPropertyReaderWriterTest
         Element element = null;
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("null element should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -108,7 +108,7 @@ public class GroupPropertyReaderWriterTest
         element = new Element("foo");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("element not named 'property' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -116,7 +116,7 @@ public class GroupPropertyReaderWriterTest
         element = new Element("property");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("element without 'key' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -124,7 +124,7 @@ public class GroupPropertyReaderWriterTest
         element.setAttribute("key", "foo");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("element without 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -132,7 +132,7 @@ public class GroupPropertyReaderWriterTest
         element.setAttribute("type", "Double");
         try
         {
-            GroupProperty gp = GroupPropertyReader.read(element);
+            GroupProperty gp = getGroupProperty(element);
             fail("Unsupported 'type' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -144,15 +144,16 @@ public class GroupPropertyReaderWriterTest
     {
         try
         {
-            Element element = GroupPropertyWriter.write(null);
+            GroupProperty groupProperty = null;
+            Element element = getElement(groupProperty);
             fail("null GroupProperty should throw WriterException");
         }
         catch (WriterException e) {}
          
-        GroupProperty gp = new GroupProperty("key", new Double(1.0), true);
+        GroupProperty groupProperty = new GroupProperty("key", new Double(1.0), true);
         try
         {
-            Element element = GroupPropertyWriter.write(gp);
+            Element element = getElement(groupProperty);
             fail("Unsupported GroupProperty type should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -164,20 +165,20 @@ public class GroupPropertyReaderWriterTest
     {
         // String type
         GroupProperty expected = new GroupProperty("key", "value", true);
-        Element element = GroupPropertyWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
          
-        GroupProperty actual = GroupPropertyReader.read(element);
+        GroupProperty actual = getGroupProperty(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
          
         // Integer tuype
         expected = new GroupProperty("key", new Integer(1), false);
-        element = GroupPropertyWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = GroupPropertyReader.read(element);
+        actual = getGroupProperty(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java
index 58918c8d17fc6d9e056305e9246f4e3ce9e0c121..7ff46919aa479a05a077ca1b07f26a175a700018 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/IdentityReaderWriterTest.java
@@ -89,7 +89,7 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class IdentityReaderWriterTest
+public class IdentityReaderWriterTest extends AbstractXML
 {
     private static Logger log = Logger.getLogger(IdentityReaderWriterTest.class);
 
@@ -100,7 +100,7 @@ public class IdentityReaderWriterTest
         Element element = null;
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("null element should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -108,7 +108,7 @@ public class IdentityReaderWriterTest
         element = new Element("foo");
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("element not named 'identity' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -116,7 +116,7 @@ public class IdentityReaderWriterTest
         element = new Element("identity");
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("element without 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -124,7 +124,7 @@ public class IdentityReaderWriterTest
         element.setAttribute("type", "foo");
         try
         {
-            Principal p = IdentityReader.read(element);
+            Principal p = getPrincipal(element);
             fail("element with unknown 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -134,17 +134,18 @@ public class IdentityReaderWriterTest
     public void testWriterExceptions()
         throws Exception
     {
+        Principal p = null;
         try
         {
-            Element element = IdentityWriter.write(null);
+            Element element = getElement(p);
             fail("null Identity should throw WriterException");
         }
         catch (WriterException e) {}
          
-        Principal p = new JMXPrincipal("foo");
+        p = new JMXPrincipal("foo");
         try
         {
-            Element element = IdentityWriter.write(p);
+            Element element = getElement(p);
             fail("Unsupported Principal type should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -156,40 +157,40 @@ public class IdentityReaderWriterTest
     {
         // X500
         Principal expected = new X500Principal("cn=foo,o=bar");
-        Element element = IdentityWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
          
-        Principal actual = IdentityReader.read(element);
+        Principal actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
-         
+
         // UID
         expected = new NumericPrincipal(123l);
-        element = IdentityWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = IdentityReader.read(element);
+        actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
         
         // OpenID
         expected = new OpenIdPrincipal("bar");
-        element = IdentityWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = IdentityReader.read(element);
+        actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
         
         // HTTP
         expected = new HttpPrincipal("baz");
-        element = IdentityWriter.write(expected);
+        element = getElement(expected);
         assertNotNull(element);
          
-        actual = IdentityReader.read(element);
+        actual = getPrincipal(element);
         assertNotNull(actual);
          
         assertEquals(expected, actual);
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java
index a7badabbaa4ce6e745946e879b6d4636b5141d65..3a1baaa140a213beef10ce1a5e30f3faf54f0701 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserDetailsReaderWriterTest.java
@@ -85,7 +85,7 @@ import static org.junit.Assert.fail;
  *
  * @author jburke
  */
-public class UserDetailsReaderWriterTest
+public class UserDetailsReaderWriterTest extends AbstractXML
 {
     private static Logger log = Logger.getLogger(UserDetailsReaderWriterTest.class);
 
@@ -96,7 +96,7 @@ public class UserDetailsReaderWriterTest
         Element element = null;
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("null element should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -104,7 +104,7 @@ public class UserDetailsReaderWriterTest
         element = new Element("foo");
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("element not named 'userDetails' should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -112,7 +112,7 @@ public class UserDetailsReaderWriterTest
         element = new Element(UserDetails.NAME);
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("element without 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -120,7 +120,7 @@ public class UserDetailsReaderWriterTest
         element.setAttribute("type", "foo");
         try
         {
-            UserDetails ud = UserDetailsReader.read(element);
+            UserDetails ud = getUserDetails(element);
             fail("element with unknown 'type' attribute should throw ReaderException");
         }
         catch (ReaderException e) {}
@@ -132,7 +132,8 @@ public class UserDetailsReaderWriterTest
     {
         try
         {
-            Element element = UserDetailsWriter.write(null);
+            UserDetails ud = null;
+            Element element = getElement(ud);
             fail("null UserDetails should throw WriterException");
         }
         catch (WriterException e) {}
@@ -148,10 +149,10 @@ public class UserDetailsReaderWriterTest
         expected.country = "country";
         expected.email = "email";
         expected.institute = "institute";
-        Element element = UserDetailsWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
         
-        PersonalDetails actual = (PersonalDetails) UserDetailsReader.read(element);
+        PersonalDetails actual = (PersonalDetails) getUserDetails(element);
         assertNotNull(actual);
         assertEquals(expected, actual);
         assertEquals(expected.address, actual.address);
@@ -166,10 +167,10 @@ public class UserDetailsReaderWriterTest
         throws Exception
     {
         UserDetails expected = new PosixDetails(123l, 456, "/dev/null");
-        Element element = UserDetailsWriter.write(expected);
+        Element element = getElement(expected);
         assertNotNull(element);
         
-        UserDetails actual = UserDetailsReader.read(element);
+        UserDetails actual = getUserDetails(element);
         assertNotNull(actual);
         assertEquals(expected, actual);
     }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserListReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserListReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ddca58a35b80651b347fb533e575b9f825a19b40
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserListReaderWriterTest.java
@@ -0,0 +1,92 @@
+package ca.nrc.cadc.ac.xml;
+
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.WriterException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import org.apache.log4j.Logger;
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class UserListReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(UserListReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            UserListReader UserListReader = new UserListReader();
+            List<User<Principal>> u = UserListReader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+
+        try
+        {
+            InputStream in = null;
+            UserListReader userListReader = new UserListReader();
+            List<User<Principal>> u = userListReader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+
+        try
+        {
+            Reader r = null;
+            UserListReader userListReader = new UserListReader();
+            List<User<Principal>> u = userListReader.read(r);
+            fail("null element should throw ReaderException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            UserListWriter userListWriter = new UserListWriter();
+            userListWriter.write(null, new StringBuilder());
+            fail("null User should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+
+    @Test
+    public void testMinimalReadWrite()
+        throws Exception
+    {
+        List<User<Principal>> expected = new ArrayList<User<Principal>>();
+        expected.add(new User<Principal>(new HttpPrincipal("foo")));
+        expected.add(new User<Principal>(new X500Principal("cn=foo,o=bar")));
+
+        StringBuilder xml = new StringBuilder();
+        UserListWriter userListWriter = new UserListWriter();
+        userListWriter.write(expected, xml);
+        assertFalse(xml.toString().isEmpty());
+
+        UserListReader userListReader = new UserListReader();
+        List<User<Principal>> actual = userListReader.read(xml.toString());
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+        assertEquals(expected.get(0), actual.get(0));
+        assertEquals(expected.get(1), actual.get(1));
+    }
+
+}
\ No newline at end of file
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java
index 4e8d9f2b066ef053fb815854ee2a3ce03c66e30c..c420e7ca34c5bf47b8290238d9011d4f73c5a6d5 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/xml/UserReaderWriterTest.java
@@ -102,7 +102,7 @@ public class UserReaderWriterTest
         {
             String s = null;
             UserReader userReader = new UserReader();
-            User<? extends Principal> u = userReader.read(s);
+            User<Principal> u = userReader.read(s);
             fail("null String should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -111,7 +111,7 @@ public class UserReaderWriterTest
         {
             InputStream in = null;
             UserReader userReader = new UserReader();
-            User<? extends Principal> u = userReader.read(in);
+            User<Principal> u = userReader.read(in);
             fail("null InputStream should throw IOException");
         }
         catch (IOException e) {}
@@ -120,7 +120,7 @@ public class UserReaderWriterTest
         {
             Reader r = null;
             UserReader userReader = new UserReader();
-            User<? extends Principal> u = userReader.read(r);
+            User<Principal> u = userReader.read(r);
             fail("null Reader should throw IllegalArgumentException");
         }
         catch (IllegalArgumentException e) {}
@@ -143,7 +143,7 @@ public class UserReaderWriterTest
     public void testReadWrite()
         throws Exception
     {
-        User<? extends Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
+        User<Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
         expected.getIdentities().add(new NumericPrincipal(123l));
         expected.details.add(new PersonalDetails("firstname", "lastname"));