Example usage for java.lang String CASE_INSENSITIVE_ORDER

List of usage examples for java.lang String CASE_INSENSITIVE_ORDER

Introduction

In this page you can find the example usage for java.lang String CASE_INSENSITIVE_ORDER.

Prototype

Comparator CASE_INSENSITIVE_ORDER

To view the source code for java.lang String CASE_INSENSITIVE_ORDER.

Click Source Link

Document

A Comparator that orders String objects as by compareToIgnoreCase .

Usage

From source file:com.hichinaschool.flashcards.anki.CardEditor.java

private void actualizeTagDialog(StyledDialog ad) {
    TreeSet<String> tags = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
    for (String tag : mCol.getTags().all()) {
        tags.add(tag);//from   w  w  w . j a va2s .c  om
    }
    tags.addAll(selectedTags);
    int len = tags.size();
    allTags = new String[len];
    boolean[] checked = new boolean[len];
    int i = 0;
    for (String t : tags) {
        allTags[i++] = t;
        if (selectedTags.contains(t)) {
            checked[i - 1] = true;
        }
    }
    ad.setMultiChoiceItems(allTags, checked, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface arg0, int which) {
            String tag = allTags[which];
            if (selectedTags.contains(tag)) {
                // Log.i(AnkiDroidApp.TAG, "unchecked tag: " + tag);
                selectedTags.remove(tag);
            } else {
                // Log.i(AnkiDroidApp.TAG, "checked tag: " + tag);
                selectedTags.add(tag);
            }
        }
    });
}

From source file:weave.servlets.AdminService.java

/**
 * getSortedUniqueValues/*from w w  w .  j a  v  a2s .co m*/
 * 
 * @param values
 *            A list of string values which may contain duplicates.
 * @param moveEmptyStringToEnd
 *            If set to true and "" is at the front of the list, "" is moved
 *            to the end.
 * @return A sorted list of unique values found in the given list.
 */
private List<String> getSortedUniqueValues(List<String> values, boolean moveEmptyStringToEnd) {
    Set<String> uniqueValues = new HashSet<String>();
    uniqueValues.addAll(values);
    Vector<String> result = new Vector<String>(uniqueValues);
    Collections.sort(result, String.CASE_INSENSITIVE_ORDER);
    // if empty string is at beginning of sorted list, move it to the end of
    // the list
    if (moveEmptyStringToEnd && result.size() > 0 && result.get(0).equals(""))
        result.add(result.remove(0));
    return result;
}

From source file:com.mirth.connect.server.controllers.DefaultConfigurationController.java

private void saveConfigurationProperties(Map<String, ConfigurationProperty> map) throws ControllerException {
    try {/* www .  ja va 2  s.  co  m*/
        PropertiesConfiguration configurationMapProperties = new PropertiesConfiguration();
        configurationMapProperties.setDelimiterParsingDisabled(true);
        configurationMapProperties.setListDelimiter((char) 0);
        configurationMapProperties.clear();

        PropertiesConfigurationLayout layout = configurationMapProperties.getLayout();

        Map<String, ConfigurationProperty> sortedMap = new TreeMap<String, ConfigurationProperty>(
                String.CASE_INSENSITIVE_ORDER);
        sortedMap.putAll(map);

        for (Entry<String, ConfigurationProperty> entry : sortedMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue().getValue();
            String comment = entry.getValue().getComment();

            if (StringUtils.isNotBlank(key)) {
                configurationMapProperties.addProperty(key, value);
                layout.setComment(key, StringUtils.isBlank(comment) ? null : comment);
            }
        }

        configurationMapProperties.save(new File(configurationFile));
    } catch (Exception e) {
        throw new ControllerException(e);
    }
}

From source file:org.apache.usergrid.persistence.Schema.java

/** @return entity properties from columns as a map */
public static Map<String, Object> deserializeEntityProperties(Map<String, ByteBuffer> columns, boolean checkId,
        boolean checkRequired) {

    if (columns == null) {
        return null;
    }//from   w  w w.ja  v a 2  s.co m

    String entityType = string(columns.get(PROPERTY_TYPE));
    if (entityType == null) {
        logger.debug("deserializeEntityProperties(): No type for entity found, entity probably doesn't exist");
        return null;
    }
    if (checkId && !columns.containsKey(PROPERTY_UUID)) {
        logger.error("No id for entity ( {} ) found!", entityType);
        return null;
    }

    if (checkRequired) {
        Set<String> required_properties = Schema.getDefaultSchema().getRequiredProperties(entityType);
        if (required_properties != null) {
            for (String property_name : required_properties) {
                if (!columns.containsKey(property_name)) {
                    logger.error("Entity (" + entityType + ") missing required property: " + property_name,
                            new Throwable());
                    return null;
                }
            }
        }
    }

    Map<String, Object> properties_map = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
    for (Entry<String, ByteBuffer> column : columns.entrySet()) {
        String propertyName = column.getKey();
        Object propertyValue = deserializeEntityProperty(entityType, propertyName, column.getValue());
        properties_map.put(propertyName, propertyValue);
    }
    return properties_map;
}

From source file:com.afwsamples.testdpc.policy.PolicyManagementFragment.java

/**
 * Shows a list of account types that is disabled for account management.
 *//*from  w  w  w  . ja va  2  s.c o m*/
private void showDisableAccountTypeList() {
    if (getActivity() == null || getActivity().isFinishing()) {
        return;
    }
    String[] disabledAccountTypeList = mDevicePolicyManager.getAccountTypesWithManagementDisabled();
    Arrays.sort(disabledAccountTypeList, String.CASE_INSENSITIVE_ORDER);
    if (disabledAccountTypeList == null || disabledAccountTypeList.length == 0) {
        showToast(R.string.no_disabled_account);
    } else {
        new AlertDialog.Builder(getActivity()).setTitle(R.string.list_of_disabled_account_types)
                .setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1,
                        android.R.id.text1, disabledAccountTypeList), null)
                .setPositiveButton(android.R.string.ok, null).show();
    }
}

From source file:pcgen.core.GameMode.java

public void addHiddenType(Class<?> cl, String s) {
    Set<String> set = hiddenTypes.computeIfAbsent(cl, k -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER));
    set.add(s);/*w ww.  j  a  va2  s .c o m*/
}

From source file:org.alfresco.repo.security.sync.TenantChainingUserRegistrySynchronizer.java

/**
 * Synchronizes local groups and users with a {@link UserRegistry} for a
 * particular zone, optionally handling deletions.
 * //w  w w  . jav  a  2s  . com
 * @param zone
 *            the zone id. This identifier is used to tag all created groups
 *            and users, so that in the future we can tell those that have
 *            been deleted from the registry.
 * @param userRegistry
 *            the user registry for the zone.
 * @param forceUpdate
 *            Should the complete set of users and groups be updated /
 *            created locally or just those known to have changed since the
 *            last sync? When <code>true</code> then <i>all</i> users and
 *            groups are queried from the user registry and updated locally.
 *            When <code>false</code> then each source is only queried for
 *            those users and groups modified since the most recent
 *            modification date of all the objects last queried from that
 *            same source.
 * @param isFullSync
 *            Should a complete set of user and group IDs be queried from
 *            the user registries in order to determine deletions? This
 *            parameter is independent of <code>force</code> as a separate
 *            query is run to process updates.
 * @param splitTxns
 *            Can the modifications to Alfresco be split across multiple
 *            transactions for maximum performance? If <code>true</code>,
 *            users and groups are created/updated in batches for increased
 *            performance. If <code>false</code>, all users and groups are
 *            processed in the current transaction. This is required if
 *            calling synchronously (e.g. in response to an authentication
 *            event in the same transaction).
 * @param visitedZoneIds
 *            the set of zone ids already processed. These zones have
 *            precedence over the current zone when it comes to group name
 *            'collisions'. If a user or group is queried that already
 *            exists locally but is tagged with one of the zones in this
 *            set, then it will be ignored as this zone has lower priority.
 * @param allZoneIds
 *            the set of all zone ids in the authentication chain. Helps us
 *            work out whether the zone information recorded against a user
 *            or group is invalid for the current authentication chain and
 *            whether the user or group needs to be 're-zoned'.
 */
