org.alfresco.repo.security.sync.TenantChainingUserRegistrySynchronizer.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.security.sync.TenantChainingUserRegistrySynchronizer.java

Source

/*
 * Copyright (C) 2005-2012 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */
package org.alfresco.repo.security.sync;

import java.io.IOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.batch.BatchProcessor;
import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
import org.alfresco.repo.domain.tenant.TenantAdminDAO;
import org.alfresco.repo.domain.tenant.TenantEntity;
import org.alfresco.repo.lock.JobLockService;
import org.alfresco.repo.lock.LockAcquisitionException;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.management.subsystems.ChildApplicationContextManager;
import org.alfresco.repo.security.SEIPTenantIntegration;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.authority.UnknownAuthorityException;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.rule.RuleService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.TraceableThreadFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;

/**
 * A <code>TenantChainingUserRegistrySynchronizer</code> is responsible for
 * synchronizing Alfresco's local user (person) and group (authority)
 * information with the external subsystems in the authentication chain (most
 * typically LDAP directories). When the {@link #synchronize(boolean)} method is
 * called, it visits each {@link UserRegistry} bean in the 'chain' of
 * application contexts, managed by a {@link ChildApplicationContextManager},
 * and compares its timestamped user and group information with the local users
 * and groups last retrieved from the same source. Any updates and additions
 * made to those users and groups are applied to the local copies. The ordering
 * of each {@link UserRegistry} in the chain determines its precedence when it
 * comes to user and group name collisions. The {@link JobLockService} is used
 * to ensure that in a cluster, no two nodes actually run a synchronize at the
 * same time.
 * <p>
 * The <code>force</code> argument determines whether a complete or partial set
 * of information is queried from the {@link UserRegistry}. When
 * <code>true</code> then <i>all</i> users and groups are queried. With this
 * complete set of information, the synchronizer is able to identify which users
 * and groups have been deleted, so it will delete users and groups as well as
 * update and create them. Since processing all users and groups may be fairly
 * time consuming, it is recommended this mode is only used by a background
 * scheduled synchronization job. When the argument is <code>false</code> then
 * only those users and groups modified since the most recent modification date
 * of all the objects last queried from the same {@link UserRegistry} are
 * retrieved. In this mode, local users and groups are created and updated, but
 * not deleted (except where a name collision with a lower priority
 * {@link UserRegistry} is detected). This 'differential' mode is much faster,
 * and by default is triggered on subsystem startup and also by
 * {@link #createMissingPerson(String)} when a user is successfully
 * authenticated who doesn't yet have a local person object in Alfresco. This
 * should mean that new users and their group information are pulled over from
 * LDAP servers as and when required.
 * 
 * @author dward
 */
