/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see 
     * Z0
     * G1
     * U6
     * 
     * Z1
     * G2 - U1, G3 - U2, G4, G5
     * 
     * Z2
     * G2 - U1, U3, U4
     * G6 - U3, U4, G7 - U5
     *  
     * 
     * @throws Exception
     *             the exception
     */
    private void setUpTestUsersAndGroups() throws Exception
    {
        this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", new NodeDescription[]
        {
            newPerson("U6")
        }, new NodeDescription[]
        {
            newGroup("G1")
        }), new MockUserRegistry("Z1", new NodeDescription[]
        {
            newPerson("U1"), newPerson("U2"), newPerson("U7")
        }, new NodeDescription[]
        {
            newGroup("G2", "U1", "G3"), newGroup("G3", "U2", "G4", "G5"), newGroup("G4"), newGroup("G5")
        }), new MockUserRegistry("Z2", new NodeDescription[]
        {
            newPerson("U1"), newPerson("U3"), newPerson("U4"), newPerson("U5")
        }, new NodeDescription[]
        {
            newGroup("G2", "U1", "U3", "U4"), newGroup("G6", "U3", "U4", "G7"), newGroup("G7", "U5")
        }));
        this.synchronizer.synchronize(true, true, true);
        this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback()
        {
            public Object execute() throws Throwable
            {
                assertExists("Z0", "U6");
                assertExists("Z0", "G1");
                assertExists("Z1", "U1");
                assertExists("Z1", "U2");
                assertExists("Z1", "G2", "U1", "G3");
                assertExists("Z1", "G3", "U2", "G4", "G5");
                assertExists("Z1", "G4");
                assertExists("Z1", "G5");
                assertExists("Z2", "U3");
                assertExists("Z2", "U4");
                assertExists("Z2", "U5");
                assertExists("Z2", "G6", "U3", "U4", "G7");
                assertExists("Z2", "G7", "U5");
                return null;
            }
        }, false, true);
    }
    /**
     * Tear down test users and groups.
     * 
     * @throws Exception
     *             the exception
     */
    public void tearDownTestUsersAndGroups() throws Exception
    {
        // Wipe out everything that was in Z1 and Z2
        this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", new NodeDescription[] {},
                new NodeDescription[] {}), new MockUserRegistry("Z1", new NodeDescription[] {},
                new NodeDescription[] {}), new MockUserRegistry("Z2", new NodeDescription[] {},
                new NodeDescription[] {}));
        this.synchronizer.synchronize(true, true, true);
        this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback()
        {
            public Object execute() throws Throwable
            {
                assertNotExists("U1");
                assertNotExists("U2");
                assertNotExists("U3");
                assertNotExists("U4");
                assertNotExists("U5");
                assertNotExists("U6");
                assertNotExists("U7");
                assertNotExists("G1");
                assertNotExists("G2");
                assertNotExists("G3");
                assertNotExists("G4");
                assertNotExists("G5");
                assertNotExists("G6");
                assertNotExists("G7");
                return null;
            }
        }, false, true);
    }
    public void setHomeFolderCreationEager(boolean homeFolderCreationEager)
    {
        this.homeFolderCreationEager = homeFolderCreationEager;
        ((PersonServiceImpl)personService).setHomeFolderCreationEager(homeFolderCreationEager);
    }
    /**
     * Tests a differential update of the test users and groups. The layout is as follows
     * 
     * 
     * Z1
     * G2 - U1, G1 - U1, U6
     * G3 - U2, G4, G5 - U6
     * 
     * Z2
     * G2 - U1, U3, U4, U6
     * G6 - U3, U4, G7, G8 - U4, U8
     *  
     * 
     * @throws Exception
     *             the exception
     */
    public void testDifferentialUpdate() throws Exception
    {
        setUpTestUsersAndGroups();
        this.applicationContextManager.removeZone("Z0");
        this.applicationContextManager.updateZone("Z1", new NodeDescription[]
        {
            newPerson("U1", "changeofemail@alfresco.com"), newPerson("U6"), newPerson("U7")
        }, new NodeDescription[]
        {
            newGroup("G1", "U1", "U6", "UDangling", "G2"), newGroup("G2", "U1", "GDangling", "G1"), // test cyclic G2
                                                                                                    // <-> G1
            newGroupWithDisplayName("G5", "Amazing Group", "U6", "U7", "G4")
        });
        this.applicationContextManager.updateZone("Z2", new NodeDescription[]
        {
            newPerson("U1", "shouldbeignored@alfresco.com"), newPerson("U5", "u5email@alfresco.com"), newPerson("U6"),
            newPerson("U8")
        }, new NodeDescription[]
        {
            newGroup("G2", "U1", "U3", "U4", "U6"), newGroup("G7"), newGroup("G8", "U4", "U8")
        });
        this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback()
        {
            public Object execute() throws Throwable
            {
                ChainingUserRegistrySynchronizerTest.this.synchronizer.synchronize(false, false, false);
                // Stay in the same transaction
                assertExists("Z1", "U1");
                assertEmailEquals("U1", "changeofemail@alfresco.com");
                assertExists("Z1", "U2");
                assertExists("Z1", "U6");
                assertExists("Z1", "U7");
                assertExists("Z1", "G1", "U1", "U6");
                assertExists("Z1", "G2", "U1", "G1");
                assertExists("Z1", "G3", "U2", "G4", "G5");
                assertExists("Z1", "G4");
                assertExists("Z1", "G5", "U6", "U7", "G4");
                assertGroupDisplayNameEquals("G5", "Amazing Group");
                assertExists("Z2", "U3");
                assertExists("Z2", "U4");
                assertExists("Z2", "U5");
                assertEmailEquals("U5", "u5email@alfresco.com");
                assertExists("Z2", "U8");
                assertExists("Z2", "G6", "U3", "U4", "G7");
                assertExists("Z2", "G7");
                assertExists("Z2", "G8", "U4", "U8");
                return null;
            }
        });
        tearDownTestUsersAndGroups();
    }
    /**
     * Tests a forced update of the test users and groups. Also tests that groups and users that previously existed in
     * Z2 get moved when they appear in Z1. Also tests that 'dangling references' to removed users (U4, U5) do not cause
     * any problems. Also tests that case-sensitivity is not a problem when an occluded user is recreated with different
     * case. The layout is as follows
     * 
     * 
     * Z1
     * G1 - U6
     * G3 - U2, G5 - U6, G2 - G1
     * G6 - u3
     * 
     * Z2
     * G2 - U1
     * G6 - U3, G7
	 * G8 - U1, U8
     *  
     * 
     * @throws Exception
     *             the exception
     */
    public void testForcedUpdate() throws Exception
    {
        setUpTestUsersAndGroups();
        this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[]
        {
            newPerson("U2"), newPerson("u3"), newPerson("U6")
        }, new NodeDescription[]
        {
            newGroup("G1", "U6", "G5"), newGroup("G2", "G1"), newGroup("G3", "U2", "G5"), newGroup("G5", "U6", "G2"), // cycle g1 -> g5 -> g2 -> g1
            newGroup("G6", "u3")
        }), new MockUserRegistry("Z2", new NodeDescription[]
        {
            newPerson("U1", "somenewemail@alfresco.com"), newPerson("U3"), newPerson("U6"), newPerson("U8"),
        }, new NodeDescription[]
        {
            newGroup("G2", "U1", "U3", "U4", "U6"), newGroup("G6", "U3", "U4", "G7"),
            newGroupWithDisplayName("G7", "Late Arrival", "U4", "U5"), newGroup("G8", "U1", "U8")
        }));
        this.synchronizer.synchronize(true, true, true);
        this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback()
        {
            public Object execute() throws Throwable
            {
                assertExists("Z1", "U2");
                assertExists("Z1", "u3");
                assertExists("Z1", "U6");
                assertExists("Z1", "G1", "U6");
                assertExists("Z1", "G2", "G1");
                assertExists("Z1", "G3", "U2", "G5");
                assertNotExists("G4");
                assertExists("Z1", "G5", "U6", "G2");
                assertExists("Z1", "G6", "u3");
                assertExists("Z2", "U1");
                assertEmailEquals("U1", "somenewemail@alfresco.com");
                assertNotExists("U4");
                assertNotExists("U5");
                assertExists("Z2", "U8");
				assertExists("Z2", "G7");
                assertGroupDisplayNameEquals("G7", "Late Arrival");
                assertExists("Z2", "G8", "U1", "U8");
                return null;
            }
        }, false, true);
        tearDownTestUsersAndGroups();
    }
    /**
     * Tests a forced update of the test users and groups where some of the users change their case and some groups
     * appear with different case.
     */
    public void testCaseChange() throws Exception
    {
        setUpTestUsersAndGroups();
        final Map personNodes = new TreeMap();
        this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback()
        {
            public Object execute() throws Throwable
            {
                // Get hold of the original person nodes so we can compare them later
                personNodes.put("u1", ChainingUserRegistrySynchronizerTest.this.personService.getPerson("U1", false));
                personNodes.put("u2", ChainingUserRegistrySynchronizerTest.this.personService.getPerson("U2", false));
                personNodes.put("u6", ChainingUserRegistrySynchronizerTest.this.personService.getPerson("U6", false));
                return null;
            }
        }, false, true);
        this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[]
        {
            newPerson("u1"), newPerson("u2"), newPerson("u6"), newPerson("U7")
        }, new NodeDescription[]
        {
            newGroup("g1", "u6"), newGroup("g2", "u1", "G3"), newGroup("G3", "u2", "g4", "g5"), newGroup("g4"),
            newGroup("g5")
        }), new MockUserRegistry("Z2", new NodeDescription[]
        {
            newPerson("U1"), newPerson("U3"), newPerson("U4"), newPerson("U5")
        }, new NodeDescription[]
        {
            newGroup("G2", "U1", "U3", "U4"), newGroup("G6", "U3", "U4", "G7"), newGroup("G7", "U5")
        }));
        this.synchronizer.synchronize(true, true, true);
        this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback()
        {
            public Object execute() throws Throwable
            {
                assertExists("Z1", "u1");
                assertExists("Z1", "u2");
                assertExists("Z1", "u6");
                assertExists("Z1", "g1", "u6");
                assertExists("Z1", "g2", "u1", "G3");
                assertExists("Z1", "G3", "u2", "g4", "g5");
                assertExists("Z1", "g4");
                assertExists("Z1", "g5");
                assertExists("Z2", "U3");
                assertExists("Z2", "U4");
                assertExists("Z2", "U5");
                assertExists("Z2", "G2", "U3", "U4");
                assertExists("Z2", "G6", "U3", "U4", "G7");
                assertExists("Z2", "G7", "U5");
                // Make sure the original people have been preserved
                assertEquals(personNodes.get("u1"), ChainingUserRegistrySynchronizerTest.this.personService.getPerson(
                        "U1", false));
                assertEquals(personNodes.get("u2"), ChainingUserRegistrySynchronizerTest.this.personService.getPerson(
                        "U2", false));
                assertEquals(personNodes.get("u6"), ChainingUserRegistrySynchronizerTest.this.personService.getPerson(
                        "U6", false));
                return null;
            }
        }, false, true);
        tearDownTestUsersAndGroups();
    }
    
    public void testDifferentialUpdateWithHomeFolderCreation() throws Exception
    {
        setHomeFolderCreationEager(!homeFolderCreationEager);
        testDifferentialUpdate();
    }
    public void testForcedUpdateWithHomeFolderCreation() throws Exception
    {
        setHomeFolderCreationEager(!homeFolderCreationEager);
        testDifferentialUpdate();
    }
    public void testCaseChangeWithHomeFolderCreation() throws Exception
    {
        setHomeFolderCreationEager(!homeFolderCreationEager);
        testDifferentialUpdate();
    }
    /**
     * Tests synchronization with a zone with a larger volume of authorities.
     * 
     * @throws Exception
     *             the exception
     */
    public void testVolume() throws Exception
    {
        List persons = new ArrayList(new RandomPersonCollection(100));
        List groups = new ArrayList(new RandomGroupCollection(50, persons));
        this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", persons, groups));
        this.synchronizer.synchronize(true, true, true);
        tearDownTestUsersAndGroups();
    }
    /**
     * Tests synchronization of group associations in a zone with a larger volume of authorities.
     * 
     * @throws Exception
     *             the exception
     */
    public void dontTestAssocs() throws Exception
    {
        List groups = this.retryingTransactionHelper.doInTransaction(
                new RetryingTransactionCallback>()
                {
                    public List execute() throws Throwable
                    {
                        return new ArrayList(new RandomGroupCollection(1000,
                                ChainingUserRegistrySynchronizerTest.this.authorityService.getAllAuthoritiesInZone(
                                        AuthorityService.ZONE_AUTH_EXT_PREFIX + "Z0", null)));
                    }
                }, true, true);
        ChainingUserRegistrySynchronizerTest.this.applicationContextManager.setUserRegistries(new MockUserRegistry(
                "Z0", Collections. emptyList(), groups));
        ChainingUserRegistrySynchronizerTest.this.synchronizer.synchronize(true, true, true);
        tearDownTestUsersAndGroups();
    }
    /**
     * Constructs a description of a test group.
     * 
     * @param name
     *            the name
     * @param members
     *            the members
     * @return the node description
     */
    private NodeDescription newGroup(String name, String... members)
    {
        return newGroupWithDisplayName(name, name, members);
    }
    /**
     * Constructs a description of a test group with a display name.
     * 
     * @param name
     *            the name
     * @param displayName
     *            the display name
     * @param members
     *            the members
     * @return the node description
     */
    private NodeDescription newGroupWithDisplayName(String name, String displayName, String... members)
    {
        String longName = longName(name);
        NodeDescription group = new NodeDescription(longName);
        PropertyMap properties = group.getProperties();
        properties.put(ContentModel.PROP_AUTHORITY_NAME, longName);
        properties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, displayName);
        if (members.length > 0)
        {
            Set assocs = group.getChildAssociations();
            for (String member : members)
            {
                assocs.add(longName(member));
            }
        }
        group.setLastModified(new Date());
        return group;
    }
    /**
     * Constructs a description of a test person with default email (userName@alfresco.com)
     * 
     * @param userName
     *            the user name
     * @return the node description
     */
    private NodeDescription newPerson(String userName)
    {
        return newPerson(userName, userName + "@alfresco.com");
    }
    /**
     * Constructs a description of a test person with a given email.
     * 
     * @param userName
     *            the user name
     * @param email
     *            the email
     * @return the node description
     */
    private NodeDescription newPerson(String userName, String email)
    {
        NodeDescription person = new NodeDescription(userName);
        PropertyMap properties = person.getProperties();
        properties.put(ContentModel.PROP_USERNAME, userName);
        properties.put(ContentModel.PROP_FIRSTNAME, userName + "F");
        properties.put(ContentModel.PROP_LASTNAME, userName + "L");
        properties.put(ContentModel.PROP_EMAIL, email);
        person.setLastModified(new Date());
        return person;
    }
    /**
     * Perform all the necessary assertions to ensure that an authority and its members exist in the correct zone.
     * 
     * @param zone
     *            the zone
     * @param name
     *            the name
     * @param members
     *            the members
     */
    private void assertExists(String zone, String name, String... members)
    {
        String longName = longName(name);
        // Check authority exists
        assertTrue(this.authorityService.authorityExists(longName));
        // Check in correct zone
        assertTrue(this.authorityService.getAuthorityZones(longName).contains(
                AuthorityService.ZONE_AUTH_EXT_PREFIX + zone));
        if (AuthorityType.getAuthorityType(longName).equals(AuthorityType.GROUP))
        {
            // Check groups have expected members
            Set memberSet = new HashSet(members.length * 2);
            for (String member : members)
            {
                memberSet.add(longName(member));
            }
            assertEquals(memberSet, this.authorityService.getContainedAuthorities(null, longName, true));
        }
        else
        {
            // Check users exist as persons
            assertTrue(this.personService.personExists(name));
            // Check case matches
            assertEquals(this.personService.getUserIdentifier(name), name);
            
            // Check the person exist.
            NodeRef person = personService.getPerson(name, false);
            assertNotNull("Person for "+name+" should exist", person);
            
            // Check the home folder exists or not.
            NodeRef homeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER));
            if (homeFolderCreationEager)
            {
                assertNotNull("Home folder for "+name+" should exist", homeFolder);
            }
            else
            {
                assertNull("Home folder for "+name+" should not exist", homeFolder);
            }
        }
    }
    /**
     * Perform all the necessary assertions to ensure that an authority does not exist.
     * 
     * @param name
     *            the name
     */
    private void assertNotExists(String name)
    {
        String longName = longName(name);
        // Check authority does not exist
        assertFalse(this.authorityService.authorityExists(longName));
        // Check there is no zone
        assertNull(this.authorityService.getAuthorityZones(longName));
        if (!AuthorityType.getAuthorityType(longName).equals(AuthorityType.GROUP))
        {
            // Check person does not exist
            assertFalse(this.personService.personExists(name));
        }
    }
    /**
     * Asserts that a person's email has the expected value.
     * 
     * @param personName
     *            the person name
     * @param email
     *            the email
     */
    private void assertEmailEquals(String personName, String email)
    {
        NodeRef personRef = this.personService.getPerson(personName, false);
        assertEquals(email, this.nodeService.getProperty(personRef, ContentModel.PROP_EMAIL));
    }
    /**
     * Asserts that a group's display name has the expected value.
     * 
     * @param personName
     *            the person name
     * @param email
     *            the email
     */
    private void assertGroupDisplayNameEquals(String name, String displayName)
    {
        assertEquals(displayName, this.authorityService.getAuthorityDisplayName(longName(name)));
    }
    /**
     * Converts the given short name to a full authority name, assuming that those short names beginning with 'G'
     * correspond to groups and all others correspond to users.
     * 
     * @param shortName
     *            the short name
     * @return the full authority name
     */
    private String longName(String shortName)
    {
        return this.authorityService.getName(shortName.toLowerCase().startsWith("g") ? AuthorityType.GROUP
                : AuthorityType.USER, shortName);
    }
    /**
     * A Mock {@link UserRegistry} that returns a fixed set of users and groups.
     */
    public static class MockUserRegistry implements UserRegistry
    {
        /** The zone id. */
        private String zoneId;
        /** The persons. */
        private Collection persons;
        /** The groups. */
        private Collection groups;
        /**
         * Instantiates a new mock user registry.
         * 
         * @param zoneId
         *            the zone id
         * @param persons
         *            the persons
         * @param groups
         *            the groups
         */
        public MockUserRegistry(String zoneId, Collection persons, Collection groups)
        {
            this.zoneId = zoneId;
            this.persons = persons;
            this.groups = groups;
        }
        /**
         * Modifies the state to match the arguments. Compares new with old and records new modification dates only for
         * changes.
         * 
         * @param persons
         *            the persons
         * @param groups
         *            the groups
         */
        public void updateState(Collection persons, Collection groups)
        {
            List newPersons = new ArrayList(this.persons);
            mergeNodeDescriptions(newPersons, persons, ContentModel.PROP_USERNAME, false);
            this.persons = newPersons;
            List newGroups = new ArrayList(this.groups);
            mergeNodeDescriptions(newGroups, groups, ContentModel.PROP_AUTHORITY_NAME, true);
            this.groups = newGroups;
        }
        /**
         * Merges together an old and new list of node descriptions. Retains the old node with its old modification date
         * if it is the same in the new list, otherwises uses the node from the new list.
         * 
         * @param oldNodes
         *            the old node list
         * @param newNodes
         *            the new node list
         * @param idProp
         *            the name of the ID property
         * @param caseSensitive
         *            are IDs case sensitive?
         */
        private void mergeNodeDescriptions(List oldNodes, Collection newNodes,
                QName idProp, boolean caseSensitive)
        {
            Map nodeMap = new LinkedHashMap(newNodes.size() * 2);
            for (NodeDescription node : newNodes)
            {
                String id = (String) node.getProperties().get(idProp);
                if (!caseSensitive)
                {
                    id = id.toLowerCase();
                }
                nodeMap.put(id, node);
            }
            for (int i = 0; i < oldNodes.size(); i++)
            {
                NodeDescription oldNode = oldNodes.get(i);
                String id = (String) oldNode.getProperties().get(idProp);
                if (!caseSensitive)
                {
                    id = id.toLowerCase();
                }
                NodeDescription newNode = nodeMap.remove(id);
                if (newNode == null)
                {
                    oldNodes.remove(i);
                    i--;
                }
                else if (!oldNode.getProperties().equals(newNode.getProperties())
                        || !oldNode.getChildAssociations().equals(newNode.getChildAssociations()))
                {
                    oldNodes.set(i, newNode);
                }
            }
            oldNodes.addAll(nodeMap.values());
        }
        /**
         * Instantiates a new mock user registry.
         * 
         * @param zoneId
         *            the zone id
         * @param persons
         *            the persons
         * @param groups
         *            the groups
         */
        public MockUserRegistry(String zoneId, NodeDescription[] persons, NodeDescription[] groups)
        {
            this(zoneId, Arrays.asList(persons), Arrays.asList(groups));
        }
        /**
         * Gets the zone id.
         * 
         * @return the zoneId
         */
        public String getZoneId()
        {
            return this.zoneId;
        }
        /*
         * (non-Javadoc)
         * @see org.alfresco.repo.security.sync.UserRegistry#getGroupNames()
         */
        public Collection getGroupNames()
        {
            List groupNames = new LinkedList();
            for (NodeDescription group : this.groups)
            {
                groupNames.add((String) group.getProperties().get(ContentModel.PROP_AUTHORITY_NAME));
            }
            return groupNames;
        }
        /*
         * (non-Javadoc)
         * @see org.alfresco.repo.security.sync.UserRegistry#getPersonNames()
         */
        public Collection getPersonNames()
        {
            List personNames = new LinkedList();
            for (NodeDescription person : this.persons)
            {
                personNames.add((String) person.getProperties().get(ContentModel.PROP_USERNAME));
            }
            return personNames;
        }
        /*
         * (non-Javadoc)
         * @see org.alfresco.repo.security.sync.UserRegistry#getGroups(java.util.Date)
         */
        public Collection getGroups(Date modifiedSince)
        {
            return filterNodeDescriptions(this.groups, modifiedSince);
        }
        /**
         * Filters the given list of node descriptions, retaining only those with a modification date greater than the
         * given date.
         * 
         * @param nodes
         *            the list of nodes
         * @param modifiedSince
         *            the modified date
         * @return the filter list of nodes
         */
        private Collection filterNodeDescriptions(Collection nodes, Date modifiedSince)
        {
            if (modifiedSince == null)
            {
                return nodes;
            }
            List filteredNodes = new LinkedList();
            for (NodeDescription node : nodes)
            {
                Date modified = node.getLastModified();
                if (modifiedSince.compareTo(modified) < 0)
                {
                    filteredNodes.add(node);
                }
            }
            return filteredNodes;
        }
        /*
         * (non-Javadoc)
         * @see org.alfresco.repo.security.sync.UserRegistry#getPersons(java.util.Date)
         */
        public Collection getPersons(Date modifiedSince)
        {
            return filterNodeDescriptions(this.persons, modifiedSince);
        }
        /*
         * (non-Javadoc)
         * @see org.alfresco.repo.security.sync.UserRegistry#getPersonMappedProperties()
         */
        public Set getPersonMappedProperties()
        {
            return new HashSet(Arrays.asList(new QName[]
            {
                ContentModel.PROP_USERNAME, ContentModel.PROP_FIRSTNAME, ContentModel.PROP_LASTNAME,
                ContentModel.PROP_EMAIL, ContentModel.PROP_ORGID, ContentModel.PROP_ORGANIZATION,
                ContentModel.PROP_HOME_FOLDER_PROVIDER
            }));
        }
    }
    /**
     * An {@link ChildApplicationContextManager} for a chain of application contexts containing mock user registries.
     */
    public static class MockApplicationContextManager implements ChildApplicationContextManager
    {
        /** The contexts. */
        private Map contexts = Collections.emptyMap();
        /**
         * Sets the user registries.
         * 
         * @param registries
         *            the new user registries
         */
        public void setUserRegistries(MockUserRegistry... registries)
        {
            this.contexts = new LinkedHashMap(registries.length * 2);
            for (MockUserRegistry registry : registries)
            {
                StaticApplicationContext context = new StaticApplicationContext();
                context.getDefaultListableBeanFactory().registerSingleton("userRegistry", registry);
                this.contexts.put(registry.getZoneId(), context);
            }
        }
        /**
         * Removes the application context for the given zone ID (simulating a change in the authentication chain).
         * 
         * @param zoneId
         *            the zone id
         */
        public void removeZone(String zoneId)
        {
            this.contexts.remove(zoneId);
        }
        /**
         * Updates the state of the given zone ID, oopying in new modification dates only where changes have been made.
         * 
         * @param zoneId
         *            the zone id
         * @param persons
         *            the new list of persons
         * @param groups
         *            the new list of groups
         */
        public void updateZone(String zoneId, NodeDescription[] persons, NodeDescription[] groups)
        {
            ApplicationContext context = this.contexts.get(zoneId);
            MockUserRegistry registry = (MockUserRegistry) context.getBean("userRegistry");
            registry.updateState(Arrays.asList(persons), Arrays.asList(groups));
        }
        /*
         * (non-Javadoc)
         * @see
         * org.alfresco.repo.management.subsystems.ChildApplicationContextManager#getApplicationContext(java.lang.String
         * )
         */
        public ApplicationContext getApplicationContext(String id)
        {
            return this.contexts.get(id);
        }
        /*
         * (non-Javadoc)
         * @see org.alfresco.repo.management.subsystems.ChildApplicationContextManager#getInstanceIds()
         */
        public Collection getInstanceIds()
        {
            return this.contexts.keySet();
        }
    }
    /**
     * A collection whose iterator returns randomly generated persons.
     */
    public class RandomPersonCollection extends AbstractCollection
    {
        /** The collection size. */
        private final int size;
        /**
         * The Constructor.
         * 
         * @param size
         *            the collection size
         */
        public RandomPersonCollection(int size)
        {
            this.size = size;
        }
        /*
         * (non-Javadoc)
         * @see java.util.AbstractCollection#iterator()
         */
        @Override
        public Iterator iterator()
        {
            return new Iterator()
            {
                private int pos;
                public boolean hasNext()
                {
                    return this.pos < RandomPersonCollection.this.size;
                }
                public NodeDescription next()
                {
                    this.pos++;
                    return newPerson("U" + GUID.generate());
                }
                public void remove()
                {
                    throw new UnsupportedOperationException();
                }
            };
        }
        /*
         * (non-Javadoc)
         * @see java.util.AbstractCollection#size()
         */
        @Override
        public int size()
        {
            return this.size;
        }
    }
    /**
     * A collection whose iterator returns randomly generated groups with random associations to a given list of
     * persons.
     */
    public class RandomGroupCollection extends AbstractCollection
    {
        /** Use a fixed seed to give this class deterministic behaviour */
        private Random generator = new Random(1628876500L);
        /** The collection size. */
        private final int size;
        /** The authorities. */
        private final List authorities;
        /**
         * The Constructor.
         * 
         * @param size
         *            the collection size
         * @param authorities
         *            the authorities
         */
        public RandomGroupCollection(int size, Set authorities)
        {
            this.size = size;
            this.authorities = new ArrayList(authorities);
        }
        /**
         * The Constructor.
         * 
         * @param size
         *            the collection size
         * @param authorities
         *            the authorities
         */
        public RandomGroupCollection(int size, Collection persons)
        {
            this.size = size;
            this.authorities = new ArrayList(persons.size());
            for (NodeDescription nodeDescription : persons)
            {
                this.authorities.add((String) nodeDescription.getProperties().get(ContentModel.PROP_USERNAME));
            }
        }
        /*
         * (non-Javadoc)
         * @see java.util.AbstractCollection#iterator()
         */
        @Override
        public Iterator iterator()
        {
            return new Iterator()
            {
                private int pos;
                public boolean hasNext()
                {
                    return this.pos < RandomGroupCollection.this.size;
                }
                public NodeDescription next()
                {
                    this.pos++;
                    // Just for fun, make the last group one that includes ALL authorities!
                    String[] authorityNames = new String[this.pos == RandomGroupCollection.this.size ? RandomGroupCollection.this.size : 17];
                    for (int i = 0; i < authorityNames.length; i++)
                    {
                        // Choose an authority at random from the list of known authorities
                        int index = this.pos == RandomGroupCollection.this.size ? i : RandomGroupCollection.this.generator.nextInt(RandomGroupCollection.this.authorities
                                .size());
                        authorityNames[i] = ChainingUserRegistrySynchronizerTest.this.authorityService
                                .getShortName((String) RandomGroupCollection.this.authorities.get(index));
                    }
                    NodeDescription group = newGroup("G" + GUID.generate(), authorityNames);
                    // Make this group a candidate for adding to other groups
                    RandomGroupCollection.this.authorities.add((String) group.getProperties().get(
                            ContentModel.PROP_AUTHORITY_NAME));
                    return group;
                }
                public void remove()
                {
                    throw new UnsupportedOperationException();
                }
            };
        }
        /*
         * (non-Javadoc)
         * @see java.util.AbstractCollection#size()
         */
        @Override
        public int size()
        {
            return this.size;
        }
    }
}