private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate,
        boolean isFullSync, boolean splitTxns, final Set<String> visitedZoneIds, final Set<String> allZoneIds) {
    // Create a prefixed zone ID for use with the authority service
    final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;

    // Ensure that the zoneId exists before multiple threads start using it
    this.transactionService.getRetryingTransactionHelper()
            .doInTransaction(new RetryingTransactionCallback<Void>() {
                @Override
                public Void execute() throws Throwable {
                    authorityService.getOrCreateZone(zoneId);
                    return null;
                }
            }, false, splitTxns);

    // The set of zones we associate with new objects (default plus registry
    // specific)
    final Set<String> zoneSet = getZones(zoneId);

    long lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);

    if (TenantChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            TenantChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all groups from user registry '" + zone + "'");
        } else {
            TenantChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving groups changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // First, analyze the group structure. Create maps of authorities to
    // their parents for associations to create
    // and delete. Also deal with 'overlaps' with other zones in the
    // authentication chain.
    final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(
            zone + " Group Analysis", this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher,
            TenantChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class Analyzer extends BaseBatchProcessWorker<NodeDescription> {
        private final Map<String, String> groupsToCreate = new TreeMap<String, String>();
        private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap();
        private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap();
        private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>();
        private List<String> personsProcessed = new LinkedList<String>();
        private Set<String> allZonePersons = Collections.emptySet();
        private Set<String> deletionCandidates;

        private long latestTime;

        public Analyzer(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public Set<String> getDeletionCandidates() {
            return this.deletionCandidates;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(final NodeDescription group) throws Throwable {
            PropertyMap groupProperties = group.getProperties();
            String tenantDomain = (String) groupProperties.get(ContentModel.PROP_ORGANIZATION);
            logger.debug("Process group: " + groupProperties.get(ContentModel.PROP_AUTHORITY_NAME)
                    + (tenantDomain == null ? "" : ("/" + tenantDomain)));

            if (tenantDomain != null) {
                if (!isTenantEnabled(tenantDomain)) {
                    return;
                }
                AuthenticationUtil.runAs(new RunAsWork<Void>() {
                    @Override
                    public Void doWork() throws Exception {
                        processInTenantMode(group);
                        return null;
                    }
                }, SEIPTenantIntegration.getSystemUserByTenantId(tenantDomain));
            } else {
                processInTenantMode(group);
            }
        }

        private void processInTenantMode(NodeDescription group) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupShortName = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getShortName(groupName);
            Set<String> groupZones = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(groupName);

            if (groupZones == null) {
                // The group did not exist at all
                updateGroup(group, false);
            } else {
                // Check whether the group is in any of the authentication
                // chain zones
                Set<String> intersection = new TreeSet<String>(groupZones);
                intersection.retainAll(allZoneIds);
                // Check whether the group is in any of the higher priority
                // authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);

                if (groupZones.contains(zoneId)) {
                    // The group already existed in this zone: update the
                    // group
                    updateGroup(group, true);
                } else if (!visited.isEmpty()) {
                    // A group that exists in a different zone with higher
                    // precedence
                    return;
                } else if (!allowDeletions || intersection.isEmpty()) {
                    // Deletions are disallowed or the group exists, but not
                    // in a zone that's in the authentication
                    // chain. May be due to upgrade or zone changes. Let's
                    // re-zone them
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName
                                + "'. This group will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(groupName, groupZones, zoneSet);

                    // The group now exists in this zone: update the group
                    updateGroup(group, true);
                } else {
                    // The group existed, but in a zone with lower
                    // precedence
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Recreating occluded group '"
                                + groupShortName
                                + "'. This group was previously created through synchronization with a lower priority user registry.");
                    }
                    TenantChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName);

                    // create the group
                    updateGroup(group, false);
                }
            }

            synchronized (this) {
                // Maintain the last modified date
                Date groupLastModified = group.getLastModified();
                if (groupLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
                }
            }
        }

        // Recursively walks and caches the authorities relating to and from
        // this group so that we can later detect potential cycles
        private Set<String> getContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // First, recurse to the parent most authorities
            for (String parent : TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getContainingAuthorities(null, groupName, true)) {
                getContainedAuthorities(parent);
            }

            // Now descend on unprocessed parents.
            return cacheContainedAuthorities(groupName);
        }

        private Set<String> cacheContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // Descend on unprocessed parents.
            children = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getContainedAuthorities(null, groupName, true);
            this.finalGroupChildAssocs.put(groupName, children);

            for (String child : children) {
                if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) {
                    cacheContainedAuthorities(child);
                }
            }
            return children;
        }

        private synchronized void updateGroup(NodeDescription group, boolean existed) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
            if (groupDisplayName == null) {
                groupDisplayName = TenantChainingUserRegistrySynchronizer.this.authorityService
                        .getShortName(groupName);
            }

            // Divide the child associations into person and group
            // associations, dealing with case sensitivity
            Set<String> newChildPersons = newPersonSet();
            Set<String> newChildGroups = new TreeSet<String>();

            for (String child : group.getChildAssociations()) {
                if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                    newChildPersons.add(child);
                } else {
                    newChildGroups.add(child);
                }
            }

            // Account for differences if already existing
            if (existed) {
                // Update the display name now
                TenantChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName,
                        groupDisplayName);

                // Work out the association differences
                for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) {
                    if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                        if (!newChildPersons.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    } else {
                        if (!newChildGroups.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    }
                }
            }
            // Mark as created if new
            else {
                // Make sure each group to be created features in the
                // association deletion map (as these are handled in the
                // same phase)
                recordParentAssociationDeletion(groupName, null);
                this.groupsToCreate.put(groupName, groupDisplayName);
            }

            // Create new associations
            for (String child : newChildPersons) {
                // Make sure each person with association changes features
                // as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
            for (String child : newChildGroups) {
                // Make sure each group with association changes features as
                // a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
        }

        private void recordParentAssociationDeletion(String child, String parent) {
            Map<String, Set<String>> parentAssocs;
            if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                parentAssocs = this.personParentAssocsToDelete;
            } else {
                // Reflect the change in the map of final group associations
                // (for cycle detection later)
                parentAssocs = this.groupParentAssocsToDelete;
                if (parent != null) {
                    Set<String> children = this.finalGroupChildAssocs.get(parent);
                    children.remove(child);
                }
            }
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void recordParentAssociationCreation(String child, String parent) {
            Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER
                    ? this.personParentAssocsToCreate
                    : this.groupParentAssocsToCreate;
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void validateGroupParentAssocsToCreate() {
            Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String group = entry.getKey();
                Set<String> parents = entry.getValue();
                Deque<String> visited = new LinkedList<String>();
                Iterator<String> j = parents.iterator();
                while (j.hasNext()) {
                    String parent = j.next();
                    visited.add(parent);
                    if (validateAuthorityChildren(visited, group)) {
                        // The association validated - commit it
                        Set<String> children = finalGroupChildAssocs.get(parent);
                        if (children == null) {
                            children = new TreeSet<String>();
                            finalGroupChildAssocs.put(parent, children);
                        }
                        children.add(group);
                    } else {
                        // The association did not validate - prune it out
                        if (logger.isWarnEnabled()) {
                            TenantChainingUserRegistrySynchronizer.logger.warn("Not adding group '"
                                    + TenantChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(group)
                                    + "' to group '"
                                    + TenantChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent)
                                    + "' as this creates a cyclic relationship");
                        }
                        j.remove();
                    }
                    visited.removeLast();
                }
                if (parents.isEmpty()) {
                    i.remove();
                }
            }

            // Sort the group associations in parent-first order (root
            // groups first) to minimize reindexing overhead
            Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>(
                    this.groupParentAssocsToCreate.size() * 2);
            Deque<String> visited = new LinkedList<String>();
            for (String authority : this.groupParentAssocsToCreate.keySet()) {
                visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate,
                        sortedGroupAssociations);
            }

            this.groupParentAssocsToCreate = sortedGroupAssociations;
        }

        private boolean validateAuthorityChildren(Deque<String> visited, String authority) {
            if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                return true;
            }
            if (visited.contains(authority)) {
                return false;
            }
            visited.add(authority);
            try {
                Set<String> children = this.finalGroupChildAssocs.get(authority);
                if (children != null) {
                    for (String child : children) {
                        if (!validateAuthorityChildren(visited, child)) {
                            return false;
                        }
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        /**
         * Visits the given authority by recursively visiting its parents in
         * associationsOld and then adding the authority to associationsNew.
         * Used to sort associationsOld into 'parent-first' order to
         * minimize reindexing overhead.
         * 
         * @param visited
         *            The ancestors that form the path to the authority to
         *            visit. Allows detection of cyclic child associations.
         * @param authority
         *            the authority to visit
         * @param associationsOld
         *            the association map to sort
         * @param associationsNew
         *            the association map to add to in parent-first order
         */
        private boolean visitGroupParentAssocs(Deque<String> visited, String authority,
                Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) {
            if (visited.contains(authority)) {
                // Prevent cyclic paths (Shouldn't happen as we've already
                // validated)
                return false;
            }
            visited.add(authority);
            try {
                if (!associationsNew.containsKey(authority)) {
                    Set<String> oldParents = associationsOld.get(authority);
                    if (oldParents != null) {
                        Set<String> newParents = new TreeSet<String>();

                        for (String parent : oldParents) {
                            if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) {
                                newParents.add(parent);
                            }
                        }
                        associationsNew.put(authority, newParents);
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        private Set<String> newPersonSet() {
            return TenantChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeSet<String>()
                    : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        }

        private Map<String, Set<String>> newPersonMap() {
            return TenantChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeMap<String, Set<String>>()
                    : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER);
        }

        private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) {
            if (toRetain.isEmpty()) {
                parentAssocs.clear();
                return;
            }
            Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator();
            StringBuilder groupList = null;
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String child = entry.getKey();
                if (!toRetain.contains(child)) {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        if (groupList == null) {
                            groupList = new StringBuilder(1024);
                        } else {
                            groupList.setLength(0);
                        }
                        for (String parent : entry.getValue()) {
                            if (groupList.length() > 0) {
                                groupList.append(", ");
                            }
                            groupList.append('\'')
                                    .append(TenantChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent))
                                    .append('\'');

                        }
                        TenantChainingUserRegistrySynchronizer.logger.debug("Ignoring non-existent member '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(child)
                                + "' in groups {" + groupList.toString() + "}. RunAs user:"
                                + AuthenticationUtil.getRunAsUser());
                    }
                    i.remove();
                }
            }
        }

        public void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) {
            // If we got back some groups, we have to cross reference them
            // with the set of known authorities
            if (isFullSync || !this.groupParentAssocsToDelete.isEmpty()
                    || !this.groupParentAssocsToDelete.isEmpty()) {
                processGroupSynchInTenantMode(userRegistry, isFullSync, splitTxns);
            }
        }

        private void processGroupSynchInTenantMode(UserRegistry userRegistry, boolean isFullSync,
                boolean splitTxns) {
            final Set<String> allZonePersons = newPersonSet();
            final Set<String> allZoneGroups = new TreeSet<String>();
            final String tenantId = SEIPTenantIntegration.getTenantId();
            // Add in current set of known authorities
            TenantChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper()
                    .doInTransaction(new RetryingTransactionCallback<Void>() {
                        public Void execute() throws Throwable {
                            allZonePersons.addAll(TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .getAllAuthoritiesInZone(zoneId, AuthorityType.USER));
                            allZoneGroups.addAll(TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP));
                            return null;
                        }
                    }, true, splitTxns);

            allZoneGroups.addAll(this.groupsToCreate.keySet());

            // Prune our set of authorities according to deletions
            if (isFullSync) {
                final Set<String> personDeletionCandidates = newPersonSet();
                personDeletionCandidates.addAll(allZonePersons);

                final Set<String> groupDeletionCandidates = new TreeSet<String>();
                groupDeletionCandidates.addAll(allZoneGroups);

                this.deletionCandidates = new TreeSet<String>();

                for (String person : userRegistry.getPersonNames()) {
                    personDeletionCandidates.remove(person);
                }

                for (String group : userRegistry.getGroupNames()) {
                    groupDeletionCandidates.remove(group);
                }

                this.deletionCandidates = new TreeSet<String>();
                this.deletionCandidates.addAll(personDeletionCandidates);
                this.deletionCandidates.addAll(groupDeletionCandidates);
                if (allowDeletions) {
                    allZonePersons.removeAll(personDeletionCandidates);
                    allZoneGroups.removeAll(groupDeletionCandidates);
                } else {
                    if (!personDeletionCandidates.isEmpty()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn(
                                "The following missing users are not being deleted as allowDeletions == false");
                        for (String person : personDeletionCandidates) {
                            TenantChainingUserRegistrySynchronizer.logger.warn("    " + person);
                        }
                    }
                    if (!groupDeletionCandidates.isEmpty()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn(
                                "The following missing groups are not being deleted as allowDeletions == false");
                        for (String group : groupDeletionCandidates) {
                            TenantChainingUserRegistrySynchronizer.logger.warn("    " + group);
                        }
                    }

                    // Complete association deletion information by
                    // scanning deleted groups
                    BatchProcessor<String> groupScanner = new BatchProcessor<String>(
                            zone + " Missing Authority Scanning",
                            TenantChainingUserRegistrySynchronizer.this.transactionService
                                    .getRetryingTransactionHelper(),
                            this.deletionCandidates, TenantChainingUserRegistrySynchronizer.this.workerThreads,
                            20, TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                            TenantChainingUserRegistrySynchronizer.logger,
                            TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                    groupScanner.process(new BaseBatchProcessWorker<String>() {

                        @Override
                        public String getIdentifier(String entry) {
                            return entry;
                        }

                        @Override
                        public void process(final String authority) throws Throwable {

                            AuthenticationUtil.runAs(new RunAsWork<Void>() {

                                @Override
                                public Void doWork() throws Exception {
                                    proceesInTenantMode(zoneId, authority);
                                    return null;
                                }
                            }, SEIPTenantIntegration.getSystemUserByTenantId(tenantId));

                        }

                        private void proceesInTenantMode(final String zoneId, String authority) {
                            // Disassociate it from this zone, allowing
                            // it to be reclaimed by something further
                            // down the chain
                            TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .removeAuthorityFromZones(authority, Collections.singleton(zoneId));

                            // For groups, remove all members
                            if (AuthorityType.getAuthorityType(authority) != AuthorityType.USER) {
                                String groupShortName = TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authority);
                                String groupDisplayName = TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getAuthorityDisplayName(authority);
                                NodeDescription dummy = new NodeDescription(groupShortName + " (Deleted)");
                                PropertyMap dummyProperties = dummy.getProperties();
                                dummyProperties.put(ContentModel.PROP_AUTHORITY_NAME, authority);
                                if (groupDisplayName != null) {
                                    dummyProperties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME,
                                            groupDisplayName);
                                }
                                updateGroup(dummy, true);
                            }
                        }
                    }, splitTxns);

                }
            }

            // Prune the group associations now that we have complete
            // information
            this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups);
            logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups);
            this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups);

            // Pruning person associations will have to wait until we
            // have passed over all persons and built up
            // this set
            this.allZonePersons = allZonePersons;

            if (!this.groupParentAssocsToDelete.isEmpty()) {
                // Create/update the groups and delete parent
                // associations to be deleted
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        zone + " Group Creation and Association Deletion",
                        TenantChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.groupParentAssocsToDelete.entrySet(),
                        TenantChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        TenantChainingUserRegistrySynchronizer.logger,
                        TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(final Map.Entry<String, Set<String>> entry) throws Throwable {

                        AuthenticationUtil.runAs(new RunAsWork<Void>() {

                            @Override
                            public Void doWork() throws Exception {
                                processInternal(zoneSet, entry.getKey());
                                return null;
                            }
                        }, SEIPTenantIntegration.getSystemUserByTenantId(tenantId));

                    }

                    private void processInternal(final Set<String> zoneSet, String child) {
                        String groupDisplayName = Analyzer.this.groupsToCreate.get(child);
                        if (groupDisplayName != null) {
                            String groupShortName = TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .getShortName(child);
                            if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                                TenantChainingUserRegistrySynchronizer.logger
                                        .debug("Creating group '" + groupShortName + "'");
                            }
                            // create the group
                            TenantChainingUserRegistrySynchronizer.this.authorityService.createAuthority(
                                    AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName,
                                    zoneSet);
                        } else {
                            // Maintain association deletions now. The
                            // creations will have to be done later once
                            // we have performed all the deletions in
                            // order to avoid creating cycles
                            maintainAssociationDeletions(child);
                        }
                    }
                }, splitTxns);
            }
        }

        public void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) {
            // First validate the group associations to be created for
            // potential cycles. Remove any offending association
            validateGroupParentAssocsToCreate();

            // Now go ahead and create the group associations
            if (!this.groupParentAssocsToCreate.isEmpty()) {
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        zone + " Group Association Creation",
                        TenantChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.groupParentAssocsToCreate.entrySet(),
                        TenantChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        TenantChainingUserRegistrySynchronizer.logger,
                        TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {

                        final String user = entry.getKey();
                        AuthenticationUtil.runAs(new RunAsWork<Void>() {

                            @Override
                            public Void doWork() throws Exception {
                                maintainAssociationCreations(user);
                                return null;
                            }
                        }, SEIPTenantIntegration.getSystemUser(user));
                    }
                }, splitTxns);
            }

            // Remove all the associations we have already dealt with
            this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed);

            // Filter out associations to authorities that simply can't
            // exist (and log if debugging is enabled)
            logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons);

            // Update associations to persons not updated themselves
            if (!this.personParentAssocsToDelete.isEmpty()) {
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        zone + " Person Association",
                        TenantChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.personParentAssocsToDelete.entrySet(),
                        TenantChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        TenantChainingUserRegistrySynchronizer.logger,
                        TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(final Map.Entry<String, Set<String>> entry) throws Throwable {
                        final String user = entry.getKey();
                        AuthenticationUtil.runAs(new RunAsWork<Void>() {

                            @Override
                            public Void doWork() throws Exception {
                                maintainAssociationDeletions(user);
                                maintainAssociationCreations(user);
                                return null;
                            }
                        }, SEIPTenantIntegration.getSystemUser(user));

                    }
                }, splitTxns);
            }
        }

        private void maintainAssociationDeletions(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName)
                    : this.groupParentAssocsToDelete.get(authorityName);
            if (parentsToDelete != null && !parentsToDelete.isEmpty()) {
                for (String parent : parentsToDelete) {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.debug("Removing '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' from group '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(parent)
                                + "'");
                    }
                    TenantChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent,
                            authorityName);
                }
            }

        }

        private void maintainAssociationCreations(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName)
                    : this.groupParentAssocsToCreate.get(authorityName);
            if (parents != null && !parents.isEmpty()) {
                if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    for (String groupName : parents) {
                        TenantChainingUserRegistrySynchronizer.logger.debug("Adding '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' to group '" + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(groupName)
                                + "'");
                    }
                }
                try {
                    TenantChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents,
                            authorityName);
                } catch (UnknownAuthorityException e) {
                    // Let's force a transaction retry if a parent doesn't
                    // exist. It may be because we are
                    // waiting for another worker thread to create it
                    throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e);
                } catch (InvalidNodeRefException e) {
                    // Another thread may have written the node, but it is
                    // not visible to this transaction
                    // See: ALF-5471: 'authorityMigration' patch can report
                    // 'Node does not exist'
                    throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e);
                }
            }
            // Remember that this person's associations have been maintained
            if (isPerson) {
                synchronized (this) {
                    this.personsProcessed.add(authorityName);
                }
            }
        }
    }

    final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis);
    int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns);

    groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns);

    // Process persons and their parent associations

    lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
    if (TenantChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            TenantChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all users from user registry '" + zone + "'");
        } else {
            TenantChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving users changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }
    final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(
            zone + " User Creation and Association", this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher,
            TenantChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class PersonWorker extends BaseBatchProcessWorker<NodeDescription> {
        private long latestTime;

        public PersonWorker(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(final NodeDescription person) throws Throwable {
            // Make a mutable copy of the person properties, since they get
            // written back to by person service
            HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>(
                    person.getProperties());
            String personName = (String) personProperties.get(ContentModel.PROP_USERNAME);
            String ou = (String) personProperties.get(ContentModel.PROP_ORGANIZATION);
            if (SEIPTenantIntegration.isValidTenant(ou) && !personName.endsWith(ou)) {
                personName += ("@" + ou);
                person.getProperties().put(ContentModel.PROP_USERNAME, personName);
            }
            final String personFullName = personName;
            logger.debug("Check user: " + personFullName);
            String tenantDomain = SEIPTenantIntegration.getTenantId(personFullName);
            if (tenantDomain != null && !tenantDomain.isEmpty()) {
                if (!isTenantEnabled(tenantDomain)) {
                    logger.debug("Tenant is missing/disabled for user: " + personFullName);
                    return;
                }
                logger.debug("Process user: " + personFullName);
                AuthenticationUtil.runAs(new RunAsWork<Void>() {
                    @Override
                    public Void doWork() throws Exception {
                        processInTenantMode(personFullName, person);
                        return null;
                    }
                }, SEIPTenantIntegration.getSystemUserByTenantId(tenantDomain));
            } else {
                logger.debug("Process user: " + personFullName);
                processInTenantMode(personFullName, person);
            }
        }

        private void processInTenantMode(String personName, NodeDescription person) {
            HashMap<QName, Serializable> personProperties = person.getProperties();
            // Make a mutable copy of the person properties, since they get
            // written back to by person service

            Set<String> zones = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(personName);
            if (zones == null) {
                // The person did not exist at all
                if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    TenantChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'");
                }

                TenantChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                        zoneSet);
            } else if (zones.contains(zoneId)) {
                // The person already existed in this zone: update the
                // person
                if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    TenantChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'");
                }

                TenantChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                        personProperties, false);
            } else {
                // Check whether the user is in any of the authentication
                // chain zones
                Set<String> intersection = new TreeSet<String>(zones);
                intersection.retainAll(allZoneIds);
                // Check whether the user is in any of the higher priority
                // authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);
                if (visited.size() > 0) {
                    // A person that exists in a different zone with higher
                    // precedence - ignore
                    return;
                }

                else if (!allowDeletions || intersection.isEmpty()) {
                    // The person exists, but in a different zone. Either
                    // deletions are disallowed or the zone is
                    // not in the authentication chain. May be due to
                    // upgrade or zone changes. Let's re-zone them
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName
                                + "'. This user will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(personName, zones, zoneSet);
                    TenantChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                            personProperties, false);
                } else {
                    // The person existed, but in a zone with lower
                    // precedence
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Recreating occluded user '"
                                + personName
                                + "'. This user was previously created through synchronization with a lower priority user registry.");
                    }
                    TenantChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
                    TenantChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                            zoneSet);
                }
            }

            // Maintain association deletions and creations in one shot
            // (safe to do this with persons as we can't
            // create cycles)
            groupAnalyzer.maintainAssociationDeletions(personName);
            groupAnalyzer.maintainAssociationCreations(personName);

            synchronized (this) {
                // Maintain the last modified date
                Date personLastModified = person.getLastModified();
                if (personLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
                }
            }
        }
    }

    PersonWorker persons = new PersonWorker(lastModifiedMillis);
    int personProcessedCount = personProcessor.process(persons, splitTxns);

    // Process those associations to persons who themselves have not been
    // updated
    groupAnalyzer.finalizeAssociations(userRegistry, splitTxns);

    // Only now that the whole tree has been processed is it safe to persist
    // the last modified dates
    long latestTime = groupAnalyzer.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }
    latestTime = persons.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }

    // Delete authorities if we have complete information for the zone
    Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates();
    if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) {
        BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(
                zone + " Authority Deletion", this.transactionService.getRetryingTransactionHelper(),
                deletionCandidates, this.workerThreads, 10, this.applicationEventPublisher,
                TenantChainingUserRegistrySynchronizer.logger, this.loggingInterval);
        class AuthorityDeleter extends BaseBatchProcessWorker<String> {
            private int personProcessedCount;
            private int groupProcessedCount;

            public int getPersonProcessedCount() {
                return this.personProcessedCount;
            }

            public int getGroupProcessedCount() {
                return this.groupProcessedCount;
            }

            public String getIdentifier(String entry) {
                return entry;
            }

            public void process(String authority) throws Throwable {
                if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger
                                .debug("Deleting user '" + authority + "'");
                    }
                    TenantChainingUserRegistrySynchronizer.this.personService.deletePerson(authority);
                    synchronized (this) {
                        this.personProcessedCount++;
                    }
                } else {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.debug("Deleting group '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authority)
                                + "'");
                    }
                    TenantChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
                    synchronized (this) {
                        this.groupProcessedCount++;
                    }
                }
            }
        }
        AuthorityDeleter authorityDeleter = new AuthorityDeleter();
        authorityDeletionProcessor.process(authorityDeleter, splitTxns);
        groupProcessedCount += authorityDeleter.getGroupProcessedCount();
        personProcessedCount += authorityDeleter.getPersonProcessedCount();
    }

    // Remember we have visited this zone
    visitedZoneIds.add(zoneId);

    if (TenantChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        TenantChainingUserRegistrySynchronizer.logger
                .info("Finished synchronizing users and groups with user registry '" + zone + "'");
        TenantChainingUserRegistrySynchronizer.logger
                .info(personProcessedCount + " user(s) and " + groupProcessedCount + " group(s) processed");
    }
}

