From 174fd8fd77d95a0dd220adb800e943c3200a1e0f Mon Sep 17 00:00:00 2001 From: Dave Ward Date: Wed, 24 Mar 2010 13:49:03 +0000 Subject: [PATCH] Merged V3.2 to HEAD: 19472: ALF-725: Revert to using jTDS JDBC driver for SQL Server in 3.2 SP1, since the Microsoft driver doesn't work with the v3.2.r iBATIS stuff - All example/installer alfresco-global.properties updated - Wiki updated http://wiki.alfresco.com/wiki/Database_Configuration#MS-SQL_Databases - Logged doc bug ALF-2144 and release note bug ALF-2145 19501:Merged DEV/BELARUS/V3.2-2010_02_24 to V3.2 (with corrections) 19243: ALF-757: Cannot start up on JBoss 5.1 due to audit configuration error - Removed getPath() method because it is incompatible with JBoss and other app servers where resources can't be resolved to a file - Now use Spring ResourceLoader instead of creating FileInputStream - getLastModified() still returned where the resource resolves to a file; otherwise the server startup time 19503: (RECORD ONLY) ALF-2100: Merged HEAD to V3.2 19155: ALF-1995: Removed remaining direct dependencies on portlet API from Alfresco Explorer classes - Moved into AlfrescoFacesPortlet - portlet.jar was removed from alfresco.war for Liferay compatibility 19506: Merged PATCHES/V3.1.2 to V3.2 19218: (RECORD ONLY) Created hotfix branch off TAGS/ENTERPRISE/V3.1.2 19229: (RECORD ONLY) Merged V3.1 to V3.1.2 18577: Fix for ETHREEOH-4117, based on CHK-11154 19341: Merged DEV/BELARUS/V3.1-2010_02_05 to PATCHES/V3.1.2 (with corrections) 19156: ALF-1906: splitPersonCleanUpBootstrapBean is not able to remove duplicated users Also - improved detection of 'split' persons - added unit tests for person splitting and deleting - fixed duplicate person caching and sorting problems - prevented onUpdateProperties from firing needlessly in PersonServiceImpl and AuthorityDAOImpl when persons and authorities are created initially 19342: (RECORD ONLY) Incremented version number 19508: Merged PATCHES/V3.2.0 to V3.2 18762: (RECORD ONLY) Created hotfix branch off V3.2.0-ENTERPRISE-FINAL 18789: (RECORD ONLY) Merged BRANCHES/V3.2:r17905,18254,18319 to PATCHES/V3.2.0 r17905 | markr | 2010-01-06 16:55:12 +0000 (Wed, 06 Jan 2010) | 3 lines ETHREEOH-3809 - WCM - First test server deploy fails. added yet another transaction to read the previous snapshot transaction. added a new system test based upon the WCM services. The beginnings of testing against layered authored sandboxes. r18254 | janv | 2010-01-22 18:15:43 +0000 (Fri, 22 Jan 2010) | 1 line WCM/AVM - ETHREEOH-2057 (Submitting WCM Content through WF JSF Error - due to AVM Sync issue) r18319 | royw | 2010-01-27 12:18:27 +0000 (Wed, 27 Jan 2010) | 4 lines Merged BRANCHES/DEV/BELARUS/V3.2-2010_01_11 to V3.2 18273: ETHREEOH-3834: WCM: An extral .xml.html file is created when editing newly created content 18822: (RECORD ONLY) Merged DEV_TEMPORARY to PATCHES/V3.2.0 18478: SAP XForms errors - ACT 15969 18699: ETHREEOH-4171: HTTP 500 when filling in a WCM webform - ACT 15969 18842: (RECORD ONLY) Merged V3.2 to PATCHES/V3.2.0 18701: Merged DEV_TEMPORARY to V3.2 18693 : ETHREEOH-4182: ASR deployer fails to set the contentUrl of documents on the target system - Merged in fix related to closing output streams. - Increased coverage of unit test. 18854: (RECORD ONLY) Merged V3.2 to V3.2.0 18019: ETHREEOH-3770: LDAP sync now supports attribute range retrieval to get around limits imposed by Active Directory on multi-valued attributes - Meant that groups with more than 1000 members were getting truncated in Active Directory - Now switched on in ldap-ad and off in ldap subsystem - Also switched off result set paging in ldap subsystem by default for wider compatibility with non-AD systems 18272: Merged DEV/BELARUS/V3.2-2010_01_11 to V3.2 18257: ETHREEOH-4002: User/Group sync does not handle LDAP communication failures - Merged with corrections 18276: ETHREEOH-4002: Correction to previous checkin - modification dates are only persisted after successful processing of users and groups, so need to delete them on comms failure 18340: ETHREEOH-4069: LDAP sync cannot resolve DNs containing a slash character - Due to JNDI interpreting the slash character as a separator 18403: ETHREEOH-4008: LDAP sync should preserve case of group members - Was incorrectly extracting attributes from lower-cased DN 18846: ETHREEOH-4233: LDAP sync now synchronizes group display names - New ldap.synchronization.groupDisplayNameAttributeName property provides name of LDAP attribute 18877: (RECORD ONLY) Merged /alfresco/BRANCHES/V3.2:r18616 r18616 | markr | 2010-02-12 14:08:52 +0000 (Fri, 12 Feb 2010) | 1 line ETHREEOH-4181 - Access denied exception when deploying via avm deployment receiver 19319: ALF-2043: User ID case sensitivity issues with Sharepoint Connector and External Authentication Subsystem - DefaultRemoteUserMapper and AlfrescoUserGroupServiceHandler should use personService.getUserIdentifier() to 'normalize' a username according to case sensitivity settings - NtlmAuthenticationHandler should also leave the normalization to personService 19320: (RECORD ONLY) Incremented version label 19380: ALF-2043: Revisit user ID case sensitivity in DefaultRemoteUserMapper - Has to use public PersonService in case it is accessed outside of a transaction - Fixed regular expression matching - Added unit tests to try out all the remote user mapper options 19509: Merged PATCHES/V3.2.r to V3.2 18803: (RECORD ONLY) Created hotfix branch off V3.2.r-ENTERPRISE-FINAL 18833: (RECORD ONLY) Turn on Repo Doclib by default 19054: (RECORD ONLY) Merging V3.2 to PATCHES/V3.2.r 18787: MT: fix ETHREEOH-4125 - authority migration / batch processor (when upgrading groups from 3.1 to 3.2) 19358: (RECORD ONLY) Merged DEV/BELARUS/V3.2-2010_01_11 to PATCHES/V3.2.r 18699: ETHREEOH-4171: HTTP 500 when filling in a WCM webform 19447: (RECORD ONLY) Incremented version label 19518: ALF-757: Corrected audit config resource URL so that it resolves inside Tomcat as well as JUnit! 19525: ALF-708: Use BatchProcessor to process duplicate persons in small batches in SplitPersonCleanupBootstrapBean - Even tested in a unit test! git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@19536 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco-global.properties.sample | 10 +- config/alfresco/audit-services-context.xml | 2 +- .../repo/audit/AuditConfiguration.java | 4 +- .../repo/audit/AuditConfigurationImpl.java | 54 ++++++++--- .../audit/hibernate/HibernateAuditDAO.java | 4 +- .../AbstractChainedSubsystemTest.java | 43 +++++++++ .../security/authority/AuthorityDAOImpl.java | 23 ++++- .../security/person/PersonServiceImpl.java | 57 +++++++---- .../repo/security/person/PersonTest.java | 94 +++++++++++++++++++ .../SplitPersonCleanupBootstrapBean.java | 62 +++++++++--- 10 files changed, 298 insertions(+), 55 deletions(-) create mode 100644 source/java/org/alfresco/repo/management/subsystems/AbstractChainedSubsystemTest.java diff --git a/config/alfresco-global.properties.sample b/config/alfresco-global.properties.sample index 6f6be43b95..3645ace537 100644 --- a/config/alfresco-global.properties.sample +++ b/config/alfresco-global.properties.sample @@ -34,10 +34,14 @@ #db.url=jdbc:oracle:thin:@localhost:1521:alfresco # -# SQLServer connection (note you must enable TCP protocol on fixed port 1433) +# SQLServer connection +# Requires jTDS driver version 1.2.5 and SNAPSHOT isolation mode +# Enable TCP protocol on fixed port 1433 +# Prepare the database with: +# ALTER DATABASE alfresco SET ALLOW_SNAPSHOT_ISOLATION ON; # -#db.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver -#db.url=jdbc:sqlserver://localhost:1433;databaseName=alfresco +#db.driver=net.sourceforge.jtds.jdbc.Driver +#db.url=jdbc:jtds:sqlserver://localhost:1433/alfresco #db.txn.isolation=4096 # diff --git a/config/alfresco/audit-services-context.xml b/config/alfresco/audit-services-context.xml index ad13cddd1d..866c5b4fa3 100644 --- a/config/alfresco/audit-services-context.xml +++ b/config/alfresco/audit-services-context.xml @@ -49,7 +49,7 @@ - alfresco/auditConfig.xml + classpath:alfresco/auditConfig.xml diff --git a/source/java/org/alfresco/repo/audit/AuditConfiguration.java b/source/java/org/alfresco/repo/audit/AuditConfiguration.java index e290943008..63443e98c2 100644 --- a/source/java/org/alfresco/repo/audit/AuditConfiguration.java +++ b/source/java/org/alfresco/repo/audit/AuditConfiguration.java @@ -35,8 +35,8 @@ public interface AuditConfiguration InputStream getInputStream(); /** - * Return path of the XML + * Return last modified time of the XML * @return path */ - String getPath(); + long getLastModified(); } \ No newline at end of file diff --git a/source/java/org/alfresco/repo/audit/AuditConfigurationImpl.java b/source/java/org/alfresco/repo/audit/AuditConfigurationImpl.java index 2d72f3ceef..4f5ebee488 100644 --- a/source/java/org/alfresco/repo/audit/AuditConfigurationImpl.java +++ b/source/java/org/alfresco/repo/audit/AuditConfigurationImpl.java @@ -14,28 +14,34 @@ * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License + * FLOSS exception. You should have recieved a copy of the text describing * along with Alfresco. If not, see . */ package org.alfresco.repo.audit; -import java.io.FileInputStream; -import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; /** * A class to read the audit configuration from the class path * * @author Andy Hind */ -public class AuditConfigurationImpl implements AuditConfiguration +public class AuditConfigurationImpl implements AuditConfiguration, ResourceLoaderAware { private static Log logger = LogFactory.getLog(AuditConfigurationImpl.class); + private static long STARTUP_TIME = System.currentTimeMillis(); private String config; + private ResourceLoader resourceLoader; + /** * Default constructor * @@ -55,25 +61,45 @@ public class AuditConfigurationImpl implements AuditConfiguration this.config = config; } + private Resource getResource() + { + return this.resourceLoader.getResource(config); + } + public InputStream getInputStream() { - InputStream is = null; try { - is = new FileInputStream(getPath()); + return getResource().getInputStream(); } - catch (FileNotFoundException e) + catch (IOException e) { - if (logger.isWarnEnabled()) - { - logger.warn("File not found: " + getPath()); - } + logger.warn("Unable to resolve " + config + " as input stream", e); + return null; } - return is; } - - public String getPath() + + /* + * (non-Javadoc) + * @see + * org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) + */ + public void setResourceLoader(ResourceLoader resourceLoader) { - return this.getClass().getClassLoader().getResource(config).getPath(); + this.resourceLoader = resourceLoader; + } + + public long getLastModified() + { + try + { + return getResource().getFile().lastModified(); + } + catch (IOException e) + { + // Not all resources can be resolved to files on the filesystem. If this is the case, just return the time + // the server was last started + return STARTUP_TIME; + } } } diff --git a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java index 513f362bb4..aad9895a0a 100644 --- a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java +++ b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java @@ -19,7 +19,6 @@ package org.alfresco.repo.audit.hibernate; import java.io.BufferedInputStream; -import java.io.File; import java.io.InputStream; import java.io.Serializable; import java.net.URL; @@ -362,8 +361,7 @@ public class HibernateAuditDAO extends HibernateDaoSupport implements AuditDAO, { if (contentStore instanceof FileContentStore) { - File currFile = new File(auditInfo.getAuditConfiguration().getPath()); - long currTimestamp = currFile.lastModified(); + long currTimestamp = auditInfo.getAuditConfiguration().getLastModified(); long timestamp = ((FileContentStore)contentStore).getReader(auditConfig.getConfigURL()).getLastModified(); if (timestamp < currTimestamp) { diff --git a/source/java/org/alfresco/repo/management/subsystems/AbstractChainedSubsystemTest.java b/source/java/org/alfresco/repo/management/subsystems/AbstractChainedSubsystemTest.java new file mode 100644 index 0000000000..9b76d9fdd3 --- /dev/null +++ b/source/java/org/alfresco/repo/management/subsystems/AbstractChainedSubsystemTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.management.subsystems; + +import junit.framework.TestCase; + +/** + * Uses package level protection to allow us to sneak inside chained subsystems for test purposes. + * + * @author dward + */ +public abstract class AbstractChainedSubsystemTest extends TestCase +{ + + public ChildApplicationContextFactory getChildApplicationContextFactory(DefaultChildApplicationContextManager childApplicationContextManager, String id) + { + DefaultChildApplicationContextManager.ApplicationContextManagerState state = (DefaultChildApplicationContextManager.ApplicationContextManagerState)childApplicationContextManager.getState(true); + return state.getApplicationContextFactory(id); + } + +} diff --git a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java index 0ea7c2f99f..81e9c56299 100644 --- a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java +++ b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java @@ -888,23 +888,38 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor ContentModel.TYPE_AUTHORITY_CONTAINER); QName idProp = isAuthority ? ContentModel.PROP_AUTHORITY_NAME : ContentModel.PROP_USERNAME; String authBefore = DefaultTypeConverter.INSTANCE.convert(String.class, before.get(idProp)); + if (authBefore == null) + { + // Node has just been created; nothing to do + return; + } String authAfter = DefaultTypeConverter.INSTANCE.convert(String.class, after.get(idProp)); if (!EqualsHelper.nullSafeEquals(authBefore, authAfter)) { - if ((authBefore == null) || authBefore.equalsIgnoreCase(authAfter)) + if (authBefore.equalsIgnoreCase(authAfter)) { if (isAuthority) { // Fix any ACLs aclDao.updateAuthority(authBefore, authAfter); - // Fix primary association local name - // Unfortunately all the zone and group associations will still be bust! + // Fix primary association local name QName newAssocQName = QName.createQName("cm", authAfter, namespacePrefixResolver); ChildAssociationRef assoc = nodeService.getPrimaryParent(nodeRef); nodeService.moveNode(nodeRef, assoc.getParentRef(), assoc.getTypeQName(), newAssocQName); - // We can't be totally sure which tenant domain we need to target so clear the noderef cache + // Fix other non-case sensitive parent associations + QName oldAssocQName = QName.createQName("cm", authBefore, namespacePrefixResolver); + newAssocQName = QName.createQName("cm", authAfter, namespacePrefixResolver); + for (ChildAssociationRef parent : nodeService.getParentAssocs(nodeRef)) + { + if (!parent.isPrimary() && parent.getQName().equals(oldAssocQName)) + { + nodeService.removeChildAssociation(parent); + nodeService.addChild(parent.getParentRef(), parent.getChildRef(), parent.getTypeQName(), + newAssocQName); + } + } authorityLookupCache.clear(); // Cache is out of date diff --git a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java index 54c8e8b8db..a925b7367a 100644 --- a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java +++ b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java @@ -355,8 +355,6 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, searchUserName.toLowerCase()), false); allRefs = new LinkedHashSet(childRefs.size() * 2); - // add to cache - personCache.put(cacheKey, allRefs); for (ChildAssociationRef childRef : childRefs) { @@ -382,6 +380,9 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per else if (refs.size() == 1) { returnRef = refs.get(0); + + // Don't bother caching unless we get a result that doesn't need duplicate processing + personCache.put(cacheKey, allRefs); } return returnRef; } @@ -411,6 +412,7 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per } private static final String KEY_POST_TXN_DUPLICATES = "PersonServiceImpl.KEY_POST_TXN_DUPLICATES"; + private static final String KEY_ALLOW_UID_UPDATE = "PersonServiceImpl.KEY_ALLOW_UID_UPDATE"; /** * Get the txn-bound usernames that need cleaning up @@ -455,6 +457,8 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per if (duplicateMode.equalsIgnoreCase(SPLIT)) { + // Allow UIDs to be updated in this transaction + AlfrescoTransactionSupport.bindResource(KEY_ALLOW_UID_UPDATE, Boolean.TRUE); split(postTxnDuplicates); s_logger.info("Split duplicate person objects"); } @@ -497,13 +501,15 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per private NodeRef findBest(List refs) { + // Given that we might not have audit attributes, use the assumption that the node ID increases to sort the + // nodes if (lastIsBest) { - Collections.sort(refs, new CreationDateComparator(nodeService, false)); + Collections.sort(refs, new NodeIdComparator(nodeService, false)); } else { - Collections.sort(refs, new CreationDateComparator(nodeService, true)); + Collections.sort(refs, new NodeIdComparator(nodeService, true)); } NodeRef fallBack = null; @@ -775,6 +781,7 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per { nodeService.deleteNode(personNodeRef); } + personCache.remove(userName.toLowerCase()); } public Set getAllPeople() @@ -845,6 +852,7 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per { NodeRef personRef = childAssocRef.getChildRef(); String username = (String) this.nodeService.getProperty(personRef, ContentModel.PROP_USERNAME); + personCache.remove(username.toLowerCase()); permissionsManager.setPermissions(personRef, username, username); // Make sure there is an authority entry - with a DB constraint for uniqueness @@ -936,13 +944,13 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per return null; } - public static class CreationDateComparator implements Comparator + public static class NodeIdComparator implements Comparator { private NodeService nodeService; boolean ascending; - CreationDateComparator(NodeService nodeService, boolean ascending) + NodeIdComparator(NodeService nodeService, boolean ascending) { this.nodeService = nodeService; this.ascending = ascending; @@ -950,14 +958,14 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per public int compare(NodeRef first, NodeRef second) { - Date firstDate = DefaultTypeConverter.INSTANCE.convert(Date.class, nodeService.getProperty(first, ContentModel.PROP_CREATED)); - Date secondDate = DefaultTypeConverter.INSTANCE.convert(Date.class, nodeService.getProperty(second, ContentModel.PROP_CREATED)); + Long firstId = DefaultTypeConverter.INSTANCE.convert(Long.class, nodeService.getProperty(first, ContentModel.PROP_NODE_DBID)); + Long secondId = DefaultTypeConverter.INSTANCE.convert(Long.class, nodeService.getProperty(second, ContentModel.PROP_NODE_DBID)); - if (firstDate != null) + if (firstId != null) { - if (secondDate != null) + if (secondId != null) { - return firstDate.compareTo(secondDate) * (ascending ? 1 : -1); + return firstId.compareTo(secondId) * (ascending ? 1 : -1); } else { @@ -966,7 +974,7 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per } else { - if (secondDate != null) + if (secondId != null) { return ascending ? 1 : -1; } @@ -996,22 +1004,39 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per public void onUpdateProperties(NodeRef nodeRef, Map before, Map after) { String uidBefore = DefaultTypeConverter.INSTANCE.convert(String.class, before.get(ContentModel.PROP_USERNAME)); + if (uidBefore == null) + { + // Node has just been created; nothing to do + return; + } String uidAfter = DefaultTypeConverter.INSTANCE.convert(String.class, after.get(ContentModel.PROP_USERNAME)); if (!EqualsHelper.nullSafeEquals(uidBefore, uidAfter)) { - if ((uidBefore == null) || uidBefore.equalsIgnoreCase(uidAfter)) + // Only allow UID update if we are in the special split processing txn or we are just changing case + if (AlfrescoTransactionSupport.getResource(KEY_ALLOW_UID_UPDATE) != null || uidBefore.equalsIgnoreCase(uidAfter)) { // Fix any ACLs aclDao.updateAuthority(uidBefore, uidAfter); + // Fix primary association local name QName newAssocQName = QName.createQName("cm", uidAfter.toLowerCase(), namespacePrefixResolver); ChildAssociationRef assoc = nodeService.getPrimaryParent(nodeRef); nodeService.moveNode(nodeRef, assoc.getParentRef(), assoc.getTypeQName(), newAssocQName); - // Fix cache - if (uidBefore != null) + + // Fix other non-case sensitive parent associations + QName oldAssocQName = QName.createQName("cm", uidBefore, namespacePrefixResolver); + newAssocQName = QName.createQName("cm", uidAfter, namespacePrefixResolver); + for (ChildAssociationRef parent : nodeService.getParentAssocs(nodeRef)) { - personCache.remove(uidBefore.toLowerCase()); + if (!parent.isPrimary() && parent.getQName().equals(oldAssocQName)) + { + nodeService.removeChildAssociation(parent); + nodeService.addChild(parent.getParentRef(), parent.getChildRef(), parent.getTypeQName(), newAssocQName); + } } + + // Fix cache + personCache.remove(uidBefore.toLowerCase()); } else { diff --git a/source/java/org/alfresco/repo/security/person/PersonTest.java b/source/java/org/alfresco/repo/security/person/PersonTest.java index 6ed238868a..4641235a60 100644 --- a/source/java/org/alfresco/repo/security/person/PersonTest.java +++ b/source/java/org/alfresco/repo/security/person/PersonTest.java @@ -21,15 +21,19 @@ package org.alfresco.repo.security.person; import java.io.Serializable; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import junit.framework.Assert; + import org.alfresco.model.ContentModel; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; @@ -726,4 +730,94 @@ public class PersonTest extends BaseSpringTest } }, true, true); } + + public void testSplitDuplicates() + { + testProcessDuplicates(true); + + // Test out the SplitPersonCleanupBootstrapBean for removal of the duplicates + SplitPersonCleanupBootstrapBean splitPersonBean = new SplitPersonCleanupBootstrapBean(); + splitPersonBean.setNodeService(nodeService); + splitPersonBean.setPersonService(personService); + splitPersonBean.setTransactionService(transactionService); + Assert.assertEquals(9, splitPersonBean.removePeopleWithGUIDBasedIds()); + + } + + public void testDeleteDuplicates() + { + testProcessDuplicates(false); + } + + private void testProcessDuplicates(final boolean split) + { + // Kill the annoying Spring-managed txn + super.setComplete(); + super.endTransaction(); + + // Set the duplicate processing mode + ((PersonServiceImpl) personService).setDuplicateMode(split ? "SPLIT" : "DELETE"); + + final String duplicateUserName = GUID.generate(); + final NodeRef[] duplicates = transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + + public NodeRef[] execute() throws Throwable + { + NodeRef[] duplicates = new NodeRef[10]; + + // Generate a first person node + Map properties = createDefaultProperties(duplicateUserName, "firstName", "lastName", "email@orgId", "orgId", null); + duplicates[0] = personService.createPerson(properties); + ChildAssociationRef container = nodeService.getPrimaryParent(duplicates[0]); + List parents = nodeService.getParentAssocs(duplicates[0]); + + // Generate some duplicates + for (int i = 1; i < duplicates.length; i++) + { + // Create the node with the same parent assocs + duplicates[i] = nodeService.createNode(container.getParentRef(), container.getTypeQName(), + container.getQName(), ContentModel.TYPE_PERSON, properties).getChildRef(); + for (ChildAssociationRef parent : parents) + { + if (!parent.isPrimary()) + { + nodeService.addChild(parent.getParentRef(), duplicates[i], parent.getTypeQName(), + parent.getQName()); + } + } + } + // With the default settings, the last created node should be the one that wins + assertEquals(duplicates[duplicates.length - 1], personService.getPerson(duplicateUserName)); + return duplicates; + } + }, false, true); + + // Check the duplicates were processed appropriately in the previous transaction + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + for (int i = 0; i < duplicates.length - 1; i++) + { + if (split) + { + String newUserName = (String) nodeService + .getProperty(duplicates[i], ContentModel.PROP_USERNAME); + assertNotSame(duplicateUserName, newUserName); + } + else + { + assertFalse(nodeService.exists(duplicates[i])); + } + } + + // Get rid of the non-split person + assertTrue(personService.personExists(duplicateUserName)); + personService.deletePerson(duplicateUserName); + return null; + } + }, false, true); + } } diff --git a/source/java/org/alfresco/repo/security/person/SplitPersonCleanupBootstrapBean.java b/source/java/org/alfresco/repo/security/person/SplitPersonCleanupBootstrapBean.java index 183db7c1b2..2f7ab5f6e8 100644 --- a/source/java/org/alfresco/repo/security/person/SplitPersonCleanupBootstrapBean.java +++ b/source/java/org/alfresco/repo/security/person/SplitPersonCleanupBootstrapBean.java @@ -19,8 +19,11 @@ package org.alfresco.repo.security.person; import java.util.Set; +import java.util.TreeSet; import org.alfresco.model.ContentModel; +import org.alfresco.repo.batch.BatchProcessor; +import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -78,14 +81,14 @@ public class SplitPersonCleanupBootstrapBean extends AbstractLifecycleBean * * @return */ - private int removePeopleWithGUIDBasedIds() + protected int removePeopleWithGUIDBasedIds() { - Integer count = transactionService.getRetryingTransactionHelper().doInTransaction( - new RetryingTransactionCallback() + Set uidsToRemove = transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback>() { - public Integer execute() throws Exception + public Set execute() throws Exception { - int count = 0; + Set uidsToRemove = new TreeSet(); // A GUID should be 36 chars Set people = personService.getAllPeople(); @@ -95,21 +98,56 @@ public class SplitPersonCleanupBootstrapBean extends AbstractLifecycleBean person, ContentModel.PROP_USERNAME)); if (isUIDWithGUID(uid)) { - // Delete via the person service to get the correct tidy up - personService.deletePerson(uid); + uidsToRemove.add(uid); if (log.isDebugEnabled()) { - log.debug("... removed person with uid " + uid); + log.debug("... will remove person with uid " + uid); } - log.info("... removed person with uid " + uid); - count++; } } - return count; + return uidsToRemove; } }); - return count.intValue(); + + if (uidsToRemove.isEmpty()) + { + return 0; + } + + // Process the duplicate persons in small batches + BatchProcessor batchProcessor = new BatchProcessor("Split Person Removal", transactionService + .getRetryingTransactionHelper(), uidsToRemove, 2, 10, getApplicationContext(), log, 100); + batchProcessor.process(new BatchProcessor.BatchProcessWorker() + { + + public String getIdentifier(String entry) + { + return entry; + } + + public void beforeProcess() throws Throwable + { + // Authenticate as system + String systemUsername = AuthenticationUtil.getSystemUserName(); + AuthenticationUtil.setFullyAuthenticatedUser(systemUsername); + } + + public void afterProcess() throws Throwable + { + } + + public void process(String entry) throws Throwable + { + // Delete via the person service to get the correct tidy up + personService.deletePerson(entry); + if (log.isDebugEnabled()) + { + log.debug("... removed person with uid " + entry); + } + } + }, true); + return uidsToRemove.size(); }