public class TenantChainingUserRegistrySynchronizer extends AbstractLifecycleBean
        implements UserRegistrySynchronizer, ApplicationEventPublisherAware {
    /** The logger. */
    private static final Log logger = LogFactory.getLog(TenantChainingUserRegistrySynchronizer.class);

    /**
     * The name of the lock used to ensure that a synchronize does not run on
     * more than one node at the same time.
     */
    private static final QName LOCK_QNAME = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI,
            "TenantChainingUserRegistrySynchronizer");

    /**
     * The time this lock will persist for in the database (now only 2 minutes
     * but refreshed at regular intervals).
     */
    private static final long LOCK_TTL = 1000 * 60 * 2;

    /** The path in the attribute service below which we persist attributes. */
    public static final String ROOT_ATTRIBUTE_PATH = ".TenantChainingUserRegistrySynchronizer";

    /**
     * The label under which the last group modification timestamp is stored for
     * each zone.
     */
    private static final String GROUP_LAST_MODIFIED_ATTRIBUTE = "GROUP";

    /**
     * The label under which the last user modification timestamp is stored for
     * each zone.
     */
    private static final String PERSON_LAST_MODIFIED_ATTRIBUTE = "PERSON";

    /** The manager for the autentication chain to be traversed. */
    private ChildApplicationContextManager applicationContextManager;

    /**
     * The name used to look up a {@link UserRegistry} bean in each child
     * application context.
     */
    private String sourceBeanName;

    /** The authority service. */
    private AuthorityService authorityService;

    /** The person service. */
    private PersonService personService;

    /** The attribute service. */
    private AttributeService attributeService;

    /** The transaction service. */
    private TransactionService transactionService;

    /** The rule service. */
    private RuleService ruleService;

    /** The job lock service. */
    private JobLockService jobLockService;

    /** The application event publisher. */
    private ApplicationEventPublisher applicationEventPublisher;

    /** Should we trigger a differential sync when missing people log in?. */
    private boolean syncWhenMissingPeopleLogIn = true;

    /** Should we trigger a differential sync on startup?. */
    private boolean syncOnStartup = true;

    /** Should we auto create a missing person on log in?. */
    private boolean autoCreatePeopleOnLogin = true;

    /** The number of entries to process before reporting progress. */
    private int loggingInterval = 100;

    /** The number of worker threads. */
    private int workerThreads = 2;

    private MBeanServerConnection mbeanServer;

    /** Allow a full sync to perform deletions? */
    private boolean allowDeletions = true;

    private TenantAdminDAO tenantAdminDAO;

    /**
     * Sets the application context manager.
     * 
     * @param applicationContextManager
     *            the applicationContextManager to set
     */
    public void setApplicationContextManager(ChildApplicationContextManager applicationContextManager) {
        this.applicationContextManager = applicationContextManager;
    }

    /**
     * Sets the name used to look up a {@link UserRegistry} bean in each child
     * application context.
     * 
     * @param sourceBeanName
     *            the bean name
     */
    public void setSourceBeanName(String sourceBeanName) {
        this.sourceBeanName = sourceBeanName;
    }

    /**
     * Sets the authority service.
     * 
     * @param authorityService
     *            the new authority service
     */
    public void setAuthorityService(AuthorityService authorityService) {
        this.authorityService = authorityService;
    }

    /**
     * Sets the person service.
     * 
     * @param personService
     *            the new person service
     */
    public void setPersonService(PersonService personService) {
        this.personService = personService;
    }

    /**
     * Sets the attribute service.
     * 
     * @param attributeService
     *            the new attribute service
     */
    public void setAttributeService(AttributeService attributeService) {
        this.attributeService = attributeService;
    }

    /**
     * Sets the transaction service.
     * 
     * @param transactionService
     *            the transaction service
     */
    public void setTransactionService(TransactionService transactionService) {
        this.transactionService = transactionService;
    }

    /**
     * Sets the rule service.
     * 
     * @param ruleService
     *            the new rule service
     */
    public void setRuleService(RuleService ruleService) {
        this.ruleService = ruleService;
    }

    /**
     * Sets the job lock service.
     * 
     * @param jobLockService
     *            the job lock service
     */
    public void setJobLockService(JobLockService jobLockService) {
        this.jobLockService = jobLockService;
    }

    public void setTenantAdminDAO(TenantAdminDAO tenantAdminDAO) {
        this.tenantAdminDAO = tenantAdminDAO;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.springframework.context.ApplicationEventPublisherAware#
     * setApplicationEventPublisher(org.springframework.context
     * .ApplicationEventPublisher)
     */
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * Controls whether we auto create a missing person on log in.
     * 
     * @param autoCreatePeopleOnLogin
     *            <code>true</code> if we should auto create a missing person on
     *            log in
     */
    public void setAutoCreatePeopleOnLogin(boolean autoCreatePeopleOnLogin) {
        this.autoCreatePeopleOnLogin = autoCreatePeopleOnLogin;
    }

    /**
     * Controls whether we trigger a differential sync when missing people log
     * in.
     * 
     * @param syncWhenMissingPeopleLogIn
     *            <codetrue</code> if we should trigger a sync when missing
     *            people log in
     */
    public void setSyncWhenMissingPeopleLogIn(boolean syncWhenMissingPeopleLogIn) {
        this.syncWhenMissingPeopleLogIn = syncWhenMissingPeopleLogIn;
    }

    /**
     * Controls whether we trigger a differential sync when the subsystem starts
     * up.
     * 
     * @param syncOnStartup
     *            <codetrue</code> if we should trigger a sync on startup
     */
    public void setSyncOnStartup(boolean syncOnStartup) {
        this.syncOnStartup = syncOnStartup;
    }

    /**
     * Sets the number of entries to process before reporting progress.
     * 
     * @param loggingInterval
     *            the number of entries to process before reporting progress or
     *            zero to disable progress reporting.
     */
    public void setLoggingInterval(int loggingInterval) {
        this.loggingInterval = loggingInterval;
    }

    /**
     * Sets the number of worker threads.
     * 
     * @param workerThreads
     *            the number of worker threads
     */
    public void setWorkerThreads(int workerThreads) {
        this.workerThreads = workerThreads;
    }

    /**
     * Fullsync is run with deletions. By default is set to true.
     * 
     * @param allowDeletions
     */
    public void setAllowDeletions(boolean allowDeletions) {
        this.allowDeletions = allowDeletions;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.alfresco.repo.security.sync.UserRegistrySynchronizer#synchronize(
     * boolean, boolean, boolean)
     */
    public void synchronize(final boolean forceUpdate, final boolean isFullSync, final boolean splitTxns) {

        if (AuthenticationUtil.isMtEnabled()) {
            List<TenantEntity> listTenants = tenantAdminDAO.listTenants();
            for (TenantEntity tenantEntity : listTenants) {
                logger.info("Sycnhronizing a tenant:" + tenantEntity.getTenantDomain());
                if (tenantEntity.getEnabled().booleanValue()) {
                    AuthenticationUtil.runAs(new RunAsWork<Object>() {
                        public Object doWork() throws Exception {
                            try {
                                synchronizeInternal(forceUpdate, isFullSync, splitTxns);
                            } catch (Exception e) {
                                TenantChainingUserRegistrySynchronizer.logger
                                        .warn("Failed initial synchronize with user registries", e);
                            }
                            return null;
                        }
                    }, SEIPTenantIntegration.getSystemUserByTenantId(tenantEntity.getTenantDomain()));
                }
            }
        }
        AuthenticationUtil.runAs(new RunAsWork<Object>() {
            public Object doWork() throws Exception {
                try {
                    logger.info("Sycnhronizing the default tenant:");
                    synchronizeInternal(forceUpdate, isFullSync, splitTxns);
                } catch (Exception e) {
                    TenantChainingUserRegistrySynchronizer.logger
                            .warn("Failed initial synchronize with user registries", e);
                }
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
    }

    public void synchronize(String tenantId, final boolean forceUpdate, final boolean isFullSync,
            final boolean splitTxns) {

        if (AuthenticationUtil.isMtEnabled()) {
            TenantEntity tenant = tenantAdminDAO.getTenant(tenantId);
            logger.info("Sycnhronizing a tenant:" + tenantId);
            if (tenant.getEnabled().booleanValue()) {
                logger.warn("Information for tenant is missing or it is disabled!");
            }
            AuthenticationUtil.runAs(new RunAsWork<Object>() {
                public Object doWork() throws Exception {
                    try {
                        synchronizeInternal(forceUpdate, isFullSync, splitTxns);
                    } catch (Exception e) {
                        TenantChainingUserRegistrySynchronizer.logger
                                .warn("Failed initial synchronize with user registries", e);
                    }
                    return null;
                }
            }, SEIPTenantIntegration.getSystemUserByTenantId(tenant.getTenantDomain()));
        }
    }

    private void synchronizeInternal(boolean forceUpdate, boolean isFullSync, final boolean splitTxns) {
        TenantChainingUserRegistrySynchronizer.logger
                .debug("Running a sync for domain: " + SEIPTenantIntegration.getTenantId());
        if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {

            if (forceUpdate) {
                TenantChainingUserRegistrySynchronizer.logger.debug("Running a full sync.");
            } else {
                TenantChainingUserRegistrySynchronizer.logger.debug("Running a differential sync.");
            }
            if (allowDeletions) {
                TenantChainingUserRegistrySynchronizer.logger.debug("deletions are allowed");
            } else {
                TenantChainingUserRegistrySynchronizer.logger.debug("deletions are not allowed");
            }
            // Don't proceed with the sync if the repository is read only
            if (this.transactionService.isReadOnly()) {
                TenantChainingUserRegistrySynchronizer.logger
                        .warn("Unable to proceed with user registry synchronization. Repository is read only.");
                return;
            }
        }

        // Don't proceed with the sync if the repository is read only
        if (this.transactionService.isReadOnly()) {
            TenantChainingUserRegistrySynchronizer.logger
                    .warn("Unable to proceed with user registry synchronization. Repository is read only.");
            return;
        }

        // Create a background executor that will refresh our lock. This means
        // we can request a lock with a relatively
        // small persistence time and not worry about it lasting after server
        // restarts. Note we use an independent
        // executor because this is a compound operation that spans accross
        // multiple batch processors.
        String lockToken = null;
        TraceableThreadFactory threadFactory = new TraceableThreadFactory();
        threadFactory.setNamePrefix("TenantChainingUserRegistrySynchronizer lock refresh");
        threadFactory.setThreadDaemon(true);
        ScheduledExecutorService lockRefresher = new ScheduledThreadPoolExecutor(1, threadFactory);

        // Let's ensure all exceptions get logged
        try {
            // First, try to obtain a lock to ensure we are the only node trying
            // to run this job
            try {
                if (splitTxns) {
                    // If this is an automated sync on startup or scheduled
                    // sync, don't even wait around for the lock.
                    // Assume the sync will be completed on another node.
                    lockToken = this.transactionService.getRetryingTransactionHelper()
                            .doInTransaction(new RetryingTransactionCallback<String>() {
                                public String execute() throws Throwable {
                                    return TenantChainingUserRegistrySynchronizer.this.jobLockService.getLock(
                                            TenantChainingUserRegistrySynchronizer.LOCK_QNAME,
                                            TenantChainingUserRegistrySynchronizer.LOCK_TTL, 0, 1);
                                }
                            }, false, splitTxns);
                } else {
                    // If this is a login-triggered sync, give it a few retries
                    // before giving up
                    lockToken = this.jobLockService.getLock(TenantChainingUserRegistrySynchronizer.LOCK_QNAME,
                            TenantChainingUserRegistrySynchronizer.LOCK_TTL, 3000, 10);
                }
            } catch (LockAcquisitionException e) {
                // Don't proceed with the sync if it is running on another node
                TenantChainingUserRegistrySynchronizer.logger.warn(
                        "User registry synchronization already running in another thread. Synchronize aborted");
                return;
            }

            // Schedule the lock refresh to run at regular intervals
            final String token = lockToken;
            lockRefresher.scheduleAtFixedRate(new Runnable() {
                public void run() {
                    TenantChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper()
                            .doInTransaction(new RetryingTransactionCallback<Object>() {
                                public Object execute() throws Throwable {
                                    TenantChainingUserRegistrySynchronizer.this.jobLockService.refreshLock(token,
                                            TenantChainingUserRegistrySynchronizer.LOCK_QNAME,
                                            TenantChainingUserRegistrySynchronizer.LOCK_TTL);
                                    return null;
                                }
                            }, false, splitTxns);
                }
            }, TenantChainingUserRegistrySynchronizer.LOCK_TTL / 2,
                    TenantChainingUserRegistrySynchronizer.LOCK_TTL / 2, TimeUnit.MILLISECONDS);

            Set<String> visitedZoneIds = new TreeSet<String>();
            Collection<String> instanceIds = this.applicationContextManager.getInstanceIds();

            // Work out the set of all zone IDs in the authentication chain so
            // that we can decide which users / groups
            // need 're-zoning'
            Set<String> allZoneIds = new TreeSet<String>();
            for (String id : instanceIds) {
                allZoneIds.add(AuthorityService.ZONE_AUTH_EXT_PREFIX + id);
            }
            for (String id : instanceIds) {
                ApplicationContext context = this.applicationContextManager.getApplicationContext(id);
                try {
                    UserRegistry plugin = (UserRegistry) context.getBean(this.sourceBeanName);
                    if (!(plugin instanceof ActivateableBean) || ((ActivateableBean) plugin).isActive()) {
                        if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                            mbeanServer = (MBeanServerConnection) getApplicationContext()
                                    .getBean("alfrescoMBeanServer");
                            try {
                                StringBuilder nameBuff = new StringBuilder(200).append(
                                        "Alfresco:Type=Configuration,Category=Authentication,id1=managed,id2=")
                                        .append(URLDecoder.decode(id, "UTF-8"));
                                ObjectName name = new ObjectName(nameBuff.toString());
                                if (mbeanServer != null && mbeanServer.isRegistered(name)) {
                                    MBeanInfo info = mbeanServer.getMBeanInfo(name);
                                    MBeanAttributeInfo[] attributes = info.getAttributes();
                                    TenantChainingUserRegistrySynchronizer.logger.debug(id + " attributes:");
                                    for (MBeanAttributeInfo attribute : attributes) {
                                        Object value = mbeanServer.getAttribute(name, attribute.getName());
                                        TenantChainingUserRegistrySynchronizer.logger
                                                .debug(attribute.getName() + " = " + value);
                                    }
                                }
                            } catch (UnsupportedEncodingException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            } catch (MalformedObjectNameException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            } catch (InstanceNotFoundException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            } catch (IntrospectionException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            } catch (AttributeNotFoundException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            } catch (ReflectionException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            } catch (MBeanException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            } catch (IOException e) {
                                if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                                    TenantChainingUserRegistrySynchronizer.logger.warn("Exception during logging",
                                            e);
                                }
                            }

                        }
                        if (TenantChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
                            TenantChainingUserRegistrySynchronizer.logger
                                    .info("Synchronizing users and groups with user registry '" + id + "'");
                        }
                        if (isFullSync && TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                            TenantChainingUserRegistrySynchronizer.logger
                                    .warn("Full synchronization with user registry '" + id + "'");
                            if (allowDeletions) {
                                TenantChainingUserRegistrySynchronizer.logger.warn(
                                        "Some users and groups previously created by synchronization with this user registry may be removed.");
                            } else {
                                TenantChainingUserRegistrySynchronizer.logger.warn(
                                        "Deletions are disabled. Users and groups removed from this registry will be logged only and will remain in the repository. Users previously found in a different registry will be moved in the repository rather than recreated.");
                            }
                        }
                        // Work out whether we should do the work in a separate
                        // transaction (it's most performant if we
                        // bunch it into small transactions, but if we are doing
                        // a sync on login, it has to be the same
                        // transaction)
                        boolean requiresNew = splitTxns || AlfrescoTransactionSupport
                                .getTransactionReadState() == TxnReadState.TXN_READ_ONLY;

                        syncWithPlugin(id, plugin, forceUpdate, isFullSync, requiresNew, visitedZoneIds,
                                allZoneIds);
                    }
                } catch (NoSuchBeanDefinitionException e) {
                    // Ignore and continue
                }

            }
        } catch (RuntimeException e) {
            TenantChainingUserRegistrySynchronizer.logger.error("Synchronization aborted due to error", e);
            throw e;
        }
        // Release the lock if necessary
        finally {
            if (lockToken != null) {
                // Cancel the lock refresher
                // Because we may hit a perfect storm when trying to interrupt
                // workers in their unsynchronized getTask()
                // method we can't wait indefinitely and may have to retry the
                // shutdown
                int trys = 0;
                do {
                    lockRefresher.shutdown();
                    try {
                        lockRefresher.awaitTermination(TenantChainingUserRegistrySynchronizer.LOCK_TTL,
                                TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                    }
                } while (!lockRefresher.isTerminated() && trys++ < 3);
                if (!lockRefresher.isTerminated()) {
                    lockRefresher.shutdownNow();
                    TenantChainingUserRegistrySynchronizer.logger.error("Failed to shut down lock refresher");
                }

                final String token = lockToken;
                this.transactionService.getRetryingTransactionHelper()
                        .doInTransaction(new RetryingTransactionCallback<Object>() {
                            public Object execute() throws Throwable {
                                TenantChainingUserRegistrySynchronizer.this.jobLockService.releaseLock(token,
                                        TenantChainingUserRegistrySynchronizer.LOCK_QNAME);
                                return null;
                            }
                        }, false, splitTxns);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#
     * getPersonMappedProperties(java.lang.String)
     */
    public Set<QName> getPersonMappedProperties(String username) {
        Set<String> authorityZones = this.authorityService.getAuthorityZones(username);
        if (authorityZones == null) {
            return Collections.emptySet();
        }
        Collection<String> instanceIds = this.applicationContextManager.getInstanceIds();

        // Visit the user registries in priority order and return the person
        // mapping of the first registry that matches
        // one of the person's zones
        for (String id : instanceIds) {
            String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + id;
            if (!authorityZones.contains(zoneId)) {
                continue;
            }
            ApplicationContext context = this.applicationContextManager.getApplicationContext(id);
            try {
                UserRegistry plugin = (UserRegistry) context.getBean(this.sourceBeanName);
                if (!(plugin instanceof ActivateableBean) || ((ActivateableBean) plugin).isActive()) {
                    return plugin.getPersonMappedProperties();
                }
            } catch (NoSuchBeanDefinitionException e) {
                // Ignore and continue
            }
        }

        return Collections.emptySet();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#
     * createMissingPerson(java.lang.String)
     */
    public boolean createMissingPerson(String userName) {
        // synchronize or auto-create the missing person if we are allowed
        if (userName != null && !userName.equals(AuthenticationUtil.getSystemUserName())) {
            if (this.syncWhenMissingPeopleLogIn) {
                try {
                    synchronizeInternal(false, false, false);
                } catch (Exception e) {
                    // We don't want to fail the whole login if we can help it
                    TenantChainingUserRegistrySynchronizer.logger
                            .warn("User authenticated but failed to sync with user registry", e);
                }
                if (this.personService.personExists(userName)) {
                    return true;
                }
            }
            if (this.autoCreatePeopleOnLogin && this.personService.createMissingPeople()) {
                AuthorityType authorityType = AuthorityType.getAuthorityType(userName);
                if (authorityType == AuthorityType.USER) {
                    this.personService.getPerson(userName);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Synchronizes local groups and users with a {@link UserRegistry} for a
     * particular zone, optionally handling deletions.
     * 
     * @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");
        }
    }

    private boolean isTenantEnabled(String tenantDomain) {
        TenantEntity tenant = null;
        return (tenant = tenantAdminDAO.getTenant(tenantDomain)) != null && tenant.getEnabled().booleanValue();
    }

    /**
     * Gets the persisted most recent update time for a label and zone.
     * 
     * @param label
     *            the label
     * @param zoneId
     *            the zone id
     * @return the most recent update time in milliseconds
     */
    private long getMostRecentUpdateTime(final String label, final String zoneId, boolean splitTxns) {
        return this.transactionService.getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionCallback<Long>() {
                    public Long execute() throws Throwable {
                        Long updateTime = (Long) TenantChainingUserRegistrySynchronizer.this.attributeService
                                .getAttribute(TenantChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, label,
                                        zoneId);
                        return updateTime == null ? -1 : updateTime;
                    }
                }, true, splitTxns);
    }

    /**
     * Persists the most recent update time for a label and zone.
     * 
     * @param label
     *            the label
     * @param zoneId
     *            the zone id
     * @param lastModifiedMillis
     *            the update time in milliseconds
     * @param splitTxns
     *            Can the modifications to Alfresco be split across multiple
     *            transactions for maximum performance? If <code>true</code>,
     *            the attribute is persisted in a new transaction for increased
     *            performance and reliability.
     */
    private void setMostRecentUpdateTime(final String label, final String zoneId, final long lastModifiedMillis,
            boolean splitTxns) {
        this.transactionService.getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Object>() {
                    public Object execute() throws Throwable {
                        TenantChainingUserRegistrySynchronizer.this.attributeService.setAttribute(
                                Long.valueOf(lastModifiedMillis),
                                TenantChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, label, zoneId);
                        return null;
                    }
                }, false, splitTxns);
    }

    /**
     * Gets the default set of zones to set on a person or group belonging to
     * the user registry with the given zone ID. We add the default zone as well
     * as the zone corresponding to the user registry so that the users and
     * groups are visible in the UI.
     * 
     * @param zoneId
     *            the zone id
     * @return the zone set
     */
    private Set<String> getZones(final String zoneId) {
        Set<String> zones = new HashSet<String>(5);
        zones.add(AuthorityService.ZONE_APP_DEFAULT);
        zones.add(zoneId);
        return zones;
    }

    /**
     * Modifies an authority's zone set from oldZones to newZones in the most
     * efficient manner (avoiding unnecessary reindexing cost).
     * 
     * @param authorityName
     * @param oldZones
     * @param newZones
     */
    private void updateAuthorityZones(String authorityName, Set<String> oldZones, final Set<String> newZones) {
        Set<String> zonesToRemove = new HashSet<String>(oldZones);
        zonesToRemove.removeAll(newZones);
        // Let's keep the authority in the alfresco auth zone if it was already
        // there. Otherwise we may have to
        // regenerate all paths to this authority from site groups, which could
        // be very expensive!
        zonesToRemove.remove(AuthorityService.ZONE_AUTH_ALFRESCO);
        if (!zonesToRemove.isEmpty()) {
            this.authorityService.removeAuthorityFromZones(authorityName, zonesToRemove);
        }
        Set<String> zonesToAdd = new HashSet<String>(newZones);
        zonesToAdd.removeAll(oldZones);
        if (!zonesToAdd.isEmpty()) {
            this.authorityService.addAuthorityToZones(authorityName, zonesToAdd);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.springframework.extensions.surf.util.AbstractLifecycleBean#
     * onBootstrap(org.springframework.context. ApplicationEvent)
     */
    @Override
    protected void onBootstrap(ApplicationEvent event) {
        // Do an initial differential sync on startup, using transaction
        // splitting. This ensures that on the very
        // first startup, we don't have to wait for a very long login operation
        // to trigger the first sync!

        synchronize(false, false, true);
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.springframework.extensions.surf.util.AbstractLifecycleBean#
     * onShutdown(org.springframework.context. ApplicationEvent)
     */
    @Override
    protected void onShutdown(ApplicationEvent event) {
    }

    protected abstract class BaseBatchProcessWorker<T> implements BatchProcessWorker<T> {
        public final void beforeProcess() throws Throwable {
            // Disable rules
            TenantChainingUserRegistrySynchronizer.this.ruleService.disableRules();
            // Authentication
            AuthenticationUtil.setRunAsUser(AuthenticationUtil.getSystemUserName());
        }

        public final void afterProcess() throws Throwable {
            // Enable rules
            TenantChainingUserRegistrySynchronizer.this.ruleService.enableRules();
            // Clear authentication
            AuthenticationUtil.clearCurrentSecurityContext();
        }
    }
}