From source file:org.alfresco.repo.security.sync.ChainingUserRegistrySynchronizer.java

/**
 * Synchronizes local groups and users with a {@link UserRegistry} for a particular zone, optionally handling
 * deletions./* w ww.ja  v a 2s .  c  o m*/
 * 
 * @param zone
 *            the zone id. This identifier is used to tag all created groups and users, so that in the future we can
 *            tell those that have been deleted from the registry.
 * @param userRegistry
 *            the user registry for the zone.
 * @param forceUpdate
 *            Should the complete set of users and groups be updated / created locally or just those known to have
 *            changed since the last sync? When <code>true</code> then <i>all</i> users and groups are queried from
 *            the user registry and updated locally. When <code>false</code> then each source is only queried for
 *            those users and groups modified since the most recent modification date of all the objects last
 *            queried from that same source.
 * @param isFullSync
 *            Should a complete set of user and group IDs be queried from the user registries in order to determine
 *            deletions? This parameter is independent of <code>force</code> as a separate query is run to process
 *            updates.
 * @param splitTxns
 *            Can the modifications to Alfresco be split across multiple transactions for maximum performance? If
 *            <code>true</code>, users and groups are created/updated in batches for increased performance. If
 *            <code>false</code>, all users and groups are processed in the current transaction. This is required if
 *            calling synchronously (e.g. in response to an authentication event in the same transaction).
 * @param visitedZoneIds
 *            the set of zone ids already processed. These zones have precedence over the current zone when it comes
 *            to group name 'collisions'. If a user or group is queried that already exists locally but is tagged
 *            with one of the zones in this set, then it will be ignored as this zone has lower priority.
 * @param allZoneIds
 *            the set of all zone ids in the authentication chain. Helps us work out whether the zone information
 *            recorded against a user or group is invalid for the current authentication chain and whether the user
 *            or group needs to be 're-zoned'.
 */
