Java tutorial
/* * Copyright (C) 2005-2013 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.cggh.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.admin.SysAdminParams; import org.alfresco.repo.batch.BatchProcessor; import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker; import org.alfresco.repo.dictionary.constraint.NameChecker; 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.authentication.AuthenticationException; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.authentication.AuthenticatorDeletedEvent; import org.alfresco.repo.security.authority.UnknownAuthorityException; import org.alfresco.repo.security.sync.ChainingUserRegistrySynchronizerStatus; import org.alfresco.repo.security.sync.NodeDescription; import org.alfresco.repo.security.sync.SyncStatus; import org.alfresco.repo.security.sync.SynchronizeDiagnostic; import org.alfresco.repo.security.sync.SynchronizeDiagnosticImpl; import org.alfresco.repo.security.sync.SynchronizeDirectoryEndEvent; import org.alfresco.repo.security.sync.SynchronizeDirectoryStartEvent; import org.alfresco.repo.security.sync.SynchronizeEndEvent; import org.alfresco.repo.security.sync.SynchronizeStartEvent; import org.alfresco.repo.security.sync.TestableChainingUserRegistrySynchronizer; import org.alfresco.repo.security.sync.UserRegistry; import org.alfresco.repo.security.sync.UserRegistrySynchronizer; 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.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.PropertyCheck; import org.alfresco.util.PropertyMap; import org.alfresco.util.TraceableThreadFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cggh.repo.security.person.AvatarService; 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; import org.springframework.extensions.surf.util.I18NUtil; /** * A <code>ChainingUserRegistrySynchronizer</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, 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 CustomChainingUserRegistrySynchronizer extends AbstractLifecycleBean implements UserRegistrySynchronizer, ChainingUserRegistrySynchronizerStatus, TestableChainingUserRegistrySynchronizer, ApplicationEventPublisherAware { /** The logger. */ private static final Log logger = LogFactory.getLog(CustomChainingUserRegistrySynchronizer.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, "ChainingUserRegistrySynchronizer"); /** 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 = ".ChainingUserRegistrySynchronizer"; /** 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 label under which the status is stored for each zone. */ private static final String STATUS_ATTRIBUTE = "STATUS"; /** The label under which the status is stored for each zone. */ private static final String LAST_ERROR_ATTRIBUTE = "LAST_ERROR"; /** The label under which the status is stored for each zone. */ private static final String START_TIME_ATTRIBUTE = "START_TIME"; /** The label under which the status is stored for each zone. */ private static final String END_TIME_ATTRIBUTE = "END_TIME"; /** The label under which the status is stored for each zone. */ private static final String SERVER_ATTRIBUTE = "LAST_RUN_HOST"; /** The label under which the status is stored for each zone. */ private static final String SUMMARY_ATTRIBUTE = "SUMMARY"; /** 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; private AvatarService avatarService; /** The attribute service. */ private AttributeService attributeService; /** The transaction service. */ private TransactionService transactionService; /** 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; /** Controls whether to query for users and groups that have been deleted in LDAP */ private boolean syncDelete = true; /** Validates person names over cm:filename constraint **/ private NameChecker nameChecker; private SysAdminParams sysAdminParams; public void init() { PropertyCheck.mandatory(this, "attributeService", attributeService); PropertyCheck.mandatory(this, "authorityService", authorityService); PropertyCheck.mandatory(this, "personService", personService); PropertyCheck.mandatory(this, "attributeService", attributeService); PropertyCheck.mandatory(this, "transactionService", transactionService); PropertyCheck.mandatory(this, "jobLockService", jobLockService); PropertyCheck.mandatory(this, "applicationEventPublisher", applicationEventPublisher); PropertyCheck.mandatory(this, "sysAdminParams", sysAdminParams); } /** * Sets name checker */ public void setNameChecker(NameChecker nameChecker) { this.nameChecker = nameChecker; } /** * 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; } public void setAvatarService(AvatarService avatarService) { this.avatarService = avatarService; } /** * 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 job lock service. * * @param jobLockService * the job lock service */ public void setJobLockService(JobLockService jobLockService) { this.jobLockService = jobLockService; } /* * (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; } /** * Controls how deleted users and groups are handled. * By default is set to true. * * @param allowDeletions * If <b>true</b> the entries are deleted from alfresco. * If <b>false</b> then they are unlinked from their LDAP authentication zone but remain within alfresco. */ public void setAllowDeletions(boolean allowDeletions) { this.allowDeletions = allowDeletions; } /** * Controls whether to query for users and groups that have been deleted in LDAP. * For large LDAP directories the delete query is expensive and time consuming, needing to read the entire LDAP directory. * By default is set to true. * * @param syncDelete * If <b>false</b> then LDAP sync does not even attempt to search for deleted users. */ public void setSyncDelete(boolean syncDelete) { this.syncDelete = syncDelete; } @Override public SynchronizeDiagnostic testSynchronize(String authenticatorName) { SynchronizeDiagnosticImpl ret = new SynchronizeDiagnosticImpl(); Collection<String> instanceIds = this.applicationContextManager.getInstanceIds(); if (instanceIds.contains(authenticatorName)) { UserRegistry plugin; ApplicationContext context = this.applicationContextManager.getApplicationContext(authenticatorName); plugin = (UserRegistry) context.getBean(this.sourceBeanName); // If the bean is ActivateableBean check whether it is active if (plugin instanceof ActivateableBean) { if (!((ActivateableBean) plugin).isActive()) { ret.setActive(false); } } long groupLastModifiedMillis = getMostRecentUpdateTime( CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, authenticatorName, false); long personLastModifiedMillis = getMostRecentUpdateTime( CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, authenticatorName, false); Date groupLastModified = groupLastModifiedMillis == -1 ? null : new Date(groupLastModifiedMillis); Date personLastModified = personLastModifiedMillis == -1 ? null : new Date(personLastModifiedMillis); ret.setGroups(plugin.getGroupNames()); ret.setUsers(plugin.getPersonNames()); if (groupLastModified != null) { ret.setGroupLastSynced(groupLastModified); } else { // fake a date to test the group query groupLastModified = new Date(); } plugin.getGroups(groupLastModified); if (personLastModified != null) { ret.setPersonLastSynced(personLastModified); } else { // fake a date to test the person query personLastModified = new Date(); } plugin.getPersons(personLastModified); return ret; } Object params[] = { authenticatorName }; throw new AuthenticationException("authentication.err.validation.authenticator.notfound", params); } @Override public void synchronize(boolean forceUpdate, boolean isFullSync) { synchronizeInternal(forceUpdate, isFullSync, true); } private void synchronizeInternal(boolean forceUpdate, boolean isFullSync, final boolean splitTxns) { if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { if (forceUpdate) { CustomChainingUserRegistrySynchronizer.logger.debug("Running a full sync."); } else { CustomChainingUserRegistrySynchronizer.logger.debug("Running a differential sync."); } if (allowDeletions) { CustomChainingUserRegistrySynchronizer.logger.debug("deletions are allowed"); } else { CustomChainingUserRegistrySynchronizer.logger.debug("deletions are not allowed"); } // Don't proceed with the sync if the repository is read only if (this.transactionService.isReadOnly()) { CustomChainingUserRegistrySynchronizer.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()) { CustomChainingUserRegistrySynchronizer.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("ChainingUserRegistrySynchronizer 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 CustomChainingUserRegistrySynchronizer.this.jobLockService.getLock( CustomChainingUserRegistrySynchronizer.LOCK_QNAME, CustomChainingUserRegistrySynchronizer.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(CustomChainingUserRegistrySynchronizer.LOCK_QNAME, CustomChainingUserRegistrySynchronizer.LOCK_TTL, 3000, 10); } } catch (LockAcquisitionException e) { // Don't proceed with the sync if it is running on another node CustomChainingUserRegistrySynchronizer.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() { CustomChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Object>() { public Object execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.jobLockService.refreshLock(token, CustomChainingUserRegistrySynchronizer.LOCK_QNAME, CustomChainingUserRegistrySynchronizer.LOCK_TTL); return null; } }, false, splitTxns); } }, CustomChainingUserRegistrySynchronizer.LOCK_TTL / 2, CustomChainingUserRegistrySynchronizer.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); } // Collect the plugins that we can sync : zoneId, plugin Map<String, UserRegistry> plugins = new HashMap<String, UserRegistry>(); for (String id : instanceIds) { UserRegistry plugin; try { ApplicationContext context = this.applicationContextManager.getApplicationContext(id); plugin = (UserRegistry) context.getBean(this.sourceBeanName); } catch (RuntimeException e) { // The bean doesn't exist or this subsystem won't start. The reason would have been logged. Ignore and continue. continue; } if (!(plugin instanceof ActivateableBean) || ((ActivateableBean) plugin).isActive()) { // yes this plugin needs to be synced plugins.put(id, plugin); } } /** * Sync starts here */ notifySyncStart(plugins.keySet()); for (String id : instanceIds) { UserRegistry plugin = plugins.get(id); if (plugin != null) { // If debug is enabled then dump out the contents of the authentication JMX bean if (CustomChainingUserRegistrySynchronizer.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(); CustomChainingUserRegistrySynchronizer.logger.debug(id + " attributes:"); for (MBeanAttributeInfo attribute : attributes) { Object value = mbeanServer.getAttribute(name, attribute.getName()); CustomChainingUserRegistrySynchronizer.logger .debug(attribute.getName() + " = " + value); } } } catch (UnsupportedEncodingException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } catch (MalformedObjectNameException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } catch (InstanceNotFoundException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } catch (IntrospectionException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } catch (AttributeNotFoundException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } catch (ReflectionException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } catch (MBeanException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } catch (IOException e) { if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Exception during logging", e); } } } // end of debug dump of active JMX bean if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { CustomChainingUserRegistrySynchronizer.logger .info("Synchronizing users and groups with user registry '" + id + "'"); } if (isFullSync && CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger .warn("Full synchronization with user registry '" + id + "'"); if (allowDeletions) { CustomChainingUserRegistrySynchronizer.logger.warn( "Some users and groups previously created by synchronization with this user registry may be removed."); } else { CustomChainingUserRegistrySynchronizer.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; try { /** * Do the sync with the specified plugin */ syncWithPlugin(id, plugin, forceUpdate, isFullSync, requiresNew, visitedZoneIds, allZoneIds); this.applicationEventPublisher.publishEvent(new SynchronizeDirectoryEndEvent(this, id)); } catch (final RuntimeException e) { notifySyncDirectoryEnd(id, e); throw e; } } // if plugin exists } // for each instanceId //End of successful synchronization here notifySyncEnd(); } catch (final RuntimeException e) { notifySyncEnd(e); CustomChainingUserRegistrySynchronizer.logger.error("Synchronization aborted due to error", e); throw e; } finally { // Release the lock if necessary 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(CustomChainingUserRegistrySynchronizer.LOCK_TTL, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { } } while (!lockRefresher.isTerminated() && trys++ < 3); if (!lockRefresher.isTerminated()) { lockRefresher.shutdownNow(); CustomChainingUserRegistrySynchronizer.logger.error("Failed to shut down lock refresher"); } final String token = lockToken; this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Object>() { public Object execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.jobLockService.releaseLock(token, CustomChainingUserRegistrySynchronizer.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; } try { ApplicationContext context = this.applicationContextManager.getApplicationContext(id); UserRegistry plugin = (UserRegistry) context.getBean(this.sourceBeanName); if (!(plugin instanceof ActivateableBean) || ((ActivateableBean) plugin).isActive()) { return plugin.getPersonMappedProperties(); } } catch (RuntimeException e) { // The bean doesn't exist or this subsystem won't start. The reason would have been logged. Ignore and continue. } } return Collections.emptySet(); } @Override public boolean createMissingPerson(String userName) { // synchronise 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 CustomChainingUserRegistrySynchronizer.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; } /** * Lookup table for sync process used by syncWithPlugin * */ private enum SyncProcess { GROUP_ANALYSIS("1 Group Analysis"), MISSING_AUTHORITY( "2 Missing Authority Scanning"), GROUP_CREATION_AND_ASSOCIATION_DELETION( "3 Group Creation and Association Deletion"), GROUP_ASSOCIATION_CREATION( "4 Group Association Creation"), PERSON_ASSOCIATION( "5 User Association"), USER_CREATION( "6 User Creation and Association"), AUTHORITY_DELETION( "7 Authority Deletion"); SyncProcess(String title) { this.title = title; } public String getTitle(String zone) { return "Synchronization,Category=directory,id1=" + zone + ",id2=" + title; } private String title; } /** * 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; // Batch Process Names final String reservedBatchProcessNames[] = { SyncProcess.GROUP_ANALYSIS.getTitle(zone), SyncProcess.USER_CREATION.getTitle(zone), SyncProcess.MISSING_AUTHORITY.getTitle(zone), SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone), SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone), SyncProcess.PERSON_ASSOCIATION.getTitle(zone), SyncProcess.AUTHORITY_DELETION.getTitle(zone) }; notifySyncDirectoryStart(zone, reservedBatchProcessNames); // Ensure that the zoneId exists before multiple threads start using it this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { authorityService.getOrCreateZone(zoneId); return null; } }, false, splitTxns); // The set of zones we associate with new objects (default plus registry specific) final Set<String> zoneSet = getZones(zoneId); long lastModifiedMillis = forceUpdate ? -1 : getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, splitTxns); Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis); if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { if (lastModified == null) { CustomChainingUserRegistrySynchronizer.logger .info("Retrieving all groups from user registry '" + zone + "'"); } else { CustomChainingUserRegistrySynchronizer.logger.info( "Retrieving groups changed since " + DateFormat.getDateTimeInstance().format(lastModified) + " from user registry '" + zone + "'"); } } // First, analyze the group structure. Create maps of authorities to their parents for associations to create // and delete. Also deal with 'overlaps' with other zones in the authentication chain. final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>( SyncProcess.GROUP_ANALYSIS.getTitle(zone), this.transactionService.getRetryingTransactionHelper(), userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger, this.loggingInterval); class Analyzer extends BaseBatchProcessWorker<NodeDescription> { private final Map<String, String> groupsToCreate = new TreeMap<String, String>(); private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap(); private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap(); private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>(); private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>(); private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>(); private List<String> personsProcessed = new LinkedList<String>(); private Set<String> allZonePersons = Collections.emptySet(); private Set<String> deletionCandidates; private long latestTime; public Analyzer(final long latestTime) { this.latestTime = latestTime; } public long getLatestTime() { return this.latestTime; } public Set<String> getDeletionCandidates() { return this.deletionCandidates; } public String getIdentifier(NodeDescription entry) { return entry.getSourceId(); } public void process(NodeDescription group) throws Throwable { PropertyMap groupProperties = group.getProperties(); String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME); String groupShortName = CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(groupName); Set<String> groupZones = CustomChainingUserRegistrySynchronizer.this.authorityService .getAuthorityZones(groupName); if (groupZones == null) { // The group did not exist at all updateGroup(group, false); } else { // Check whether the group is in any of the authentication chain zones Set<String> intersection = new TreeSet<String>(groupZones); intersection.retainAll(allZoneIds); // Check whether the group is in any of the higher priority authentication chain zones Set<String> visited = new TreeSet<String>(intersection); visited.retainAll(visitedZoneIds); if (groupZones.contains(zoneId)) { // The group already existed in this zone: update the group updateGroup(group, true); } else if (!visited.isEmpty()) { // A group that exists in a different zone with higher precedence return; } else if (!allowDeletions || intersection.isEmpty()) { // Deletions are disallowed or the group exists, but not in a zone that's in the authentication // chain. May be due to upgrade or zone changes. Let's re-zone them if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName + "'. This group will in future be assumed to originate from user registry '" + zone + "'."); } updateAuthorityZones(groupName, groupZones, zoneSet); // The group now exists in this zone: update the group updateGroup(group, true); } else { // The group existed, but in a zone with lower precedence if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Recreating occluded group '" + groupShortName + "'. This group was previously created through synchronization with a lower priority user registry."); } CustomChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName); // create the group updateGroup(group, false); } } synchronized (this) { // Maintain the last modified date Date groupLastModified = group.getLastModified(); if (groupLastModified != null) { this.latestTime = Math.max(this.latestTime, groupLastModified.getTime()); } } } // Recursively walks and caches the authorities relating to and from this group so that we can later detect potential cycles private Set<String> getContainedAuthorities(String groupName) { // Return the cached children if it is processed Set<String> children = this.finalGroupChildAssocs.get(groupName); if (children != null) { return children; } // First, recurse to the parent most authorities for (String parent : CustomChainingUserRegistrySynchronizer.this.authorityService .getContainingAuthorities(null, groupName, true)) { getContainedAuthorities(parent); } // Now descend on unprocessed parents. return cacheContainedAuthorities(groupName); } private Set<String> cacheContainedAuthorities(String groupName) { // Return the cached children if it is processed Set<String> children = this.finalGroupChildAssocs.get(groupName); if (children != null) { return children; } // Descend on unprocessed parents. children = CustomChainingUserRegistrySynchronizer.this.authorityService .getContainedAuthorities(null, groupName, true); this.finalGroupChildAssocs.put(groupName, children); for (String child : children) { if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) { cacheContainedAuthorities(child); } } return children; } private synchronized void updateGroup(NodeDescription group, boolean existed) { PropertyMap groupProperties = group.getProperties(); String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME); String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME); if (groupDisplayName == null) { groupDisplayName = CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(groupName); } // Divide the child associations into person and group associations, dealing with case sensitivity Set<String> newChildPersons = newPersonSet(); Set<String> newChildGroups = new TreeSet<String>(); for (String child : group.getChildAssociations()) { if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) { newChildPersons.add(child); } else { newChildGroups.add(child); } } // Account for differences if already existing if (existed) { // Update the display name now CustomChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName, groupDisplayName); // Work out the association differences for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) { if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) { if (!newChildPersons.remove(child)) { recordParentAssociationDeletion(child, groupName); } } else { if (!newChildGroups.remove(child)) { recordParentAssociationDeletion(child, groupName); } } } } // Mark as created if new else { // Make sure each group to be created features in the association deletion map (as these are handled in the same phase) recordParentAssociationDeletion(groupName, null); this.groupsToCreate.put(groupName, groupDisplayName); } // Create new associations for (String child : newChildPersons) { // Make sure each person with association changes features as a key in the deletion map recordParentAssociationDeletion(child, null); recordParentAssociationCreation(child, groupName); } for (String child : newChildGroups) { // Make sure each group with association changes features as a key in the deletion map recordParentAssociationDeletion(child, null); recordParentAssociationCreation(child, groupName); } } private void recordParentAssociationDeletion(String child, String parent) { Map<String, Set<String>> parentAssocs; if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) { parentAssocs = this.personParentAssocsToDelete; } else { // Reflect the change in the map of final group associations (for cycle detection later) parentAssocs = this.groupParentAssocsToDelete; if (parent != null) { Set<String> children = this.finalGroupChildAssocs.get(parent); children.remove(child); } } Set<String> parents = parentAssocs.get(child); if (parents == null) { parents = new TreeSet<String>(); parentAssocs.put(child, parents); } if (parent != null) { parents.add(parent); } } private void recordParentAssociationCreation(String child, String parent) { Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER ? this.personParentAssocsToCreate : this.groupParentAssocsToCreate; Set<String> parents = parentAssocs.get(child); if (parents == null) { parents = new TreeSet<String>(); parentAssocs.put(child, parents); } if (parent != null) { parents.add(parent); } } private void validateGroupParentAssocsToCreate() { Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator(); while (i.hasNext()) { Map.Entry<String, Set<String>> entry = i.next(); String group = entry.getKey(); Set<String> parents = entry.getValue(); Deque<String> visited = new LinkedList<String>(); Iterator<String> j = parents.iterator(); while (j.hasNext()) { String parent = j.next(); visited.add(parent); if (validateAuthorityChildren(visited, group)) { // The association validated - commit it Set<String> children = finalGroupChildAssocs.get(parent); if (children == null) { children = new TreeSet<String>(); finalGroupChildAssocs.put(parent, children); } children.add(group); } else { // The association did not validate - prune it out if (logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Not adding group '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(group) + "' to group '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(parent) + "' as this creates a cyclic relationship"); } j.remove(); } visited.removeLast(); } if (parents.isEmpty()) { i.remove(); } } // Sort the group associations in parent-first order (root groups first) to minimize reindexing overhead Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>( this.groupParentAssocsToCreate.size() * 2); Deque<String> visited = new LinkedList<String>(); for (String authority : this.groupParentAssocsToCreate.keySet()) { visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate, sortedGroupAssociations); } this.groupParentAssocsToCreate = sortedGroupAssociations; } private boolean validateAuthorityChildren(Deque<String> visited, String authority) { if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) { return true; } if (visited.contains(authority)) { return false; } visited.add(authority); try { Set<String> children = this.finalGroupChildAssocs.get(authority); if (children != null) { for (String child : children) { if (!validateAuthorityChildren(visited, child)) { return false; } } } return true; } finally { visited.removeLast(); } } /** * Visits the given authority by recursively visiting its parents in associationsOld and then adding the * authority to associationsNew. Used to sort associationsOld into 'parent-first' order to minimize * reindexing overhead. * * @param visited * The ancestors that form the path to the authority to visit. Allows detection of cyclic child * associations. * @param authority * the authority to visit * @param associationsOld * the association map to sort * @param associationsNew * the association map to add to in parent-first order */ private boolean visitGroupParentAssocs(Deque<String> visited, String authority, Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) { if (visited.contains(authority)) { // Prevent cyclic paths (Shouldn't happen as we've already validated) return false; } visited.add(authority); try { if (!associationsNew.containsKey(authority)) { Set<String> oldParents = associationsOld.get(authority); if (oldParents != null) { Set<String> newParents = new TreeSet<String>(); for (String parent : oldParents) { if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) { newParents.add(parent); } } associationsNew.put(authority, newParents); } } return true; } finally { visited.removeLast(); } } private Set<String> newPersonSet() { return CustomChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive() ? new TreeSet<String>() : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); } private Map<String, Set<String>> newPersonMap() { return CustomChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive() ? new TreeMap<String, Set<String>>() : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER); } private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) { Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator(); StringBuilder groupList = null; while (i.hasNext()) { Map.Entry<String, Set<String>> entry = i.next(); String child = entry.getKey(); if (!toRetain.contains(child)) { if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { if (groupList == null) { groupList = new StringBuilder(1024); } else { groupList.setLength(0); } for (String parent : entry.getValue()) { if (groupList.length() > 0) { groupList.append(", "); } groupList.append('\'') .append(CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(parent)) .append('\''); } CustomChainingUserRegistrySynchronizer.logger .debug("Ignoring non-existent member '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(child) + "' in groups {" + groupList.toString() + "}"); } i.remove(); } } } private void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) { // MNT-12454 fix. If syncDelete is false, there is no need to pull all users and all groups from LDAP during the full synchronization. if ((syncDelete || !groupsToCreate.isEmpty()) && (isFullSync || !this.groupParentAssocsToDelete.isEmpty())) { final Set<String> allZonePersons = newPersonSet(); final Set<String> allZoneGroups = new TreeSet<String>(); // Add in current set of known authorities CustomChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { public Void execute() throws Throwable { allZonePersons .addAll(CustomChainingUserRegistrySynchronizer.this.authorityService .getAllAuthoritiesInZone(zoneId, AuthorityType.USER)); allZoneGroups .addAll(CustomChainingUserRegistrySynchronizer.this.authorityService .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP)); return null; } }, true, splitTxns); allZoneGroups.addAll(this.groupsToCreate.keySet()); // Prune our set of authorities according to deletions if (isFullSync) { final Set<String> personDeletionCandidates = newPersonSet(); personDeletionCandidates.addAll(allZonePersons); final Set<String> groupDeletionCandidates = new TreeSet<String>(); groupDeletionCandidates.addAll(allZoneGroups); this.deletionCandidates = new TreeSet<String>(); for (String person : userRegistry.getPersonNames()) { personDeletionCandidates.remove(person); } for (String group : userRegistry.getGroupNames()) { groupDeletionCandidates.remove(group); } this.deletionCandidates = new TreeSet<String>(); this.deletionCandidates.addAll(personDeletionCandidates); this.deletionCandidates.addAll(groupDeletionCandidates); if (allowDeletions) { allZonePersons.removeAll(personDeletionCandidates); allZoneGroups.removeAll(groupDeletionCandidates); } else { // Complete association deletion information by scanning deleted groups BatchProcessor<String> groupScanner = new BatchProcessor<String>( zone + " Missing Authority Scanning", CustomChainingUserRegistrySynchronizer.this.transactionService .getRetryingTransactionHelper(), this.deletionCandidates, CustomChainingUserRegistrySynchronizer.this.workerThreads, 20, CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger, CustomChainingUserRegistrySynchronizer.this.loggingInterval); groupScanner.process(new BaseBatchProcessWorker<String>() { @Override public String getIdentifier(String entry) { return entry; } @Override public void process(String authority) throws Throwable { //MNT-12454 fix. Modifies an authority's zone. Move authority from AUTH.EXT.LDAP1 to AUTH.ALF. updateAuthorityZones(authority, Collections.singleton(zoneId), Collections.singleton(AuthorityService.ZONE_AUTH_ALFRESCO)); } }, splitTxns); } } // Prune the group associations now that we have complete information this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups); logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups); this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups); // Pruning person associations will have to wait until we have passed over all persons and built up // this set this.allZonePersons = allZonePersons; if (!this.groupParentAssocsToDelete.isEmpty()) { // Create/update the groups and delete parent associations to be deleted // Batch 4 Group Creation and Association Deletion BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>( SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone), CustomChainingUserRegistrySynchronizer.this.transactionService .getRetryingTransactionHelper(), this.groupParentAssocsToDelete.entrySet(), CustomChainingUserRegistrySynchronizer.this.workerThreads, 20, CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger, CustomChainingUserRegistrySynchronizer.this.loggingInterval); groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() { public String getIdentifier(Map.Entry<String, Set<String>> entry) { return entry.getKey() + " " + entry.getValue(); } public void process(Map.Entry<String, Set<String>> entry) throws Throwable { String child = entry.getKey(); String groupDisplayName = Analyzer.this.groupsToCreate.get(child); if (groupDisplayName != null) { String groupShortName = CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(child); if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { CustomChainingUserRegistrySynchronizer.logger .debug("Creating group '" + groupShortName + "'"); } // create the group CustomChainingUserRegistrySynchronizer.this.authorityService.createAuthority( AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName, zoneSet); } else { // Maintain association deletions now. The creations will have to be done later once // we have performed all the deletions in order to avoid creating cycles maintainAssociationDeletions(child); } } }, splitTxns); } } } private void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) { // First validate the group associations to be created for potential cycles. Remove any offending association validateGroupParentAssocsToCreate(); // Now go ahead and create the group associations if (!this.groupParentAssocsToCreate.isEmpty()) { // Batch 5 Group Association Creation BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>( SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone), CustomChainingUserRegistrySynchronizer.this.transactionService .getRetryingTransactionHelper(), this.groupParentAssocsToCreate.entrySet(), CustomChainingUserRegistrySynchronizer.this.workerThreads, 20, CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger, CustomChainingUserRegistrySynchronizer.this.loggingInterval); groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() { public String getIdentifier(Map.Entry<String, Set<String>> entry) { return entry.getKey() + " " + entry.getValue(); } public void process(Map.Entry<String, Set<String>> entry) throws Throwable { maintainAssociationCreations(entry.getKey()); } }, splitTxns); } // Remove all the associations we have already dealt with this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed); // Filter out associations to authorities that simply can't exist (and log if debugging is enabled) logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons); // Update associations to persons not updated themselves if (!this.personParentAssocsToDelete.isEmpty()) { // Batch 6 Person Association BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>( SyncProcess.PERSON_ASSOCIATION.getTitle(zone), CustomChainingUserRegistrySynchronizer.this.transactionService .getRetryingTransactionHelper(), this.personParentAssocsToDelete.entrySet(), CustomChainingUserRegistrySynchronizer.this.workerThreads, 20, CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger, CustomChainingUserRegistrySynchronizer.this.loggingInterval); groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() { public String getIdentifier(Map.Entry<String, Set<String>> entry) { return entry.getKey() + " " + entry.getValue(); } public void process(Map.Entry<String, Set<String>> entry) throws Throwable { maintainAssociationDeletions(entry.getKey()); maintainAssociationCreations(entry.getKey()); } }, splitTxns); } } private void maintainAssociationDeletions(String authorityName) { boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER; Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName) : this.groupParentAssocsToDelete.get(authorityName); if (parentsToDelete != null && !parentsToDelete.isEmpty()) { for (String parent : parentsToDelete) { if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { CustomChainingUserRegistrySynchronizer.logger.debug("Removing '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(authorityName) + "' from group '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(parent) + "'"); } CustomChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent, authorityName); } } } private void maintainAssociationCreations(String authorityName) { boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER; Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName) : this.groupParentAssocsToCreate.get(authorityName); if (parents != null && !parents.isEmpty()) { if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { for (String groupName : parents) { CustomChainingUserRegistrySynchronizer.logger.debug("Adding '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(authorityName) + "' to group '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(groupName) + "'"); } } try { CustomChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents, authorityName); } catch (UnknownAuthorityException e) { // Let's force a transaction retry if a parent doesn't exist. It may be because we are // waiting for another worker thread to create it throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e); } catch (InvalidNodeRefException e) { // Another thread may have written the node, but it is not visible to this transaction // See: ALF-5471: 'authorityMigration' patch can report 'Node does not exist' throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e); } } // Remember that this person's associations have been maintained if (isPerson) { synchronized (this) { this.personsProcessed.add(authorityName); } } } } // end of Analyzer class // Run the first process the Group Analyzer final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis); int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns); groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns); // Process persons and their parent associations lastModifiedMillis = forceUpdate ? -1 : getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId, splitTxns); lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis); if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { if (lastModified == null) { CustomChainingUserRegistrySynchronizer.logger .info("Retrieving all users from user registry '" + zone + "'"); } else { CustomChainingUserRegistrySynchronizer.logger.info( "Retrieving users changed since " + DateFormat.getDateTimeInstance().format(lastModified) + " from user registry '" + zone + "'"); } } // User Creation and Association final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>( SyncProcess.USER_CREATION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(), userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger, this.loggingInterval); class PersonWorker extends BaseBatchProcessWorker<NodeDescription> { private long latestTime; public PersonWorker(final long latestTime) { this.latestTime = latestTime; } public long getLatestTime() { return this.latestTime; } public String getIdentifier(NodeDescription entry) { return entry.getSourceId(); } public void process(NodeDescription person) throws Throwable { // Make a mutable copy of the person properties, since they get written back to by person service HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>( person.getProperties()); String personName = personProperties.get(ContentModel.PROP_USERNAME).toString().trim(); personProperties.put(ContentModel.PROP_USERNAME, personName); // for invalid names will throw ConstraintException that will be catched by BatchProcessor$TxnCallback nameChecker.evaluate(personName); Set<String> zones = CustomChainingUserRegistrySynchronizer.this.authorityService .getAuthorityZones(personName); if (zones == null) { // The person did not exist at all if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { CustomChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'"); } CustomChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet); CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName, person.getProperties(), getLatestTime()); } else if (zones.contains(zoneId)) { // The person already existed in this zone: update the person if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { CustomChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'"); } CustomChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName, personProperties, false); CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName, person.getProperties(), getLatestTime()); } else { // Check whether the user is in any of the authentication chain zones Set<String> intersection = new TreeSet<String>(zones); intersection.retainAll(allZoneIds); // Check whether the user is in any of the higher priority authentication chain zones Set<String> visited = new TreeSet<String>(intersection); visited.retainAll(visitedZoneIds); if (visited.size() > 0) { // A person that exists in a different zone with higher precedence - ignore return; } else if (!allowDeletions || intersection.isEmpty()) { // The person exists, but in a different zone. Either deletions are disallowed or the zone is // not in the authentication chain. May be due to upgrade or zone changes. Let's re-zone them if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName + "'. This user will in future be assumed to originate from user registry '" + zone + "'."); } updateAuthorityZones(personName, zones, zoneSet); CustomChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName, personProperties, false); CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName, person.getProperties(), getLatestTime()); } else { // The person existed, but in a zone with lower precedence if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { CustomChainingUserRegistrySynchronizer.logger.warn("Recreating occluded user '" + personName + "'. This user was previously created through synchronization with a lower priority user registry."); } CustomChainingUserRegistrySynchronizer.this.personService.deletePerson(personName); CustomChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet); CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName, person.getProperties(), getLatestTime()); } } // Maintain association deletions and creations in one shot (safe to do this with persons as we can't // create cycles) groupAnalyzer.maintainAssociationDeletions(personName); groupAnalyzer.maintainAssociationCreations(personName); synchronized (this) { // Maintain the last modified date Date personLastModified = person.getLastModified(); if (personLastModified != null) { this.latestTime = Math.max(this.latestTime, personLastModified.getTime()); } } } } PersonWorker persons = new PersonWorker(lastModifiedMillis); int personProcessedCount = personProcessor.process(persons, splitTxns); // Process those associations to persons who themselves have not been updated groupAnalyzer.finalizeAssociations(userRegistry, splitTxns); // Only now that the whole tree has been processed is it safe to persist the last modified dates long latestTime = groupAnalyzer.getLatestTime(); if (latestTime != -1) { setMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, latestTime, splitTxns); } latestTime = persons.getLatestTime(); if (latestTime != -1) { setMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId, latestTime, splitTxns); } // Delete authorities if we have complete information for the zone Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates(); if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) { // Batch 7 Authority Deletion BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>( SyncProcess.AUTHORITY_DELETION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(), deletionCandidates, this.workerThreads, 10, this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger, this.loggingInterval); class AuthorityDeleter extends BaseBatchProcessWorker<String> { private int personProcessedCount; private int groupProcessedCount; public int getPersonProcessedCount() { return this.personProcessedCount; } public int getGroupProcessedCount() { return this.groupProcessedCount; } public String getIdentifier(String entry) { return entry; } public void process(String authority) throws Throwable { if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) { if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { CustomChainingUserRegistrySynchronizer.logger .debug("Deleting user '" + authority + "'"); } CustomChainingUserRegistrySynchronizer.this.personService.deletePerson(authority); synchronized (this) { this.personProcessedCount++; } } else { if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) { CustomChainingUserRegistrySynchronizer.logger.debug("Deleting group '" + CustomChainingUserRegistrySynchronizer.this.authorityService .getShortName(authority) + "'"); } CustomChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority); synchronized (this) { this.groupProcessedCount++; } } } } AuthorityDeleter authorityDeleter = new AuthorityDeleter(); authorityDeletionProcessor.process(authorityDeleter, splitTxns); groupProcessedCount += authorityDeleter.getGroupProcessedCount(); personProcessedCount += authorityDeleter.getPersonProcessedCount(); } // Remember we have visited this zone visitedZoneIds.add(zoneId); Object statusParams[] = { personProcessedCount, groupProcessedCount }; final String statusMessage = I18NUtil.getMessage("synchronization.summary.status", statusParams); if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { CustomChainingUserRegistrySynchronizer.logger .info("Finished synchronizing users and groups with user registry '" + zone + "'"); CustomChainingUserRegistrySynchronizer.logger.info(statusMessage); } notifySyncDirectoryEnd(zone, statusMessage); } // syncWithPlugin /** * Gets the persisted most recent update time for a label and zone. * * @param label * the label * @param zoneId * the zone id * @param splitTxns * split transactions, if true run this in a separate transaction * @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) CustomChainingUserRegistrySynchronizer.this.attributeService .getAttribute(CustomChainingUserRegistrySynchronizer.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 { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( Long.valueOf(lastModifiedMillis), CustomChainingUserRegistrySynchronizer.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). */ 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); } } @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! if (this.syncOnStartup) { AuthenticationUtil.runAs(new RunAsWork<Object>() { public Object doWork() throws Exception { try { synchronizeInternal(false, false, true); } catch (Exception e) { CustomChainingUserRegistrySynchronizer.logger .warn("Failed initial synchronize with user registries", e); } return null; } }, AuthenticationUtil.getSystemUserName()); } } /* * (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 { // Authentication AuthenticationUtil.setRunAsUser(AuthenticationUtil.getSystemUserName()); } public final void afterProcess() throws Throwable { // Clear authentication AuthenticationUtil.clearCurrentSecurityContext(); } } private void notifySyncStart(final Set<String> toSync) { final String serverId = sysAdminParams.getAlfrescoHost() + ":" + sysAdminParams.getAlfrescoPort(); this.applicationEventPublisher.publishEvent(new SynchronizeStartEvent(this)); this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( new Date().getTime(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.START_TIME_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(-1L, CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.END_TIME_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(serverId, CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.SERVER_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( SyncStatus.IN_PROGRESS.toString(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(null, CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute("", CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.SUMMARY_ATTRIBUTE); for (String zoneId : toSync) { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( SyncStatus.WAITING.toString(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE, zoneId); } return null; } }, false, true); } private void notifySyncEnd() { this.applicationEventPublisher.publishEvent(new SynchronizeEndEvent(this)); this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( SyncStatus.COMPLETE.toString(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( new Date().getTime(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.END_TIME_ATTRIBUTE); return null; } }, false, true); } private void notifySyncEnd(final Exception e) { this.applicationEventPublisher.publishEvent(new SynchronizeEndEvent(this, e)); this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(e.getMessage(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( SyncStatus.COMPLETE_ERROR.toString(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( new Date().getTime(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.END_TIME_ATTRIBUTE); return null; } }, false, true); } private void notifyZoneDeleted(final String zoneId) { // this.applicationEventPublisher.publishEvent(new SynchronizeDirectoryDeleteZoneEvent(this, zoneId, batchProcessNames)); this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute("", CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE, zoneId); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute("", CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.SUMMARY_ATTRIBUTE, zoneId); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(null, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE, CustomChainingUserRegistrySynchronizer.SUMMARY_ATTRIBUTE, zoneId); return null; } }, false, true); } private void notifySyncDirectoryStart(final String zoneId, final String[] batchProcessNames) { this.applicationEventPublisher .publishEvent(new SynchronizeDirectoryStartEvent(this, zoneId, batchProcessNames)); this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( SyncStatus.IN_PROGRESS.toString(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE, zoneId); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute("", CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.SUMMARY_ATTRIBUTE, zoneId); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(null, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE, CustomChainingUserRegistrySynchronizer.SUMMARY_ATTRIBUTE, zoneId); return null; } }, false, true); } private void notifySyncDirectoryEnd(final String zoneId, final String statusMessage) { this.applicationEventPublisher.publishEvent(new SynchronizeDirectoryEndEvent(this, zoneId)); this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( SyncStatus.COMPLETE.toString(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE, zoneId); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute("", CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE, zoneId); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(statusMessage, CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.SUMMARY_ATTRIBUTE, zoneId); return null; } }, false, true); } private void notifySyncDirectoryEnd(final String zoneId, final Exception e) { this.applicationEventPublisher.publishEvent(new SynchronizeDirectoryEndEvent(this, zoneId, e)); CustomChainingUserRegistrySynchronizer.logger.error("Synchronization aborted due to error", e); this.transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Void>() { @Override public Void execute() throws Throwable { CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute( SyncStatus.COMPLETE_ERROR.toString(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE, zoneId); CustomChainingUserRegistrySynchronizer.this.attributeService.setAttribute(e.getMessage(), CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE, zoneId); return null; } }, false, true); } @Override public Date getSyncStartTime() { Long start = (Long) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.START_TIME_ATTRIBUTE); Date lastUserUpdate = start.longValue() == -1 ? null : new Date(start.longValue()); return lastUserUpdate; } @Override public Date getSyncEndTime() { Long start = (Long) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.END_TIME_ATTRIBUTE); Date lastUserUpdate = start.longValue() == -1 ? null : new Date(start.longValue()); return lastUserUpdate; } @Override public String getLastErrorMessage() { String status = (String) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE); return status; } @Override public String getLastRunOnServer() { String status = (String) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.SERVER_ATTRIBUTE); return status; } @Override public String getSynchronizationStatus() { String status = (String) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE); return status; } @Override public String getSynchronizationStatus(String zoneId) { String status = (String) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.STATUS_ATTRIBUTE, zoneId); return status; } @Override public Date getSynchronizationLastUserUpdateTime(String id) { String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + id; long time = getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId, false); Date lastUserUpdate = time == -1 ? null : new Date(time); return lastUserUpdate; } @Override public Date getSynchronizationLastGroupUpdateTime(String id) { String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + id; long time = getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, false); Date lastGroupUpdate = time == -1 ? null : new Date(time); return lastGroupUpdate; } @Override public String getSynchronizationLastError(String zoneId) { String status = (String) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.LAST_ERROR_ATTRIBUTE, zoneId); return status; } @Override public String getSynchronizationSummary(String zoneId) { String status = (String) CustomChainingUserRegistrySynchronizer.this.attributeService.getAttribute( CustomChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, CustomChainingUserRegistrySynchronizer.SUMMARY_ATTRIBUTE, zoneId); return status; } public void setSysAdminParams(SysAdminParams sysAdminParams) { this.sysAdminParams = sysAdminParams; } public SysAdminParams getSysAdminParams() { return sysAdminParams; } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof AuthenticatorDeletedEvent) { AuthenticatorDeletedEvent deleteEvent = (AuthenticatorDeletedEvent) event; notifyZoneDeleted((String) deleteEvent.getSource()); } else { // pass to the superclass super.onApplicationEvent(event); } } }