private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate,
        boolean isFullSync, boolean splitTxns, final Set<String> visitedZoneIds, final Set<String> allZoneIds) {
    // Create a prefixed zone ID for use with the authority service
    final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;

    // Batch Process Names
    final String reservedBatchProcessNames[] = { SyncProcess.GROUP_ANALYSIS.getTitle(zone),
            SyncProcess.USER_CREATION.getTitle(zone), SyncProcess.MISSING_AUTHORITY.getTitle(zone),
            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
            SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
            SyncProcess.PERSON_ASSOCIATION.getTitle(zone), SyncProcess.AUTHORITY_DELETION.getTitle(zone) };

    notifySyncDirectoryStart(zone, reservedBatchProcessNames);

    // Ensure that the zoneId exists before multiple threads start using it
    this.transactionService.getRetryingTransactionHelper()
            .doInTransaction(new RetryingTransactionCallback<Void>() {
                @Override
                public Void execute() throws Throwable {
                    authorityService.getOrCreateZone(zoneId);
                    return null;
                }
            }, false, splitTxns);

    // The set of zones we associate with new objects (default plus registry specific)
    final Set<String> zoneSet = getZones(zoneId);

    long lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                    splitTxns);
    Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);

    if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            ChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all groups from user registry '" + zone + "'");
        } else {
            ChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving groups changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // First, analyze the group structure. Create maps of authorities to their parents for associations to create
    // and delete. Also deal with 'overlaps' with other zones in the authentication chain.
    final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.GROUP_ANALYSIS.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher,
            ChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class Analyzer extends BaseBatchProcessWorker<NodeDescription> {
        private final Map<String, String> groupsToCreate = new TreeMap<String, String>();
        private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap();
        private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap();
        private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>();
        private List<String> personsProcessed = new LinkedList<String>();
        private Set<String> allZonePersons = Collections.emptySet();
        private Set<String> deletionCandidates;

        private long latestTime;

        public Analyzer(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public Set<String> getDeletionCandidates() {
            return this.deletionCandidates;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription group) throws Throwable {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService
                    .getShortName(groupName);
            Set<String> groupZones = ChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(groupName);

            if (groupZones == null) {
                // The group did not exist at all
                updateGroup(group, false);
            } else {
                // Check whether the group is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(groupZones);
                intersection.retainAll(allZoneIds);
                // Check whether the group is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);

                if (groupZones.contains(zoneId)) {
                    // The group already existed in this zone: update the group
                    updateGroup(group, true);
                } else if (!visited.isEmpty()) {
                    // A group that exists in a different zone with higher precedence
                    return;
                } else if (!allowDeletions || intersection.isEmpty()) {
                    // Deletions are disallowed or the group exists, but not in a zone that's in the authentication
                    // chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName
                                + "'. This group will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(groupName, groupZones, zoneSet);

                    // The group now exists in this zone: update the group
                    updateGroup(group, true);
                } else {
                    // The group existed, but in a zone with lower precedence
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Recreating occluded group '"
                                + groupShortName
                                + "'. This group was previously created through synchronization with a lower priority user registry.");
                    }
                    ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName);

                    // create the group
                    updateGroup(group, false);
                }
            }

            synchronized (this) {
                // Maintain the last modified date
                Date groupLastModified = group.getLastModified();
                if (groupLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
                }
            }
        }

        // Recursively walks and caches the authorities relating to and from this group so that we can later detect potential cycles
        private Set<String> getContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // First, recurse to the parent most authorities
            for (String parent : ChainingUserRegistrySynchronizer.this.authorityService
                    .getContainingAuthorities(null, groupName, true)) {
                getContainedAuthorities(parent);
            }

            // Now descend on unprocessed parents.
            return cacheContainedAuthorities(groupName);
        }

        private Set<String> cacheContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // Descend on unprocessed parents.
            children = ChainingUserRegistrySynchronizer.this.authorityService.getContainedAuthorities(null,
                    groupName, true);
            this.finalGroupChildAssocs.put(groupName, children);

            for (String child : children) {
                if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) {
                    cacheContainedAuthorities(child);
                }
            }
            return children;
        }

        private synchronized void updateGroup(NodeDescription group, boolean existed) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
            if (groupDisplayName == null) {
                groupDisplayName = ChainingUserRegistrySynchronizer.this.authorityService
                        .getShortName(groupName);
            }

            // Divide the child associations into person and group associations, dealing with case sensitivity
            Set<String> newChildPersons = newPersonSet();
            Set<String> newChildGroups = new TreeSet<String>();

            for (String child : group.getChildAssociations()) {
                if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                    newChildPersons.add(child);
                } else {
                    newChildGroups.add(child);
                }
            }

            // Account for differences if already existing
            if (existed) {
                // Update the display name now
                ChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName,
                        groupDisplayName);

                // Work out the association differences
                for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) {
                    if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                        if (!newChildPersons.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    } else {
                        if (!newChildGroups.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    }
                }
            }
            // Mark as created if new
            else {
                // Make sure each group to be created features in the association deletion map (as these are handled in the same phase)
                recordParentAssociationDeletion(groupName, null);
                this.groupsToCreate.put(groupName, groupDisplayName);
            }

            // Create new associations
            for (String child : newChildPersons) {
                // Make sure each person with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
            for (String child : newChildGroups) {
                // Make sure each group with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
        }

        private void recordParentAssociationDeletion(String child, String parent) {
            Map<String, Set<String>> parentAssocs;
            if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                parentAssocs = this.personParentAssocsToDelete;
            } else {
                // Reflect the change in the map of final group associations (for cycle detection later)
                parentAssocs = this.groupParentAssocsToDelete;
                if (parent != null) {
                    Set<String> children = this.finalGroupChildAssocs.get(parent);
                    children.remove(child);
                }
            }
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void recordParentAssociationCreation(String child, String parent) {
            Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER
                    ? this.personParentAssocsToCreate
                    : this.groupParentAssocsToCreate;
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void validateGroupParentAssocsToCreate() {
            Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String group = entry.getKey();
                Set<String> parents = entry.getValue();
                Deque<String> visited = new LinkedList<String>();
                Iterator<String> j = parents.iterator();
                while (j.hasNext()) {
                    String parent = j.next();
                    visited.add(parent);
                    if (validateAuthorityChildren(visited, group)) {
                        // The association validated - commit it
                        Set<String> children = finalGroupChildAssocs.get(parent);
                        if (children == null) {
                            children = new TreeSet<String>();
                            finalGroupChildAssocs.put(parent, children);
                        }
                        children.add(group);
                    } else {
                        // The association did not validate - prune it out
                        if (logger.isWarnEnabled()) {
                            ChainingUserRegistrySynchronizer.logger.warn("Not adding group '"
                                    + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(group)
                                    + "' to group '" + ChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent)
                                    + "' as this creates a cyclic relationship");
                        }
                        j.remove();
                    }
                    visited.removeLast();
                }
                if (parents.isEmpty()) {
                    i.remove();
                }
            }

            // Sort the group associations in parent-first order (root groups first) to minimize reindexing overhead
            Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>(
                    this.groupParentAssocsToCreate.size() * 2);
            Deque<String> visited = new LinkedList<String>();
            for (String authority : this.groupParentAssocsToCreate.keySet()) {
                visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate,
                        sortedGroupAssociations);
            }

            this.groupParentAssocsToCreate = sortedGroupAssociations;
        }

        private boolean validateAuthorityChildren(Deque<String> visited, String authority) {
            if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                return true;
            }
            if (visited.contains(authority)) {
                return false;
            }
            visited.add(authority);
            try {
                Set<String> children = this.finalGroupChildAssocs.get(authority);
                if (children != null) {
                    for (String child : children) {
                        if (!validateAuthorityChildren(visited, child)) {
                            return false;
                        }
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        /**
         * Visits the given authority by recursively visiting its parents in associationsOld and then adding the
         * authority to associationsNew. Used to sort associationsOld into 'parent-first' order to minimize
         * reindexing overhead.
         * 
         * @param visited
         *            The ancestors that form the path to the authority to visit. Allows detection of cyclic child
         *            associations.
         * @param authority
         *            the authority to visit
         * @param associationsOld
         *            the association map to sort
         * @param associationsNew
         *            the association map to add to in parent-first order
         */
        private boolean visitGroupParentAssocs(Deque<String> visited, String authority,
                Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) {
            if (visited.contains(authority)) {
                // Prevent cyclic paths (Shouldn't happen as we've already validated)
                return false;
            }
            visited.add(authority);
            try {
                if (!associationsNew.containsKey(authority)) {
                    Set<String> oldParents = associationsOld.get(authority);
                    if (oldParents != null) {
                        Set<String> newParents = new TreeSet<String>();

                        for (String parent : oldParents) {
                            if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) {
                                newParents.add(parent);
                            }
                        }
                        associationsNew.put(authority, newParents);
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        private Set<String> newPersonSet() {
            return ChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeSet<String>()
                    : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        }

        private Map<String, Set<String>> newPersonMap() {
            return ChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeMap<String, Set<String>>()
                    : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER);
        }

        private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) {
            Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator();
            StringBuilder groupList = null;
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String child = entry.getKey();
                if (!toRetain.contains(child)) {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        if (groupList == null) {
                            groupList = new StringBuilder(1024);
                        } else {
                            groupList.setLength(0);
                        }
                        for (String parent : entry.getValue()) {
                            if (groupList.length() > 0) {
                                groupList.append(", ");
                            }
                            groupList.append('\'').append(
                                    ChainingUserRegistrySynchronizer.this.authorityService.getShortName(parent))
                                    .append('\'');

                        }
                        ChainingUserRegistrySynchronizer.logger.debug("Ignoring non-existent member '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child)
                                + "' in groups {" + groupList.toString() + "}");
                    }
                    i.remove();
                }
            }
        }

        private void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) {
            // MNT-12454 fix. If syncDelete is false, there is no need to pull all users and all groups from LDAP during the full synchronization.
            if ((syncDelete || !groupsToCreate.isEmpty())
                    && (isFullSync || !this.groupParentAssocsToDelete.isEmpty())) {
                final Set<String> allZonePersons = newPersonSet();
                final Set<String> allZoneGroups = new TreeSet<String>();

                // Add in current set of known authorities
                ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper()
                        .doInTransaction(new RetryingTransactionCallback<Void>() {
                            public Void execute() throws Throwable {
                                allZonePersons.addAll(ChainingUserRegistrySynchronizer.this.authorityService
                                        .getAllAuthoritiesInZone(zoneId, AuthorityType.USER));
                                allZoneGroups.addAll(ChainingUserRegistrySynchronizer.this.authorityService
                                        .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP));
                                return null;
                            }
                        }, true, splitTxns);

                allZoneGroups.addAll(this.groupsToCreate.keySet());

                // Prune our set of authorities according to deletions
                if (isFullSync) {
                    final Set<String> personDeletionCandidates = newPersonSet();
                    personDeletionCandidates.addAll(allZonePersons);

                    final Set<String> groupDeletionCandidates = new TreeSet<String>();
                    groupDeletionCandidates.addAll(allZoneGroups);

                    this.deletionCandidates = new TreeSet<String>();

                    for (String person : userRegistry.getPersonNames()) {
                        personDeletionCandidates.remove(person);
                    }

                    for (String group : userRegistry.getGroupNames()) {
                        groupDeletionCandidates.remove(group);
                    }

                    this.deletionCandidates = new TreeSet<String>();
                    this.deletionCandidates.addAll(personDeletionCandidates);
                    this.deletionCandidates.addAll(groupDeletionCandidates);

                    if (allowDeletions) {
                        allZonePersons.removeAll(personDeletionCandidates);
                        allZoneGroups.removeAll(groupDeletionCandidates);
                    } else {
                        // Complete association deletion information by scanning deleted groups
                        BatchProcessor<String> groupScanner = new BatchProcessor<String>(
                                zone + " Missing Authority Scanning",
                                ChainingUserRegistrySynchronizer.this.transactionService
                                        .getRetryingTransactionHelper(),
                                this.deletionCandidates, ChainingUserRegistrySynchronizer.this.workerThreads,
                                20, ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                                ChainingUserRegistrySynchronizer.logger,
                                ChainingUserRegistrySynchronizer.this.loggingInterval);
                        groupScanner.process(new BaseBatchProcessWorker<String>() {

                            @Override
                            public String getIdentifier(String entry) {
                                return entry;
                            }

                            @Override
                            public void process(String authority) throws Throwable {
                                //MNT-12454 fix. Modifies an authority's zone. Move authority from AUTH.EXT.LDAP1 to AUTH.ALF.
                                updateAuthorityZones(authority, Collections.singleton(zoneId),
                                        Collections.singleton(AuthorityService.ZONE_AUTH_ALFRESCO));
                            }
                        }, splitTxns);
                    }

                }

                // Prune the group associations now that we have complete information
                this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups);
                logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups);
                this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups);

                // Pruning person associations will have to wait until we have passed over all persons and built up
                // this set
                this.allZonePersons = allZonePersons;

                if (!this.groupParentAssocsToDelete.isEmpty()) {
                    // Create/update the groups and delete parent associations to be deleted
                    // Batch 4 Group Creation and Association Deletion
                    BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
                            ChainingUserRegistrySynchronizer.this.transactionService
                                    .getRetryingTransactionHelper(),
                            this.groupParentAssocsToDelete.entrySet(),
                            ChainingUserRegistrySynchronizer.this.workerThreads, 20,
                            ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                            ChainingUserRegistrySynchronizer.logger,
                            ChainingUserRegistrySynchronizer.this.loggingInterval);
                    groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                        public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                            return entry.getKey() + " " + entry.getValue();
                        }

                        public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                            String child = entry.getKey();

                            String groupDisplayName = Analyzer.this.groupsToCreate.get(child);
                            if (groupDisplayName != null) {
                                String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(child);
                                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                                    ChainingUserRegistrySynchronizer.logger
                                            .debug("Creating group '" + groupShortName + "'");
                                }
                                // create the group
                                ChainingUserRegistrySynchronizer.this.authorityService.createAuthority(
                                        AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName,
                                        zoneSet);
                            } else {
                                // Maintain association deletions now. The creations will have to be done later once
                                // we have performed all the deletions in order to avoid creating cycles
                                maintainAssociationDeletions(child);
                            }
                        }
                    }, splitTxns);
                }
            }
        }

        private void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) {
            // First validate the group associations to be created for potential cycles. Remove any offending association
            validateGroupParentAssocsToCreate();

            // Now go ahead and create the group associations
            if (!this.groupParentAssocsToCreate.isEmpty()) {
                // Batch 5 Group Association Creation
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
                        ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(),
                        this.groupParentAssocsToCreate.entrySet(),
                        ChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        ChainingUserRegistrySynchronizer.logger,
                        ChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }

            // Remove all the associations we have already dealt with
            this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed);

            // Filter out associations to authorities that simply can't exist (and log if debugging is enabled)
            logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons);

            // Update associations to persons not updated themselves
            if (!this.personParentAssocsToDelete.isEmpty()) {
                // Batch 6 Person Association
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.PERSON_ASSOCIATION.getTitle(zone),
                        ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(),
                        this.personParentAssocsToDelete.entrySet(),
                        ChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        ChainingUserRegistrySynchronizer.logger,
                        ChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationDeletions(entry.getKey());
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }
        }

        private void maintainAssociationDeletions(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName)
                    : this.groupParentAssocsToDelete.get(authorityName);
            if (parentsToDelete != null && !parentsToDelete.isEmpty()) {
                for (String parent : parentsToDelete) {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.debug("Removing '"
                                + ChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' from group '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(parent)
                                + "'");
                    }
                    ChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent,
                            authorityName);
                }
            }

        }

        private void maintainAssociationCreations(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName)
                    : this.groupParentAssocsToCreate.get(authorityName);
            if (parents != null && !parents.isEmpty()) {
                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    for (String groupName : parents) {
                        ChainingUserRegistrySynchronizer.logger.debug("Adding '"
                                + ChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' to group '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(groupName)
                                + "'");
                    }
                }
                try {
                    ChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents, authorityName);
                } catch (UnknownAuthorityException e) {
                    // Let's force a transaction retry if a parent doesn't exist. It may be because we are
                    // waiting for another worker thread to create it
                    throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e);
                } catch (InvalidNodeRefException e) {
                    // Another thread may have written the node, but it is not visible to this transaction
                    // See: ALF-5471: 'authorityMigration' patch can report 'Node does not exist'
                    throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e);
                }
            }
            // Remember that this person's associations have been maintained
            if (isPerson) {
                synchronized (this) {
                    this.personsProcessed.add(authorityName);
                }
            }
        }
    } // end of Analyzer class

    // Run the first process the Group Analyzer
    final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis);
    int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns);

    groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns);

    // Process persons and their parent associations

    lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                    splitTxns);
    lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
    if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            ChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all users from user registry '" + zone + "'");
        } else {
            ChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving users changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // User Creation and Association
    final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.USER_CREATION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher,
            ChainingUserRegistrySynchronizer.logger, this.loggingInterval);

    final UserRegistry userRegistryFinalRef = userRegistry;

    class PersonWorker extends BaseBatchProcessWorker<NodeDescription> {
        private long latestTime;

        public PersonWorker(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription person) throws Throwable {
            // Make a mutable copy of the person properties, since they get written back to by person service
            HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>(
                    person.getProperties());
            String personName = personProperties.get(ContentModel.PROP_USERNAME).toString().trim();
            personProperties.put(ContentModel.PROP_USERNAME, personName);

            if (Boolean.parseBoolean(ChainingUserRegistrySynchronizer.this.externalUserControl)
                    && ChainingUserRegistrySynchronizer.this.externalUserControlSubsystemName.equals(zone)
                    && userRegistryFinalRef instanceof LDAPUserRegistry) {
                try {
                    LDAPUserRegistry ldapUserRegistry = (LDAPUserRegistry) userRegistryFinalRef;

                    if (ldapUserRegistry.getUserAccountStatusInterpreter() != null) {
                        QName propertyNameToCheck = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,
                                "userAccountStatusProperty");

                        if (personProperties.get(propertyNameToCheck) != null
                                || ldapUserRegistry.getUserAccountStatusInterpreter().acceptsNullArgument()) {
                            boolean isUserAccountDisabled = ldapUserRegistry.getUserAccountStatusInterpreter()
                                    .isUserAccountDisabled(personProperties.get(propertyNameToCheck));

                            personProperties.put(ContentModel.PROP_ENABLED, !isUserAccountDisabled);
                        }
                    }
                } catch (IllegalArgumentException iae) {
                    // Can be thrown by certain implementations of AbstractDirectoryServiceUserAccountStatusInterpreter;
                    // We'll just log it.
                    ChainingUserRegistrySynchronizer.logger.debug(iae.getMessage(), iae);
                }
            }

            // for invalid names will throw ConstraintException that will be catched by BatchProcessor$TxnCallback
            nameChecker.evaluate(personName);
            Set<String> zones = ChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(personName);
            if (zones == null) {
                // The person did not exist at all
                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    ChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'");
                }
                ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet);
            } else if (zones.contains(zoneId)) {
                // The person already existed in this zone: update the person
                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    ChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'");
                }
                ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                        personProperties, false);
            } else {
                // Check whether the user is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(zones);
                intersection.retainAll(allZoneIds);
                // Check whether the user is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);
                if (visited.size() > 0) {
                    // A person that exists in a different zone with higher precedence - ignore
                    return;
                }

                else if (!allowDeletions || intersection.isEmpty()) {
                    // The person exists, but in a different zone. Either deletions are disallowed or the zone is
                    // not in the authentication chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName
                                + "'. This user will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(personName, zones, zoneSet);
                    ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                            personProperties, false);
                } else {
                    // The person existed, but in a zone with lower precedence
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Recreating occluded user '" + personName
                                + "'. This user was previously created through synchronization with a lower priority user registry.");
                    }
                    ChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
                    ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet);
                }
            }

            // Maintain association deletions and creations in one shot (safe to do this with persons as we can't
            // create cycles)
            groupAnalyzer.maintainAssociationDeletions(personName);
            groupAnalyzer.maintainAssociationCreations(personName);

            synchronized (this) {
                // Maintain the last modified date
                Date personLastModified = person.getLastModified();
                if (personLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
                }
            }
        }
    }

    PersonWorker persons = new PersonWorker(lastModifiedMillis);
    int personProcessedCount = personProcessor.process(persons, splitTxns);

    // Process those associations to persons who themselves have not been updated
    groupAnalyzer.finalizeAssociations(userRegistry, splitTxns);

    // Only now that the whole tree has been processed is it safe to persist the last modified dates
    long latestTime = groupAnalyzer.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }
    latestTime = persons.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }

    // Delete authorities if we have complete information for the zone
    Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates();
    if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) {
        // Batch 7 Authority Deletion
        BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(
                SyncProcess.AUTHORITY_DELETION.getTitle(zone),
                this.transactionService.getRetryingTransactionHelper(), deletionCandidates, this.workerThreads,
                10, this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger,
                this.loggingInterval);
        class AuthorityDeleter extends BaseBatchProcessWorker<String> {
            private int personProcessedCount;
            private int groupProcessedCount;

            public int getPersonProcessedCount() {
                return this.personProcessedCount;
            }

            public int getGroupProcessedCount() {
                return this.groupProcessedCount;
            }

            public String getIdentifier(String entry) {
                return entry;
            }

            public void process(String authority) throws Throwable {
                if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.debug("Deleting user '" + authority + "'");
                    }
                    ChainingUserRegistrySynchronizer.this.personService.deletePerson(authority);
                    synchronized (this) {
                        this.personProcessedCount++;
                    }
                } else {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.debug("Deleting group '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(authority)
                                + "'");
                    }
                    ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
                    synchronized (this) {
                        this.groupProcessedCount++;
                    }
                }
            }
        }
        AuthorityDeleter authorityDeleter = new AuthorityDeleter();
        authorityDeletionProcessor.process(authorityDeleter, splitTxns);
        groupProcessedCount += authorityDeleter.getGroupProcessedCount();
        personProcessedCount += authorityDeleter.getPersonProcessedCount();
    }

    // Remember we have visited this zone
    visitedZoneIds.add(zoneId);

    Object statusParams[] = { personProcessedCount, groupProcessedCount };
    final String statusMessage = I18NUtil.getMessage("synchronization.summary.status", statusParams);

    if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        ChainingUserRegistrySynchronizer.logger
                .info("Finished synchronizing users and groups with user registry '" + zone + "'");
        ChainingUserRegistrySynchronizer.logger.info(statusMessage);
    }

    notifySyncDirectoryEnd(zone, statusMessage);

}

From source file:org.cggh.repo.security.sync.CustomChainingUserRegistrySynchronizer.java

/**
 * Synchronizes local groups and users with a {@link UserRegistry} for a particular zone, optionally handling
 * deletions.//from   www . j av a  2  s . co m
 * 
 * @param zone
 *            the zone id. This identifier is used to tag all created groups and users, so that in the future we can
 *            tell those that have been deleted from the registry.
 * @param userRegistry
 *            the user registry for the zone.
 * @param forceUpdate
 *            Should the complete set of users and groups be updated / created locally or just those known to have
 *            changed since the last sync? When <code>true</code> then <i>all</i> users and groups are queried from
 *            the user registry and updated locally. When <code>false</code> then each source is only queried for
 *            those users and groups modified since the most recent modification date of all the objects last
 *            queried from that same source.
 * @param isFullSync
 *            Should a complete set of user and group IDs be queried from the user registries in order to determine
 *            deletions? This parameter is independent of <code>force</code> as a separate query is run to process
 *            updates.
 * @param splitTxns
 *            Can the modifications to Alfresco be split across multiple transactions for maximum performance? If
 *            <code>true</code>, users and groups are created/updated in batches for increased performance. If
 *            <code>false</code>, all users and groups are processed in the current transaction. This is required if
 *            calling synchronously (e.g. in response to an authentication event in the same transaction).
 * @param visitedZoneIds
 *            the set of zone ids already processed. These zones have precedence over the current zone when it comes
 *            to group name 'collisions'. If a user or group is queried that already exists locally but is tagged
 *            with one of the zones in this set, then it will be ignored as this zone has lower priority.
 * @param allZoneIds
 *            the set of all zone ids in the authentication chain. Helps us work out whether the zone information
 *            recorded against a user or group is invalid for the current authentication chain and whether the user
 *            or group needs to be 're-zoned'.
 */
private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate,
        boolean isFullSync, boolean splitTxns, final Set<String> visitedZoneIds, final Set<String> allZoneIds) {
    // Create a prefixed zone ID for use with the authority service
    final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;

    // Batch Process Names
    final String reservedBatchProcessNames[] = { SyncProcess.GROUP_ANALYSIS.getTitle(zone),
            SyncProcess.USER_CREATION.getTitle(zone), SyncProcess.MISSING_AUTHORITY.getTitle(zone),
            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
            SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
            SyncProcess.PERSON_ASSOCIATION.getTitle(zone), SyncProcess.AUTHORITY_DELETION.getTitle(zone) };

    notifySyncDirectoryStart(zone, reservedBatchProcessNames);

    // Ensure that the zoneId exists before multiple threads start using it
    this.transactionService.getRetryingTransactionHelper()
            .doInTransaction(new RetryingTransactionCallback<Void>() {
                @Override
                public Void execute() throws Throwable {
                    authorityService.getOrCreateZone(zoneId);
                    return null;
                }
            }, false, splitTxns);

    // The set of zones we associate with new objects (default plus registry specific)
    final Set<String> zoneSet = getZones(zoneId);

    long lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);

    if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            CustomChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all groups from user registry '" + zone + "'");
        } else {
            CustomChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving groups changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // First, analyze the group structure. Create maps of authorities to their parents for associations to create
    // and delete. Also deal with 'overlaps' with other zones in the authentication chain.
    final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.GROUP_ANALYSIS.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher,
            CustomChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class Analyzer extends BaseBatchProcessWorker<NodeDescription> {
        private final Map<String, String> groupsToCreate = new TreeMap<String, String>();
        private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap();
        private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap();
        private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>();
        private List<String> personsProcessed = new LinkedList<String>();
        private Set<String> allZonePersons = Collections.emptySet();
        private Set<String> deletionCandidates;

        private long latestTime;

        public Analyzer(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public Set<String> getDeletionCandidates() {
            return this.deletionCandidates;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription group) throws Throwable {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupShortName = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getShortName(groupName);
            Set<String> groupZones = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(groupName);

            if (groupZones == null) {
                // The group did not exist at all
                updateGroup(group, false);
            } else {
                // Check whether the group is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(groupZones);
                intersection.retainAll(allZoneIds);
                // Check whether the group is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);

                if (groupZones.contains(zoneId)) {
                    // The group already existed in this zone: update the group
                    updateGroup(group, true);
                } else if (!visited.isEmpty()) {
                    // A group that exists in a different zone with higher precedence
                    return;
                } else if (!allowDeletions || intersection.isEmpty()) {
                    // Deletions are disallowed or the group exists, but not in a zone that's in the authentication
                    // chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName
                                + "'. This group will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(groupName, groupZones, zoneSet);

                    // The group now exists in this zone: update the group
                    updateGroup(group, true);
                } else {
                    // The group existed, but in a zone with lower precedence
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Recreating occluded group '"
                                + groupShortName
                                + "'. This group was previously created through synchronization with a lower priority user registry.");
                    }
                    CustomChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName);

                    // create the group
                    updateGroup(group, false);
                }
            }

            synchronized (this) {
                // Maintain the last modified date
                Date groupLastModified = group.getLastModified();
                if (groupLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
                }
            }
        }

        // Recursively walks and caches the authorities relating to and from this group so that we can later detect potential cycles
        private Set<String> getContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // First, recurse to the parent most authorities
            for (String parent : CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getContainingAuthorities(null, groupName, true)) {
                getContainedAuthorities(parent);
            }

            // Now descend on unprocessed parents.
            return cacheContainedAuthorities(groupName);
        }

        private Set<String> cacheContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // Descend on unprocessed parents.
            children = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getContainedAuthorities(null, groupName, true);
            this.finalGroupChildAssocs.put(groupName, children);

            for (String child : children) {
                if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) {
                    cacheContainedAuthorities(child);
                }
            }
            return children;
        }

        private synchronized void updateGroup(NodeDescription group, boolean existed) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
            if (groupDisplayName == null) {
                groupDisplayName = CustomChainingUserRegistrySynchronizer.this.authorityService
                        .getShortName(groupName);
            }

            // Divide the child associations into person and group associations, dealing with case sensitivity
            Set<String> newChildPersons = newPersonSet();
            Set<String> newChildGroups = new TreeSet<String>();

            for (String child : group.getChildAssociations()) {
                if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                    newChildPersons.add(child);
                } else {
                    newChildGroups.add(child);
                }
            }

            // Account for differences if already existing
            if (existed) {
                // Update the display name now
                CustomChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName,
                        groupDisplayName);

                // Work out the association differences
                for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) {
                    if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                        if (!newChildPersons.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    } else {
                        if (!newChildGroups.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    }
                }
            }
            // Mark as created if new
            else {
                // Make sure each group to be created features in the association deletion map (as these are handled in the same phase)
                recordParentAssociationDeletion(groupName, null);
                this.groupsToCreate.put(groupName, groupDisplayName);
            }

            // Create new associations
            for (String child : newChildPersons) {
                // Make sure each person with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
            for (String child : newChildGroups) {
                // Make sure each group with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
        }

        private void recordParentAssociationDeletion(String child, String parent) {
            Map<String, Set<String>> parentAssocs;
            if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                parentAssocs = this.personParentAssocsToDelete;
            } else {
                // Reflect the change in the map of final group associations (for cycle detection later)
                parentAssocs = this.groupParentAssocsToDelete;
                if (parent != null) {
                    Set<String> children = this.finalGroupChildAssocs.get(parent);
                    children.remove(child);
                }
            }
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void recordParentAssociationCreation(String child, String parent) {
            Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER
                    ? this.personParentAssocsToCreate
                    : this.groupParentAssocsToCreate;
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void validateGroupParentAssocsToCreate() {
            Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String group = entry.getKey();
                Set<String> parents = entry.getValue();
                Deque<String> visited = new LinkedList<String>();
                Iterator<String> j = parents.iterator();
                while (j.hasNext()) {
                    String parent = j.next();
                    visited.add(parent);
                    if (validateAuthorityChildren(visited, group)) {
                        // The association validated - commit it
                        Set<String> children = finalGroupChildAssocs.get(parent);
                        if (children == null) {
                            children = new TreeSet<String>();
                            finalGroupChildAssocs.put(parent, children);
                        }
                        children.add(group);
                    } else {
                        // The association did not validate - prune it out
                        if (logger.isWarnEnabled()) {
                            CustomChainingUserRegistrySynchronizer.logger.warn("Not adding group '"
                                    + CustomChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(group)
                                    + "' to group '"
                                    + CustomChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent)
                                    + "' as this creates a cyclic relationship");
                        }
                        j.remove();
                    }
                    visited.removeLast();
                }
                if (parents.isEmpty()) {
                    i.remove();
                }
            }

            // Sort the group associations in parent-first order (root groups first) to minimize reindexing overhead
            Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>(
                    this.groupParentAssocsToCreate.size() * 2);
            Deque<String> visited = new LinkedList<String>();
            for (String authority : this.groupParentAssocsToCreate.keySet()) {
                visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate,
                        sortedGroupAssociations);
            }

            this.groupParentAssocsToCreate = sortedGroupAssociations;
        }

        private boolean validateAuthorityChildren(Deque<String> visited, String authority) {
            if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                return true;
            }
            if (visited.contains(authority)) {
                return false;
            }
            visited.add(authority);
            try {
                Set<String> children = this.finalGroupChildAssocs.get(authority);
                if (children != null) {
                    for (String child : children) {
                        if (!validateAuthorityChildren(visited, child)) {
                            return false;
                        }
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        /**
         * Visits the given authority by recursively visiting its parents in associationsOld and then adding the
         * authority to associationsNew. Used to sort associationsOld into 'parent-first' order to minimize
         * reindexing overhead.
         * 
         * @param visited
         *            The ancestors that form the path to the authority to visit. Allows detection of cyclic child
         *            associations.
         * @param authority
         *            the authority to visit
         * @param associationsOld
         *            the association map to sort
         * @param associationsNew
         *            the association map to add to in parent-first order
         */
        private boolean visitGroupParentAssocs(Deque<String> visited, String authority,
                Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) {
            if (visited.contains(authority)) {
                // Prevent cyclic paths (Shouldn't happen as we've already validated)
                return false;
            }
            visited.add(authority);
            try {
                if (!associationsNew.containsKey(authority)) {
                    Set<String> oldParents = associationsOld.get(authority);
                    if (oldParents != null) {
                        Set<String> newParents = new TreeSet<String>();

                        for (String parent : oldParents) {
                            if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) {
                                newParents.add(parent);
                            }
                        }
                        associationsNew.put(authority, newParents);
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        private Set<String> newPersonSet() {
            return CustomChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeSet<String>()
                    : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        }

        private Map<String, Set<String>> newPersonMap() {
            return CustomChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeMap<String, Set<String>>()
                    : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER);
        }

        private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) {
            Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator();
            StringBuilder groupList = null;
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String child = entry.getKey();
                if (!toRetain.contains(child)) {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        if (groupList == null) {
                            groupList = new StringBuilder(1024);
                        } else {
                            groupList.setLength(0);
                        }
                        for (String parent : entry.getValue()) {
                            if (groupList.length() > 0) {
                                groupList.append(", ");
                            }
                            groupList.append('\'')
                                    .append(CustomChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent))
                                    .append('\'');

                        }
                        CustomChainingUserRegistrySynchronizer.logger
                                .debug("Ignoring non-existent member '"
                                        + CustomChainingUserRegistrySynchronizer.this.authorityService
                                                .getShortName(child)
                                        + "' in groups {" + groupList.toString() + "}");
                    }
                    i.remove();
                }
            }
        }

        private void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) {
            // MNT-12454 fix. If syncDelete is false, there is no need to pull all users and all groups from LDAP during the full synchronization.
            if ((syncDelete || !groupsToCreate.isEmpty())
                    && (isFullSync || !this.groupParentAssocsToDelete.isEmpty())) {
                final Set<String> allZonePersons = newPersonSet();
                final Set<String> allZoneGroups = new TreeSet<String>();

                // Add in current set of known authorities
                CustomChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper()
                        .doInTransaction(new RetryingTransactionCallback<Void>() {
                            public Void execute() throws Throwable {
                                allZonePersons
                                        .addAll(CustomChainingUserRegistrySynchronizer.this.authorityService
                                                .getAllAuthoritiesInZone(zoneId, AuthorityType.USER));
                                allZoneGroups
                                        .addAll(CustomChainingUserRegistrySynchronizer.this.authorityService
                                                .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP));
                                return null;
                            }
                        }, true, splitTxns);

                allZoneGroups.addAll(this.groupsToCreate.keySet());

                // Prune our set of authorities according to deletions
                if (isFullSync) {
                    final Set<String> personDeletionCandidates = newPersonSet();
                    personDeletionCandidates.addAll(allZonePersons);

                    final Set<String> groupDeletionCandidates = new TreeSet<String>();
                    groupDeletionCandidates.addAll(allZoneGroups);

                    this.deletionCandidates = new TreeSet<String>();

                    for (String person : userRegistry.getPersonNames()) {
                        personDeletionCandidates.remove(person);
                    }

                    for (String group : userRegistry.getGroupNames()) {
                        groupDeletionCandidates.remove(group);
                    }

                    this.deletionCandidates = new TreeSet<String>();
                    this.deletionCandidates.addAll(personDeletionCandidates);
                    this.deletionCandidates.addAll(groupDeletionCandidates);

                    if (allowDeletions) {
                        allZonePersons.removeAll(personDeletionCandidates);
                        allZoneGroups.removeAll(groupDeletionCandidates);
                    } else {
                        // Complete association deletion information by scanning deleted groups
                        BatchProcessor<String> groupScanner = new BatchProcessor<String>(
                                zone + " Missing Authority Scanning",
                                CustomChainingUserRegistrySynchronizer.this.transactionService
                                        .getRetryingTransactionHelper(),
                                this.deletionCandidates,
                                CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                                CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                                CustomChainingUserRegistrySynchronizer.logger,
                                CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                        groupScanner.process(new BaseBatchProcessWorker<String>() {

                            @Override
                            public String getIdentifier(String entry) {
                                return entry;
                            }

                            @Override
                            public void process(String authority) throws Throwable {
                                //MNT-12454 fix. Modifies an authority's zone. Move authority from AUTH.EXT.LDAP1 to AUTH.ALF.
                                updateAuthorityZones(authority, Collections.singleton(zoneId),
                                        Collections.singleton(AuthorityService.ZONE_AUTH_ALFRESCO));
                            }
                        }, splitTxns);
                    }

                }

                // Prune the group associations now that we have complete information
                this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups);
                logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups);
                this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups);

                // Pruning person associations will have to wait until we have passed over all persons and built up
                // this set
                this.allZonePersons = allZonePersons;

                if (!this.groupParentAssocsToDelete.isEmpty()) {
                    // Create/update the groups and delete parent associations to be deleted
                    // Batch 4 Group Creation and Association Deletion
                    BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
                            CustomChainingUserRegistrySynchronizer.this.transactionService
                                    .getRetryingTransactionHelper(),
                            this.groupParentAssocsToDelete.entrySet(),
                            CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                            CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                            CustomChainingUserRegistrySynchronizer.logger,
                            CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                    groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                        public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                            return entry.getKey() + " " + entry.getValue();
                        }

                        public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                            String child = entry.getKey();

                            String groupDisplayName = Analyzer.this.groupsToCreate.get(child);
                            if (groupDisplayName != null) {
                                String groupShortName = CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(child);
                                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                                    CustomChainingUserRegistrySynchronizer.logger
                                            .debug("Creating group '" + groupShortName + "'");
                                }
                                // create the group
                                CustomChainingUserRegistrySynchronizer.this.authorityService.createAuthority(
                                        AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName,
                                        zoneSet);
                            } else {
                                // Maintain association deletions now. The creations will have to be done later once
                                // we have performed all the deletions in order to avoid creating cycles
                                maintainAssociationDeletions(child);
                            }
                        }
                    }, splitTxns);
                }
            }
        }

        private void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) {
            // First validate the group associations to be created for potential cycles. Remove any offending association
            validateGroupParentAssocsToCreate();

            // Now go ahead and create the group associations
            if (!this.groupParentAssocsToCreate.isEmpty()) {
                // Batch 5 Group Association Creation
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
                        CustomChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.groupParentAssocsToCreate.entrySet(),
                        CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        CustomChainingUserRegistrySynchronizer.logger,
                        CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }

            // Remove all the associations we have already dealt with
            this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed);

            // Filter out associations to authorities that simply can't exist (and log if debugging is enabled)
            logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons);

            // Update associations to persons not updated themselves
            if (!this.personParentAssocsToDelete.isEmpty()) {
                // Batch 6 Person Association
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.PERSON_ASSOCIATION.getTitle(zone),
                        CustomChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.personParentAssocsToDelete.entrySet(),
                        CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        CustomChainingUserRegistrySynchronizer.logger,
                        CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationDeletions(entry.getKey());
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }
        }

        private void maintainAssociationDeletions(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName)
                    : this.groupParentAssocsToDelete.get(authorityName);
            if (parentsToDelete != null && !parentsToDelete.isEmpty()) {
                for (String parent : parentsToDelete) {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.debug("Removing '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' from group '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(parent)
                                + "'");
                    }
                    CustomChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent,
                            authorityName);
                }
            }

        }

        private void maintainAssociationCreations(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName)
                    : this.groupParentAssocsToCreate.get(authorityName);
            if (parents != null && !parents.isEmpty()) {
                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    for (String groupName : parents) {
                        CustomChainingUserRegistrySynchronizer.logger.debug("Adding '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' to group '" + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(groupName)
                                + "'");
                    }
                }
                try {
                    CustomChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents,
                            authorityName);
                } catch (UnknownAuthorityException e) {
                    // Let's force a transaction retry if a parent doesn't exist. It may be because we are
                    // waiting for another worker thread to create it
                    throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e);
                } catch (InvalidNodeRefException e) {
                    // Another thread may have written the node, but it is not visible to this transaction
                    // See: ALF-5471: 'authorityMigration' patch can report 'Node does not exist'
                    throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e);
                }
            }
            // Remember that this person's associations have been maintained
            if (isPerson) {
                synchronized (this) {
                    this.personsProcessed.add(authorityName);
                }
            }
        }
    } // end of Analyzer class

    // Run the first process the Group Analyzer
    final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis);
    int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns);

    groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns);

    // Process persons and their parent associations

    lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
    if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            CustomChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all users from user registry '" + zone + "'");
        } else {
            CustomChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving users changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // User Creation and Association
    final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.USER_CREATION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher,
            CustomChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class PersonWorker extends BaseBatchProcessWorker<NodeDescription> {
        private long latestTime;

        public PersonWorker(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription person) throws Throwable {
            // Make a mutable copy of the person properties, since they get written back to by person service
            HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>(
                    person.getProperties());
            String personName = personProperties.get(ContentModel.PROP_USERNAME).toString().trim();
            personProperties.put(ContentModel.PROP_USERNAME, personName);
            // for invalid names will throw ConstraintException that will be catched by BatchProcessor$TxnCallback
            nameChecker.evaluate(personName);
            Set<String> zones = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(personName);
            if (zones == null) {
                // The person did not exist at all
                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    CustomChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'");
                }
                CustomChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                        zoneSet);
                CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                        person.getProperties(), getLatestTime());
            } else if (zones.contains(zoneId)) {
                // The person already existed in this zone: update the person
                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    CustomChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'");
                }
                CustomChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                        personProperties, false);
                CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                        person.getProperties(), getLatestTime());
            } else {
                // Check whether the user is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(zones);
                intersection.retainAll(allZoneIds);
                // Check whether the user is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);
                if (visited.size() > 0) {
                    // A person that exists in a different zone with higher precedence - ignore
                    return;
                }

                else if (!allowDeletions || intersection.isEmpty()) {
                    // The person exists, but in a different zone. Either deletions are disallowed or the zone is
                    // not in the authentication chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName
                                + "'. This user will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(personName, zones, zoneSet);
                    CustomChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                            personProperties, false);
                    CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                            person.getProperties(), getLatestTime());
                } else {
                    // The person existed, but in a zone with lower precedence
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Recreating occluded user '"
                                + personName
                                + "'. This user was previously created through synchronization with a lower priority user registry.");
                    }
                    CustomChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
                    CustomChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                            zoneSet);
                    CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                            person.getProperties(), getLatestTime());
                }
            }

            // Maintain association deletions and creations in one shot (safe to do this with persons as we can't
            // create cycles)
            groupAnalyzer.maintainAssociationDeletions(personName);
            groupAnalyzer.maintainAssociationCreations(personName);

            synchronized (this) {
                // Maintain the last modified date
                Date personLastModified = person.getLastModified();
                if (personLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
                }
            }
        }
    }

    PersonWorker persons = new PersonWorker(lastModifiedMillis);
    int personProcessedCount = personProcessor.process(persons, splitTxns);

    // Process those associations to persons who themselves have not been updated
    groupAnalyzer.finalizeAssociations(userRegistry, splitTxns);

    // Only now that the whole tree has been processed is it safe to persist the last modified dates
    long latestTime = groupAnalyzer.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }
    latestTime = persons.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }

    // Delete authorities if we have complete information for the zone
    Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates();
    if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) {
        // Batch 7 Authority Deletion
        BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(
                SyncProcess.AUTHORITY_DELETION.getTitle(zone),
                this.transactionService.getRetryingTransactionHelper(), deletionCandidates, this.workerThreads,
                10, this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger,
                this.loggingInterval);
        class AuthorityDeleter extends BaseBatchProcessWorker<String> {
            private int personProcessedCount;
            private int groupProcessedCount;

            public int getPersonProcessedCount() {
                return this.personProcessedCount;
            }

            public int getGroupProcessedCount() {
                return this.groupProcessedCount;
            }

            public String getIdentifier(String entry) {
                return entry;
            }

            public void process(String authority) throws Throwable {
                if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger
                                .debug("Deleting user '" + authority + "'");
                    }
                    CustomChainingUserRegistrySynchronizer.this.personService.deletePerson(authority);
                    synchronized (this) {
                        this.personProcessedCount++;
                    }
                } else {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.debug("Deleting group '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authority)
                                + "'");
                    }
                    CustomChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
                    synchronized (this) {
                        this.groupProcessedCount++;
                    }
                }
            }
        }
        AuthorityDeleter authorityDeleter = new AuthorityDeleter();
        authorityDeletionProcessor.process(authorityDeleter, splitTxns);
        groupProcessedCount += authorityDeleter.getGroupProcessedCount();
        personProcessedCount += authorityDeleter.getPersonProcessedCount();
    }

    // Remember we have visited this zone
    visitedZoneIds.add(zoneId);

    Object statusParams[] = { personProcessedCount, groupProcessedCount };
    final String statusMessage = I18NUtil.getMessage("synchronization.summary.status", statusParams);

    if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        CustomChainingUserRegistrySynchronizer.logger
                .info("Finished synchronizing users and groups with user registry '" + zone + "'");
        CustomChainingUserRegistrySynchronizer.logger.info(statusMessage);
    }

    notifySyncDirectoryEnd(zone, statusMessage);

}