diff --git a/config/alfresco/authentication-services-context.xml b/config/alfresco/authentication-services-context.xml index 4aec5ecd87..6184cae3e4 100644 --- a/config/alfresco/authentication-services-context.xml +++ b/config/alfresco/authentication-services-context.xml @@ -375,13 +375,22 @@ - ${home.folder.creation.eager} + + + + + + + - - + + + @@ -393,8 +402,47 @@ - - + + + + + + + + + + + + + + + + + + + + + ${spaces.user_homes.regex.key} + + + ${spaces.user_homes.regex.pattern} + + + ${spaces.user_homes.regex.group_order} + + + + + + /${spaces.company_home.childname} @@ -402,7 +450,8 @@ - + @@ -417,12 +466,8 @@ - - - - - - + + /${spaces.company_home.childname}/${spaces.guest_home.childname} @@ -436,9 +481,12 @@ - + - + @@ -460,7 +508,8 @@ - + @@ -474,42 +523,34 @@ - - - - - - + + /${spaces.company_home.childname} ${spaces.store} - - - - - - - - + + /${spaces.company_home.childname}/${spaces.user_homes.childname} ${spaces.store} - - + + + + + /${spaces.company_home.childname}/${spaces.user_homes.childname} - - + + ${spaces.store} - diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml index 9be01982b6..4ffb89842b 100644 --- a/config/alfresco/bootstrap-context.xml +++ b/config/alfresco/bootstrap-context.xml @@ -686,6 +686,18 @@ ${system.usages.enabled} + + + + + + + + + + + + diff --git a/config/alfresco/model/transferModel.xml b/config/alfresco/model/transferModel.xml index 8647b5ed03..df5d8b35a2 100644 --- a/config/alfresco/model/transferModel.xml +++ b/config/alfresco/model/transferModel.xml @@ -175,7 +175,7 @@ Can this resource be enabled/disabled. - Is this enabled. + Enabled d:boolean true diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 02ce8c2d86..38227e8776 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -384,6 +384,9 @@ spaces.wcm.childname=app:wcm spaces.wcm_content_forms.childname=app:wcm_forms spaces.content_forms.childname=app:forms spaces.user_homes.childname=app:user_homes +spaces.user_homes.regex.key=userName +spaces.user_homes.regex.pattern= +spaces.user_homes.regex.group_order= spaces.sites.childname=st:sites spaces.templates.email.invite.childname=cm:invite spaces.templates.email.activities.childname=cm:activities @@ -399,6 +402,7 @@ spaces.extension_webscripts.childname=cm:extensionwebscripts spaces.models.childname=app:models spaces.workflow.definitions.childname=app:workflow_defs + # ADM VersionStore Configuration version.store.deprecated.lightWeightVersionStore=workspace://lightWeightVersionStore version.store.version2Store=workspace://version2Store diff --git a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties index 05df62c1aa..b7af1b1393 100644 --- a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties @@ -99,7 +99,7 @@ ldap.synchronization.userEmailAttributeName=mail ldap.synchronization.userOrganizationalIdAttributeName=company # The default home folder provider to use for people created via LDAP import -ldap.synchronization.defaultHomeFolderProvider=userHomesHomeFolderProvider +ldap.synchronization.defaultHomeFolderProvider=largeHomeFolderProvider # The attribute on LDAP group objects to map to the authority name property in Alfresco ldap.synchronization.groupIdAttributeName=cn diff --git a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties index 2caf6a30cf..882eb80e9a 100644 --- a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties @@ -105,7 +105,7 @@ ldap.synchronization.userEmailAttributeName=mail ldap.synchronization.userOrganizationalIdAttributeName=o # The default home folder provider to use for people created via LDAP import -ldap.synchronization.defaultHomeFolderProvider=userHomesHomeFolderProvider +ldap.synchronization.defaultHomeFolderProvider=largeHomeFolderProvider # The attribute on LDAP group objects to map to the authority name property in Alfresco ldap.synchronization.groupIdAttributeName=cn diff --git a/source/java/org/alfresco/repo/admin/patch/impl/FixUserQNamesPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/FixUserQNamesPatch.java index 0578708d18..fb229f03e5 100644 --- a/source/java/org/alfresco/repo/admin/patch/impl/FixUserQNamesPatch.java +++ b/source/java/org/alfresco/repo/admin/patch/impl/FixUserQNamesPatch.java @@ -33,6 +33,7 @@ import org.alfresco.repo.batch.BatchProcessor; import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker; import org.alfresco.repo.domain.qname.QNameDAO; import org.alfresco.repo.importer.ImporterBootstrap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.rule.RuleService; @@ -110,18 +111,22 @@ public class FixUserQNamesPatch extends AbstractPatch implements ApplicationEven 20, this.applicationEventPublisher, logger, 1000); + final String runAsUser = AuthenticationUtil.getRunAsUser(); + int updated = batchProcessor.process(new BatchProcessWorker() { public void beforeProcess() throws Throwable { // Disable rules ruleService.disableRules(); + AuthenticationUtil.setRunAsUser(runAsUser); } public void afterProcess() throws Throwable { // Enable rules ruleService.enableRules(); + AuthenticationUtil.clearCurrentSecurityContext(); } public String getIdentifier(ChildAssociationRef entry) diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java index 2429a3ad4f..5355be1caa 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java @@ -480,7 +480,14 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab { logger.debug("[getMessageInternal] " + this); } - AbstractMimeMessage mes = (AbstractMimeMessage) messages.get(uid).getMimeMessage(); + SimpleStoredMessage storedMessage = messages.get(uid); + if (storedMessage == null) + { + messagesCache.remove(uid); + msnCache.remove(uid); + return null; + } + AbstractMimeMessage mes = (AbstractMimeMessage) storedMessage.getMimeMessage(); FileInfo mesInfo = mes.getMessageInfo(); Date modified = (Date) serviceRegistry.getNodeService().getProperty(mesInfo.getNodeRef(), ContentModel.PROP_MODIFIED); @@ -1181,7 +1188,10 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab if (nodeService.hasAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER)) { modifDate = ((Long) nodeService.getProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY)); - return (modifDate - YEAR_2005) / 1000; + // we need tens part of the second at least, because + // we should avoid issues when several changes were completed within a second. + // so, divide by 100 instead of 1000. see ImapServiceImplCacheTest#testRepoBehaviourWithFoldersCache() + return (modifDate - YEAR_2005) / 100; } } return new Long(0); diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index f9ab9508aa..ab9c494b46 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -100,6 +100,8 @@ import org.springframework.extensions.surf.util.AbstractLifecycleBean; import org.springframework.extensions.surf.util.I18NUtil; import org.springframework.util.FileCopyUtils; +import com.icegreen.greenmail.imap.ImapConstants; + /** * @author Dmitry Vaserin * @author Arseny Kovalchuk @@ -216,6 +218,11 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol this.foldersCache = foldersCache; } + public SimpleCache getFoldersCache() + { + return foldersCache; + } + public FileFolderService getFileFolderService() { return fileFolderService; @@ -792,11 +799,13 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol /** * Check whether resultFolder is stale */ + /* if(resultFolder.isStale()) { logger.debug("folder is stale"); resultFolder = null; } + */ } if (resultFolder == null) @@ -841,7 +850,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol NodeRef targetNode = fileFolderService.searchSimple(nodeRef, folderNames[i]); - if (targetNode == null) + if (i == 0 && targetNode == null) { resultFolder = new AlfrescoImapFolder(user.getQualifiedMailboxName(), serviceRegistry); if (logger.isDebugEnabled()) @@ -1294,6 +1303,20 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol logger.debug("listMailboxes returning size:" + result.size()); + StringBuilder prefix = new StringBuilder(128); + prefix.append(ImapConstants.USER_NAMESPACE) + .append(AlfrescoImapConst.HIERARCHY_DELIMITER) + .append(user.getQualifiedMailboxName()) + .append(AlfrescoImapConst.HIERARCHY_DELIMITER); + int prefixLength = prefix.length(); + + for(AlfrescoImapFolder folder : result) + { + String cacheKey = folder.getFullName().substring(prefixLength + 1, folder.getFullName().length() - 1); + logger.debug("[listMailboxes] Adding the cache entry : " + cacheKey); + foldersCache.put(cacheKey, folder); + } + return result; } @@ -2150,6 +2173,38 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol } } + + public void beforeDeleteNode(NodeRef nodeRef) + { + + NodeRef parentNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + if (ContentModel.TYPE_FOLDER.equals(nodeService.getType(nodeRef))) + { + // If a node is a folder, we need to remove its' cache with its' children cache as well + invalidateFolderCacheByNodeRef(nodeRef, true); + } + else if (ContentModel.TYPE_CONTENT.equals(nodeService.getType(nodeRef))) + { + // If a node is a content, it is simpler to remove its' parent cache + // to avoid of deal with folder messages cache + invalidateFolderCacheByNodeRef(parentNodeRef, false); + } + else + { + return; + } + // Add a listener once, when a lots of messsages were created/moved into the folder + if (AlfrescoTransactionSupport.getResource(UIDVALIDITY_LISTENER_ALREADY_BOUND) == null) + { + AlfrescoTransactionSupport.bindListener(new UidValidityTransactionListener(parentNodeRef, nodeService)); + AlfrescoTransactionSupport.bindResource(UIDVALIDITY_LISTENER_ALREADY_BOUND, true); + } + if (logger.isDebugEnabled()) + { + logger.debug("[beforeDeleteNode] Node " + nodeRef + " going to be removed. UIDVALIDITY will be changed for " + parentNodeRef); + } + } + private class UidValidityTransactionListener extends TransactionListenerAdapter { @@ -2202,7 +2257,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol } if (logger.isDebugEnabled()) { - logger.debug("UIDVALIDITY was modified"); + logger.debug("UIDVALIDITY was modified for " + folderNodeRef); } return modifDate; } @@ -2212,6 +2267,69 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol } + + private void invalidateFolderCacheByNodeRef(NodeRef folderNodeRef, boolean invalidateChildren) + { + if (logger.isDebugEnabled()) + { + if (invalidateChildren) + { + logger.debug("[invalidateFolderCacheByNodeRef] Invalidate cache entries for " + folderNodeRef); + } + else + { + logger.debug("[invalidateFolderCacheByNodeRef] Invalidate cache entries for " + folderNodeRef + " and children"); + } + } + + if (invalidateChildren) + { + SimpleCache foldersCache = getFoldersCache(); + List toRemove = new LinkedList(); + for(Serializable name : foldersCache.getKeys()) + { + AlfrescoImapFolder folder = (AlfrescoImapFolder) foldersCache.get(name); + if (folderNodeRef.equals(folder.getFolderInfo().getNodeRef())) + { + toRemove.add(name); + break; + } + } + if (toRemove.size() > 0) + { + String rootName = (String) toRemove.get(0); + for(Serializable name : foldersCache.getKeys()) + { + if (((String) name).startsWith(rootName)) + { + toRemove.add(name); + } + } + if (logger.isDebugEnabled()) + { + logger.debug("Caches to invalidate: " + toRemove.toString()); + } + for(Serializable name : toRemove) + { + foldersCache.remove(name); + } + } + } + else + { + SimpleCache foldersCache = getFoldersCache(); + for(Serializable name : foldersCache.getKeys()) + { + AlfrescoImapFolder folder = (AlfrescoImapFolder) foldersCache.get(name); + if (folderNodeRef.equals(folder.getFolderInfo().getNodeRef())) + { + foldersCache.remove(name); + break; + } + } + } + } + /** * Return true if provided nodeRef is in Sites/.../documentlibrary */ diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImplCacheTest.java b/source/java/org/alfresco/repo/imap/ImapServiceImplCacheTest.java new file mode 100644 index 0000000000..c82b21c60b --- /dev/null +++ b/source/java/org/alfresco/repo/imap/ImapServiceImplCacheTest.java @@ -0,0 +1,224 @@ +package org.alfresco.repo.imap; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory; +import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.config.RepositoryFolderConfigBean; +import org.springframework.context.ApplicationContext; + +import com.icegreen.greenmail.store.SimpleStoredMessage; + +/** + * Unit test for cache implementation in the ImapServiceImpl. Based on ImapServiceImplTest, but + * we need this separate test because we need to get transactions to commit to trigger behaviours in ImapServiceImpl. + * + * @author ArsenyKo + */ +public class ImapServiceImplCacheTest extends TestCase +{ + private static final String USER_NAME = "admin"; + private static final String USER_PASSWORD = "admin"; + + private static final String TEST_IMAP_FOLDER_NAME = "aaa"; + + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private NodeService nodeService; + private MutableAuthenticationService authenticationService; + private SearchService searchService; + private NamespaceService namespaceService; + private FileFolderService fileFolderService; + private ContentService contentService; + + private ImapService imapService; + + private NodeRef testImapFolderNodeRef; + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); + nodeService = serviceRegistry.getNodeService(); + authenticationService = serviceRegistry.getAuthenticationService(); + imapService = serviceRegistry.getImapService(); + searchService = serviceRegistry.getSearchService(); + namespaceService = serviceRegistry.getNamespaceService(); + fileFolderService = serviceRegistry.getFileFolderService(); + contentService = serviceRegistry.getContentService(); + + authenticationService.authenticate(USER_NAME, USER_PASSWORD.toCharArray()); + + String storePath = "workspace://SpacesStore"; + String companyHomePathInStore = "/app:company_home"; + + StoreRef storeRef = new StoreRef(storePath); + + NodeRef storeRootNodeRef = nodeService.getRootNode(storeRef); + + List nodeRefs = searchService.selectNodes(storeRootNodeRef, companyHomePathInStore, null, namespaceService, false); + NodeRef companyHomeNodeRef = nodeRefs.get(0); + + ChildApplicationContextFactory imap = (ChildApplicationContextFactory) ctx.getBean("imap"); + ApplicationContext imapCtx = imap.getApplicationContext(); + ImapServiceImpl imapServiceImpl = (ImapServiceImpl)imapCtx.getBean("imapService"); + + // Creating IMAP test folder for IMAP root + LinkedList folders = new LinkedList(); + folders.add(TEST_IMAP_FOLDER_NAME); + FileFolderServiceImpl.makeFolders(fileFolderService, companyHomeNodeRef, folders, ContentModel.TYPE_FOLDER); + + // Setting IMAP root + RepositoryFolderConfigBean imapHome = new RepositoryFolderConfigBean(); + imapHome.setStore(storePath); + imapHome.setRootPath(companyHomePathInStore); + imapHome.setFolderPath(TEST_IMAP_FOLDER_NAME); + imapServiceImpl.setImapHome(imapHome); + + // Starting IMAP + imapServiceImpl.startup(); + + nodeRefs = searchService.selectNodes(storeRootNodeRef, + companyHomePathInStore + "/" + NamespaceService.CONTENT_MODEL_PREFIX + ":" + TEST_IMAP_FOLDER_NAME, + null, + namespaceService, + false); + testImapFolderNodeRef = nodeRefs.get(0); + + } + + public void tearDown() throws Exception + { + fileFolderService.delete(testImapFolderNodeRef); + } + + + public void testRepoBehaviourWithFoldersCache() throws Exception + { + AlfrescoImapUser localUser = new AlfrescoImapUser(USER_NAME + "@alfresco.com", USER_NAME, USER_PASSWORD); + String folderName = "ALF9361"; + String mailbox = "Alfresco IMAP" + AlfrescoImapConst.HIERARCHY_DELIMITER + + TEST_IMAP_FOLDER_NAME + AlfrescoImapConst.HIERARCHY_DELIMITER + + folderName; + int contentItemsCount = 3; + // Create a tree like ALF9361/ALF9361_0/sub_0 + // Mailbox path with default mount point should be like 'Alfresco IMAP/aaa/ALF9361/ALF9361_0/sub_0 + FileInfo localRootFolder = fileFolderService.create(testImapFolderNodeRef, folderName, ContentModel.TYPE_FOLDER); + List subFolders = new ArrayList(10); + for(int i = 0; i < 3; i++) + { + String childMailbox = folderName + "_" + i; + FileInfo subFolder = fileFolderService.create(localRootFolder.getNodeRef(), childMailbox, ContentModel.TYPE_FOLDER); + for(int j = 0; j < 3; j++) + { + String subChildMailbox = "sub_" + j; + fileFolderService.create(subFolder.getNodeRef(), subChildMailbox, ContentModel.TYPE_FOLDER); + } + subFolders.add(subFolder); + } + // Create content within 'Alfresco IMAP/aaa/ALF9361' + createTestContent(localRootFolder, contentItemsCount); + // Load the cache + imapService.listMailboxes(localUser, "*"); + imapService.listSubscribedMailboxes(localUser, "*"); + // Get the folder to examine + AlfrescoImapFolder folder = imapService.getFolder(localUser, mailbox); + // Check the folder exist via IMAP + assertNotNull("Folder wasn't successfully gotten from IMAP", folder); + assertEquals(contentItemsCount, folder.getMessageCount()); + // Check UIDVALIDITY + long uidValidityBefore = folder.getUidValidity(); + // Delete first childMailbox 'ALF9361/ALF9361_0' + //System.out.println(" --------------------- DELETE FOLDER --------------------"); + //System.out.println(" Parent " + localRootFolder.getNodeRef()); + fileFolderService.delete(subFolders.get(0).getNodeRef()); + // Get the folder once more and check it was changed since child was removed + folder = imapService.getFolder(localUser, mailbox); + // Content count should be the same since we havn't deleted a content yet + assertEquals(contentItemsCount, folder.getMessageCount()); + long uidValidity = folder.getUidValidity(); + assertTrue("UIDVALIDITY wasn't incremented", (uidValidity - uidValidityBefore) > 0); + uidValidityBefore = uidValidity; + // Try to get deleted child + try + { + String subFolderName = mailbox + AlfrescoImapConst.HIERARCHY_DELIMITER + folderName + "_0"; + folder = imapService.getFolder(localUser, subFolderName); + fail("The folder still in the cache"); + } + catch (RuntimeException e) + { + // expected + } + // Try to get deleted sub child. If the cache wasn't invalidated we will get it + // But it should be connected to AlfrescoImapFolder.isStale() method. + // ArsenyKo: I think we should avoid repo API invocation like isStale... + try + { + String subSubFolderName = mailbox + AlfrescoImapConst.HIERARCHY_DELIMITER + mailbox + "_0" + AlfrescoImapConst.HIERARCHY_DELIMITER + "sub_0"; + folder = imapService.getFolder(localUser, subSubFolderName); + fail("The folder still in the cache"); + } + catch (RuntimeException e) + { + // expected + } + // Do manipulations with a content in the folder to check the cache behaviour + folder = imapService.getFolder(localUser, mailbox); + SimpleStoredMessage message = folder.getMessages().get(0); + AbstractMimeMessage alfrescoMessage = (AbstractMimeMessage) message.getMimeMessage(); + long uid = message.getUid(); + //System.out.println(" --------------------- DELETE FILE --------------------"); + //System.out.println(" Parent " + folder.getFolderInfo().getNodeRef()); + // Delete a content + fileFolderService.delete(alfrescoMessage.getMessageInfo().getNodeRef()); + // Get a folder once again. We expect that the folder would be retrieved from the repo, + // since its' cache should be invalidated + folder = imapService.getFolder(localUser, mailbox); + // Get UIDVALIDITY. It should be changed, since we removed a message form the mailbox. + uidValidity = folder.getUidValidity(); + assertTrue("UIDVALIDITY wasn't incremented", (uidValidity - uidValidityBefore) > 0); + // Additional check whether messages cache is valid. Messages cache should be recreated + //with the new inctance of AlfrescoImapMessage + assertTrue("Messages cache is stale", contentItemsCount > folder.getMessageCount()); + long[] uids = folder.getMessageUids(); + Arrays.sort(uids); + assertFalse("Messages msn cache is stale", Arrays.binarySearch(uids, uid) > 0); + assertNull("Message is still in the messages cache", folder.getMessage(uid)); + //System.out.println(" --------------------- THE END --------------------"); + fileFolderService.delete(localRootFolder.getNodeRef()); + + } + + private List createTestContent(FileInfo parent, int count) + { + List result = new ArrayList(count); + for(int i = 0; i < count; i++) + { + FileInfo contentItem = fileFolderService.create(parent.getNodeRef(), "content_" + i, ContentModel.TYPE_CONTENT, ContentModel.ASSOC_CONTAINS); + ContentWriter contentWriter = contentService.getWriter(contentItem.getNodeRef(), ContentModel.PROP_CONTENT, false); + contentWriter.setEncoding("UTF-8"); + contentWriter.putContent("TEST" + i); + } + return result; + } + + +} diff --git a/source/java/org/alfresco/repo/processor/ScriptServiceImpl.java b/source/java/org/alfresco/repo/processor/ScriptServiceImpl.java index 835519742b..dbf2fd2493 100644 --- a/source/java/org/alfresco/repo/processor/ScriptServiceImpl.java +++ b/source/java/org/alfresco/repo/processor/ScriptServiceImpl.java @@ -441,8 +441,14 @@ public class ScriptServiceImpl implements ScriptService // add the well known node wrapper objects model.put("companyhome", companyHome); - model.put("userhome", userHome); - model.put("person", person); + if (userHome!= null) + { + model.put("userhome", userHome); + } + if (person != null) + { + model.put("person", person); + } if (script != null) { model.put("script", script); diff --git a/source/java/org/alfresco/repo/security/SecurityTestSuite.java b/source/java/org/alfresco/repo/security/SecurityTestSuite.java index 9e806a7ff2..55474eee8b 100644 --- a/source/java/org/alfresco/repo/security/SecurityTestSuite.java +++ b/source/java/org/alfresco/repo/security/SecurityTestSuite.java @@ -18,9 +18,11 @@ */ package org.alfresco.repo.security; +import junit.framework.JUnit4TestAdapter; import junit.framework.Test; import junit.framework.TestSuite; +import org.alfresco.repo.audit.access.AccessAuditorTest; import org.alfresco.repo.ownable.impl.OwnableServiceTest; import org.alfresco.repo.security.authentication.AuthenticationBootstrapTest; import org.alfresco.repo.security.authentication.AuthenticationTest; @@ -37,6 +39,7 @@ import org.alfresco.repo.security.permissions.impl.acegi.ACLEntryAfterInvocation import org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterTest; import org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSetTest; import org.alfresco.repo.security.permissions.impl.model.PermissionModelTest; +import org.alfresco.repo.security.person.HomeFolderProviderSynchronizerTest; import org.alfresco.repo.security.person.PersonTest; /** @@ -74,6 +77,8 @@ public class SecurityTestSuite extends TestSuite suite.addTestSuite(OwnableServiceTest.class); suite.addTestSuite(ReadPermissionTest.class); + suite.addTest(new JUnit4TestAdapter(HomeFolderProviderSynchronizerTest.class)); + return suite; } } diff --git a/source/java/org/alfresco/repo/security/person/AbstractHomeFolderProvider.java b/source/java/org/alfresco/repo/security/person/AbstractHomeFolderProvider.java index e127e5c735..d4793860e2 100644 --- a/source/java/org/alfresco/repo/security/person/AbstractHomeFolderProvider.java +++ b/source/java/org/alfresco/repo/security/person/AbstractHomeFolderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -19,17 +19,12 @@ package org.alfresco.repo.security.person; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.util.PropertyCheck; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; @@ -38,6 +33,9 @@ import org.springframework.beans.factory.InitializingBean; * Common support for creating home folders This is hooked into node creation events from Person type objects via the * homeFolderManager. Provider must all be wired up to the homeFolderManager. * + * @deprecated + * Depreciated since 4.0. {@link AbstractHomeFolderProvider2} should now be used. + * * @author Andy Hind */ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, BeanNameAware, InitializingBean @@ -58,40 +56,34 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, private StoreRef storeRef; /** - * Service registry to get hold of public services (so taht actions are audited) + * Service registry to get hold of public services (so that actions are audited) */ private ServiceRegistry serviceRegistry; - /** - * Tenant service - required for MT-enabled environment, else optional - */ - private TenantService tenantService; - /** * The path to a folder */ private String path; - /** - * Cache the result of the path look up. - */ - private Map pathNodeRefs; // MT-aware - /** * The owner to set on creation of a home folder (if unset this will be the uid). */ private String ownerOnCreate; - private PermissionsManager onCreatePermissionsManager; + /** + * PermissionsManager used on creating the home folder + */ + private PermissionsManager onCreatePermissionsManager; - private PermissionsManager onReferencePermissionsManager; + /** + * PermissionsManager used on referencing the home folder + */ + private PermissionsManager onReferencePermissionsManager; - public AbstractHomeFolderProvider() - { - super(); - - pathNodeRefs = new ConcurrentHashMap(); - } + /** + * Adaptor for this instance to be a HomeFolderProvider2 + */ + private V2Adaptor v2Adaptor = new V2Adaptor(this); /** * Register with the homeFolderManagewr @@ -99,13 +91,9 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, public void afterPropertiesSet() throws Exception { PropertyCheck.mandatory(this, "homeFolderManager", homeFolderManager); - homeFolderManager.addProvider(this); + homeFolderManager.addProvider(v2Adaptor); } - // === // - // IOC // - // === // - /** * Get the home folder manager. */ @@ -116,7 +104,6 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, /** * Set the home folder manager. - * * @param homeFolderManager */ public void setHomeFolderManager(HomeFolderManager homeFolderManager) @@ -127,6 +114,7 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, /** * Get the provider name */ + @Override public String getName() { return name; @@ -135,6 +123,7 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, /** * The provider name is taken from the bean name */ + @Override public void setBeanName(String name) { this.name = name; @@ -153,7 +142,14 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, */ public void setPath(String path) { + boolean reset = this.path != null; this.path = path; + + // If a reset need to clear caches + if (reset) + { + homeFolderManager.clearCaches(v2Adaptor); + } } /** @@ -201,7 +197,7 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, */ public void setTenantService(TenantService tenantService) { - this.tenantService = tenantService; + // keep class signature but no longer use value } /** @@ -212,10 +208,26 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, this.onCreatePermissionsManager = onCreatePermissionsManager; } + /** + * Gets the PermissionsManager used on creating the home folder + */ + public PermissionsManager getOnCreatePermissionsManager() + { + return onCreatePermissionsManager; + } + public void setOnReferencePermissionsManager(PermissionsManager onReferencePermissionsManager) { this.onReferencePermissionsManager = onReferencePermissionsManager; } + + /** + * Gets the PermissionsManager used on referencing the home folder + */ + public PermissionsManager getOnReferencePermissionsManager() + { + return onReferencePermissionsManager; + } /** * Set the authority to use as the owner of all home folder nodes. @@ -225,34 +237,28 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, this.ownerOnCreate = ownerOnCreate; } + /** + * Get the authority to use as the owner of all home folder nodes. + */ + public String getOwnerOnCreate() + { + return ownerOnCreate; + } + /** * Cache path to node resolution */ protected NodeRef getPathNodeRef() { - String tenantDomain = (tenantService != null ? tenantService.getCurrentUserDomain() : TenantService.DEFAULT_DOMAIN); - - NodeRef pathNodeRef = pathNodeRefs.get(tenantDomain); - if (pathNodeRef == null) - { - pathNodeRef = resolvePath(path); - pathNodeRefs.put(tenantDomain, pathNodeRef); - } - return pathNodeRef; + return homeFolderManager.getRootPathNodeRef(v2Adaptor); } /** - * Utility metho to resolve paths to nodes. + * Utility method to resolve paths to nodes. */ protected NodeRef resolvePath(String pathToResolve) { - List refs = serviceRegistry.getSearchService().selectNodes(serviceRegistry.getNodeService().getRootNode(storeRef), pathToResolve, null, - serviceRegistry.getNamespaceService(), false); - if (refs.size() != 1) - { - throw new IllegalStateException("Non-unique path: found : " + pathToResolve + " " + refs.size()); - } - return refs.get(0); + return homeFolderManager.resolvePath(v2Adaptor, pathToResolve); } /** @@ -260,69 +266,98 @@ public abstract class AbstractHomeFolderProvider implements HomeFolderProvider, */ public void onCreateNode(ChildAssociationRef childAssocRef) { - AuthenticationUtil.RunAsWork action = new OnCreateNode(childAssocRef); - AuthenticationUtil.runAs(action, AuthenticationUtil.getSystemUserName()); + homeFolderManager.homeFolderCreateAndSetPermissions(v2Adaptor, childAssocRef.getChildRef()); } /** - * Abstract implementation to find/create the approriate home space. + * Abstract implementation to find/create the appropriate home space. */ protected abstract HomeSpaceNodeRef getHomeFolder(NodeRef person); - + /** - * Helper class to encapsulate the createion settinhg permissions etc - * - * @author Andy Hind + * Get adaptor for this instance to be a HomeFolderProvider2 */ - private class OnCreateNode implements AuthenticationUtil.RunAsWork + protected V2Adaptor getV2Adaptor() { - ChildAssociationRef childAssocRef; - - OnCreateNode(ChildAssociationRef childAssocRef) + return v2Adaptor; + } + + /** + * Adaptor to the HomeFolderProvider2 interface. + */ + public class V2Adaptor implements HomeFolderProvider2 + { + AbstractHomeFolderProvider abstractHomeFolderProvider; + + public V2Adaptor(AbstractHomeFolderProvider abstractHomeFolderProvider) { - this.childAssocRef = childAssocRef; + this.abstractHomeFolderProvider = abstractHomeFolderProvider; + abstractHomeFolderProvider.v2Adaptor = this; } - public NodeRef doWork() throws Exception + @Override + public String getName() { + return abstractHomeFolderProvider.getName(); + } - // Find person - NodeRef personNodeRef = childAssocRef.getChildRef(); - // Get home folder - HomeSpaceNodeRef homeFolder = getHomeFolder(personNodeRef); - // If it exists - if (homeFolder.getNodeRef() != null) - { - // Get uid and keep - String uid = DefaultTypeConverter.INSTANCE.convert(String.class, serviceRegistry.getNodeService().getProperty(personNodeRef, ContentModel.PROP_USERNAME)); + @Override + public String getStoreUrl() + { + return abstractHomeFolderProvider.getStoreRef().toString(); + } - // If created or found then set (other wise it was already set correctly) - if (homeFolder.getStatus() != HomeSpaceNodeRef.Status.VALID) - { - serviceRegistry.getNodeService().setProperty(personNodeRef, ContentModel.PROP_HOMEFOLDER, homeFolder.getNodeRef()); - } + @Override + public String getRootPath() + { + return abstractHomeFolderProvider.getPath(); + } - String ownerToSet = ownerOnCreate == null ? uid : ownerOnCreate; - // If created.. - if (homeFolder.getStatus() == HomeSpaceNodeRef.Status.CREATED) - { - if (onCreatePermissionsManager != null) - { - onCreatePermissionsManager.setPermissions(homeFolder.getNodeRef(), ownerToSet, uid); - } - } - else - { - if (onReferencePermissionsManager != null) - { - onReferencePermissionsManager.setPermissions(homeFolder.getNodeRef(), ownerToSet, uid); - } - } - - } - return homeFolder.getNodeRef(); + @Override + public List getHomeFolderPath(NodeRef person) + { + return (abstractHomeFolderProvider instanceof UIDBasedHomeFolderProvider) + ? ((UIDBasedHomeFolderProvider)abstractHomeFolderProvider).getHomeFolderPath(person) + : null; + } + @Override + public NodeRef getTemplateNodeRef() + { + return (abstractHomeFolderProvider instanceof UIDBasedHomeFolderProvider) + ? ((UIDBasedHomeFolderProvider)abstractHomeFolderProvider).getTemplateNodeRef() + : null; + } + + @Override + public String getOwner() + { + return abstractHomeFolderProvider.getOwnerOnCreate(); + } + + @Override + public PermissionsManager getOnCreatePermissionsManager() + { + return abstractHomeFolderProvider.getOnReferencePermissionsManager(); + } + + @Override + public PermissionsManager getOnReferencePermissionsManager() + { + return abstractHomeFolderProvider.getOnReferencePermissionsManager(); + } + + @Override + public HomeSpaceNodeRef getHomeFolder(NodeRef person) + { + return abstractHomeFolderProvider.getHomeFolder(person); + } + + // The old way to create the home folder, so must still call it in case + // the method is overridden + public void onCreateNode(ChildAssociationRef childAssocRef) + { + abstractHomeFolderProvider.onCreateNode(childAssocRef); } } - } diff --git a/source/java/org/alfresco/repo/security/person/AbstractHomeFolderProvider2.java b/source/java/org/alfresco/repo/security/person/AbstractHomeFolderProvider2.java new file mode 100644 index 0000000000..09c458c7a8 --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/AbstractHomeFolderProvider2.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.security.person; + +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; + +/** + * Abstract class that implements {@link HomeFolderProvider2} which + * works with the {@link HomeFolderManager} (which performs most of + * the work) to create home folders in custom locations. + * + * @author Alan Davis + */ +public abstract class AbstractHomeFolderProvider2 implements + HomeFolderProvider2, BeanNameAware, InitializingBean +{ + /** + * The provider name + */ + private String name; + + /** + * The home folder manager + */ + private HomeFolderManager homeFolderManager; + + /** + * The store URL. + */ + private String storeUrl; + + /** + * The path to the root folder + */ + private String rootPath; + + /** + * Set the authority to use as the owner of all home folder nodes. + * May be {@code null}. + */ + private String owner; + + /** + * PermissionsManager used on creating the home folder + */ + private PermissionsManager onCreatePermissionsManager; + + /** + * PermissionsManager used on referencing the home folder + */ + private PermissionsManager onReferencePermissionsManager; + + /** + * Register with the homeFolderManagewr + */ + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "homeFolderManager", homeFolderManager); + homeFolderManager.addProvider(this); + } + + /** + * Get the home folder manager. + */ + protected HomeFolderManager getHomeFolderManager() + { + return homeFolderManager; + } + + /** + * Set the home folder manager. + * @param homeFolderManager + */ + public void setHomeFolderManager(HomeFolderManager homeFolderManager) + { + this.homeFolderManager = homeFolderManager; + } + + /** + * Get the provider name + */ + @Override + public String getName() + { + return name; + } + + /** + * The provider name is taken from the bean name + */ + @Override + public void setBeanName(String name) + { + this.name = name; + } + + /** + * Get the path of the root folder + */ + @Override + public String getRootPath() + { + return rootPath; + } + + /** + * Set the path of the root folder + */ + public void setRootPath(String rootPath) + { + boolean reset = this.rootPath != null; + this.rootPath = rootPath; + + // If a reset need to clear caches + if (reset) + { + homeFolderManager.clearCaches(this); + } + } + + @Override + public String getStoreUrl() + { + return storeUrl; + } + + /** + * Set the store URL. + */ + public void setStoreUrl(String storeUrl) + { + this.storeUrl = storeUrl; + } + + /** + * Sets the PermissionsManager used on creating the home folder + */ + public void setOnCreatePermissionsManager(PermissionsManager onCreatePermissionsManager) + { + this.onCreatePermissionsManager = onCreatePermissionsManager; + } + + @Override + public PermissionsManager getOnCreatePermissionsManager() + { + return onCreatePermissionsManager; + } + + /** + * Sets the PermissionsManager used on referencing the home folder + */ + public void setOnReferencePermissionsManager(PermissionsManager onReferencePermissionsManager) + { + this.onReferencePermissionsManager = onReferencePermissionsManager; + } + + @Override + public PermissionsManager getOnReferencePermissionsManager() + { + return onReferencePermissionsManager; + } + + /** + * Set the authority to use as the owner of all home folder nodes. + */ + public void setOwner(String owner) + { + this.owner = owner; + } + + @Override + public String getOwner() + { + return owner; + } + + @Override + public List getHomeFolderPath(NodeRef person) + { + return null; + } + + @Override + public NodeRef getTemplateNodeRef() + { + return null; + } +} diff --git a/source/java/org/alfresco/repo/security/person/BootstrapHomeFolderProvider.java b/source/java/org/alfresco/repo/security/person/BootstrapHomeFolderProvider.java index b4c043555d..b8062a9b69 100644 --- a/source/java/org/alfresco/repo/security/person/BootstrapHomeFolderProvider.java +++ b/source/java/org/alfresco/repo/security/person/BootstrapHomeFolderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -23,17 +23,16 @@ import org.alfresco.service.cmr.repository.NodeRef; /** * Provider to use in the boostrap process - does nothing * - * Probably not required as behaviour/policies are disabled during normal import. + * Thought to be probably not required as behaviour/policies are + * disabled during normal import, but is used for 'admin' and 'guest' * * @author Andy Hind */ -public class BootstrapHomeFolderProvider extends AbstractHomeFolderProvider +public class BootstrapHomeFolderProvider extends AbstractHomeFolderProvider2 { - @Override - protected HomeSpaceNodeRef getHomeFolder(NodeRef person) + public HomeSpaceNodeRef getHomeFolder(NodeRef person) { return new HomeSpaceNodeRef(null, HomeSpaceNodeRef.Status.VALID); } - } diff --git a/source/java/org/alfresco/repo/security/person/ExistingPathBasedHomeFolderProvider.java b/source/java/org/alfresco/repo/security/person/ExistingPathBasedHomeFolderProvider.java index b84eb15253..db24169f3a 100644 --- a/source/java/org/alfresco/repo/security/person/ExistingPathBasedHomeFolderProvider.java +++ b/source/java/org/alfresco/repo/security/person/ExistingPathBasedHomeFolderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,35 +18,20 @@ */ package org.alfresco.repo.security.person; -import org.alfresco.model.ContentModel; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; /** - * Set a home space from a simple path. + * HomeFolderProvider that simply uses the root path for the home folder. + * + * @deprecated + * Depreciated since 4.0. {@link ExistingPathBasedHomeFolderProvider2} should now be used. * * @author Andy Hind */ public class ExistingPathBasedHomeFolderProvider extends AbstractHomeFolderProvider { - - public ExistingPathBasedHomeFolderProvider() - { - super(); - } - protected HomeSpaceNodeRef getHomeFolder(NodeRef person) { - NodeRef existingHomeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, getServiceRegistry().getNodeService().getProperty( - person, ContentModel.PROP_HOMEFOLDER)); - if (existingHomeFolder == null) - { - return new HomeSpaceNodeRef(getPathNodeRef(), HomeSpaceNodeRef.Status.REFERENCED); - } - else - { - return new HomeSpaceNodeRef(existingHomeFolder, HomeSpaceNodeRef.Status.VALID); - } + return getHomeFolderManager().getHomeFolder(getV2Adaptor(), person, true); } - } diff --git a/source/java/org/alfresco/repo/security/person/ExistingPathBasedHomeFolderProvider2.java b/source/java/org/alfresco/repo/security/person/ExistingPathBasedHomeFolderProvider2.java new file mode 100644 index 0000000000..2c2c3d7f58 --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/ExistingPathBasedHomeFolderProvider2.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.security.person; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * HomeFolderProvider that simply uses the root path for the home folder. + * Generally it is a better idea to give each user their own home folder. + * + * @author Alan Davis + */ +public class ExistingPathBasedHomeFolderProvider2 extends AbstractHomeFolderProvider2 +{ + @Override + public HomeSpaceNodeRef getHomeFolder(NodeRef person) + { + return getHomeFolderManager().getHomeFolder(this, person, true); + } +} diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderManager.java b/source/java/org/alfresco/repo/security/person/HomeFolderManager.java index c853931452..1dc7da544f 100644 --- a/source/java/org/alfresco/repo/security/person/HomeFolderManager.java +++ b/source/java/org/alfresco/repo/security/person/HomeFolderManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,15 +18,27 @@ */ package org.alfresco.repo.security.person; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.alfresco.model.ContentModel; import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileFolderUtil; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; 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; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; @@ -34,26 +46,43 @@ import org.alfresco.service.namespace.QName; /** * Manage home folder creation by binding to events from the cm:person type. * - * @author Andy Hind + * @author Andy Hind, + * Alan Davis (support v1 and v2 HomeFolderProviders - code from + * v1 HomeFolderProviders moved into HomeFolderManager). */ public class HomeFolderManager implements NodeServicePolicies.OnCreateNodePolicy { - private PolicyComponent policyComponent; private NodeService nodeService; private boolean enableHomeFolderCreationAsPeopleAreCreated = false; + private ServiceRegistry serviceRegistry; + + private TenantService tenantService; + /** * A default provider */ - private HomeFolderProvider defaultProvider; + private HomeFolderProvider2 defaultProvider; /** - * Providers that have registered and are looken up by name (== bean name) + * Original Providers (now depreciated) that have registered and are looked up by bean name. */ - private Map providers = new HashMap(); + @SuppressWarnings("deprecation") + private Map v1Providers = new HashMap(); + + /** + * Providers that have registered and are looked up by bean name. + */ + private Map v2Providers = new HashMap(); + + /** + * Cache the result of the path look up. + */ + private Map> rootPathNodeRefMaps = + new ConcurrentHashMap>(); /** * Bind the class behaviour to this implementation @@ -90,21 +119,65 @@ public class HomeFolderManager implements NodeServicePolicies.OnCreateNodePolicy this.nodeService = nodeService; } + /** + * Set the service registry. + */ + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + /** + * Set the tenant service + */ + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + /** * Register a home folder provider. * * @param provider */ + @SuppressWarnings("deprecation") public void addProvider(HomeFolderProvider provider) { - providers.put(provider.getName(), provider); + v1Providers.put(provider.getName(), provider); + } + + /** + * Register a home folder provider. + * + * @param provider + */ + public void addProvider(HomeFolderProvider2 provider) + { + v2Providers.put(provider.getName(), provider); + } + + /** + * Returns the version 1 HomeFolderProvider with the given name. + */ + @SuppressWarnings("deprecation") + public HomeFolderProvider getHomeFolderProvider1(String providerName) + { + return v1Providers.get(providerName); + } + + /** + * Returns the version 2 HomeFolderProvider2 with the given name. + */ + public HomeFolderProvider2 getHomeFolderProvider2(String providerName) + { + return v2Providers.get(providerName); } /** * Set the default home folder provider (user which none is specified or when one is not found) * @param defaultProvider */ - public void setDefaultProvider(HomeFolderProvider defaultProvider) + public void setDefaultProvider(HomeFolderProvider2 defaultProvider) { this.defaultProvider = defaultProvider; } @@ -123,22 +196,302 @@ public class HomeFolderManager implements NodeServicePolicies.OnCreateNodePolicy /** * Find the provider and call. */ + @SuppressWarnings("deprecation") public void makeHomeFolder(ChildAssociationRef childAssocRef) { - HomeFolderProvider provider = defaultProvider; - String providerName = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(childAssocRef + HomeFolderProvider2 v2Provider = defaultProvider; + HomeFolderProvider v1Provider = null; + String providerName = DefaultTypeConverter.INSTANCE.convert( + String.class, nodeService.getProperty(childAssocRef .getChildRef(), ContentModel.PROP_HOME_FOLDER_PROVIDER)); if (providerName != null) { - provider = providers.get(providerName); - if (provider == null) + v2Provider = getHomeFolderProvider2(providerName); + if (v2Provider == null) { - provider = defaultProvider; + v1Provider = getHomeFolderProvider1(providerName); + if (v1Provider == null) + { + v2Provider = defaultProvider; + } } } - if (provider != null) + else { - provider.onCreateNode(childAssocRef); + providerName = defaultProvider.getName(); + nodeService.setProperty(childAssocRef.getChildRef(), + ContentModel.PROP_HOME_FOLDER_PROVIDER, providerName); + } + if (v2Provider != null) + { + // If a V2Adaptor we still must call onCreateNode just like a + // v1 HomeFolderProvider in case it has been overridden + if (v2Provider instanceof AbstractHomeFolderProvider.V2Adaptor) + { + ((AbstractHomeFolderProvider.V2Adaptor)v2Provider).onCreateNode(childAssocRef); + } + else + { + homeFolderCreateAndSetPermissions(v2Provider, childAssocRef.getChildRef()); + } + } + else if (v1Provider != null) + { + v1Provider.onCreateNode(childAssocRef); + } + } + + void homeFolderCreateAndSetPermissions(HomeFolderProvider2 provider, NodeRef personNodeRef) + { + AuthenticationUtil.RunAsWork action = + new RunAsCreateAndSetPermissions(provider, personNodeRef); + AuthenticationUtil.runAs(action, AuthenticationUtil.getSystemUserName()); + } + + /** + * Helper class to encapsulate the creation and setting permissions etc + */ + private class RunAsCreateAndSetPermissions implements AuthenticationUtil.RunAsWork + { + NodeRef personNodeRef; + HomeFolderProvider2 provider; + + RunAsCreateAndSetPermissions(HomeFolderProvider2 provider, NodeRef personNodeRef) + { + this.personNodeRef = personNodeRef; + this.provider = provider; + } + + public NodeRef doWork() throws Exception + { + // Get home folder + HomeSpaceNodeRef homeFolder = provider.getHomeFolder(personNodeRef); + + // If it exists + if (homeFolder.getNodeRef() != null) + { + // Get uid and keep + String uid = DefaultTypeConverter.INSTANCE.convert(String.class, + serviceRegistry.getNodeService().getProperty( + personNodeRef, ContentModel.PROP_USERNAME)); + + // If created or found then set (other wise it was already set correctly) + if (homeFolder.getStatus() != HomeSpaceNodeRef.Status.VALID) + { + serviceRegistry.getNodeService().setProperty( + personNodeRef, ContentModel.PROP_HOMEFOLDER, homeFolder.getNodeRef()); + } + + final String providerSuppliedOwner = provider.getOwner(); + String owner = (providerSuppliedOwner == null) ? uid : providerSuppliedOwner; + // If created.. + if (homeFolder.getStatus() == HomeSpaceNodeRef.Status.CREATED) + { + PermissionsManager onCreatePermissionsManager = + provider.getOnCreatePermissionsManager(); + if (onCreatePermissionsManager != null) + { + onCreatePermissionsManager.setPermissions( + homeFolder.getNodeRef(), owner, uid); + } + } + else + { + PermissionsManager onReferencePermissionsManager = + provider.getOnReferencePermissionsManager(); + if (onReferencePermissionsManager != null) + { + onReferencePermissionsManager.setPermissions( + homeFolder.getNodeRef(), owner, uid); + } + } + } + return homeFolder.getNodeRef(); + } + } + + private StoreRef getStoreRef(HomeFolderProvider2 provider) + { + // Could check to see if provider is a V2Adaptor to avoid + // object creation, but there is little point. + return new StoreRef(provider.getStoreUrl()); + } + + /** + * Helper method for {@link HomeFolderProvider2.getHomeFolder} (so that it + * does not need its own NodeService) that returns a person property value. + */ + public String getPersonProperty(NodeRef person, QName name) + { + String value = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(person, name)); + + if(value == null || value.length() == 0) + { + throw new PersonException("Can not create a home folder when the "+name+" property is null or empty"); + } + return value; + } + + void clearCaches(HomeFolderProvider2 provider) + { + getRootPathNodeRefMap(provider).clear(); + } + + NodeRef getRootPathNodeRef(HomeFolderProvider2 provider) + { + String rootPath = provider.getRootPath(); + String tenantDomain = (tenantService != null ? tenantService.getCurrentUserDomain() : TenantService.DEFAULT_DOMAIN); + Map rootPathNodeRefMap = getRootPathNodeRefMap(provider); + NodeRef rootPathNodeRef = rootPathNodeRefMap.get(tenantDomain); + if (rootPathNodeRef == null) + { + // ok with race condition for initial construction + rootPathNodeRef = resolvePath(provider, rootPath); + rootPathNodeRefMap.put(tenantDomain, rootPathNodeRef); + } + return rootPathNodeRef; + } + + private Map getRootPathNodeRefMap(HomeFolderProvider2 provider) + { + String name = provider.getName(); + Map rootPathNodeRefMap = rootPathNodeRefMaps.get(name); + if (rootPathNodeRefMap == null) + { + // ok with race condition for initial construction + rootPathNodeRefMap = new ConcurrentHashMap(); + rootPathNodeRefMaps.put(name, rootPathNodeRefMap); + } + return rootPathNodeRefMap; + } + + /** + * Utility method to resolve paths to nodes. + */ + NodeRef resolvePath(HomeFolderProvider2 provider, String pathToResolve) + { + List refs = serviceRegistry.getSearchService().selectNodes( + serviceRegistry.getNodeService().getRootNode(getStoreRef(provider)), + pathToResolve, null, + serviceRegistry.getNamespaceService(), false); + if (refs.size() != 1) + { + throw new IllegalStateException("Non-unique path: found : " + + pathToResolve + " " + refs.size()); + } + return refs.get(0); + } + + /** + * Helper method for {@link HomeFolderProvider2.getHomeFolder(NodeRef)} + * implementations to return a {@link HomeSpaceNodeRef} + * @param referenceRootNode indicates that a reference to the root node + * should be returned if the home folder property on the person + * has not yet been set. + */ + public HomeSpaceNodeRef getHomeFolder(HomeFolderProvider2 provider, NodeRef person, boolean referenceRootNode) + { + HomeSpaceNodeRef homeSpaceNodeRef = null; + NodeRef existingHomeFolder = DefaultTypeConverter.INSTANCE.convert( + NodeRef.class, serviceRegistry.getNodeService().getProperty( + person, ContentModel.PROP_HOMEFOLDER)); + if (existingHomeFolder != null) + { + homeSpaceNodeRef = new HomeSpaceNodeRef(existingHomeFolder, + HomeSpaceNodeRef.Status.VALID); + } + else if (referenceRootNode) + { + homeSpaceNodeRef = new HomeSpaceNodeRef(getRootPathNodeRef(provider), + HomeSpaceNodeRef.Status.REFERENCED); + } + else + { + FileFolderService fileFolderService = serviceRegistry.getFileFolderService(); + List homeFolderPath = provider.getHomeFolderPath(person); + + FileInfo fileInfo; + + // Test if it already exists + NodeRef existing = getExisting(provider, fileFolderService, homeFolderPath); + if (existing != null) + { + fileInfo = fileFolderService.getFileInfo(existing); + } + else + { + fileInfo = createTree(provider, getRootPathNodeRef(provider), homeFolderPath, + provider.getTemplateNodeRef(), fileFolderService); + } + NodeRef homeFolderNodeRef = fileInfo.getNodeRef(); + return new HomeSpaceNodeRef(homeFolderNodeRef, HomeSpaceNodeRef.Status.CREATED); + } + return homeSpaceNodeRef; + } + + private NodeRef getExisting(HomeFolderProvider2 provider, FileFolderService fileFolderService, + List homeFolderPath) + { + NodeRef existing; + try + { + FileInfo existingFileInfo = fileFolderService.resolveNamePath(getRootPathNodeRef(provider), homeFolderPath); + existing = existingFileInfo.getNodeRef(); + } + catch (FileNotFoundException fnfe) + { + existing = null;// home folder noderef doesn't exist yet + } + return existing; + } + + /** + * creates a tree of folder nodes based on the path elements provided. + */ + private FileInfo createTree(HomeFolderProvider2 provider, NodeRef root, + List homeFolderPath, NodeRef templateNodeRef, + FileFolderService fileFolderService) + { + NodeRef newParent = createNewParentIfRequired(root, homeFolderPath, fileFolderService); + String homeFolderName = homeFolderPath.get(homeFolderPath.size()-1); + FileInfo fileInfo; + if (templateNodeRef == null) + { + fileInfo = fileFolderService.create( + newParent, + homeFolderName, + ContentModel.TYPE_FOLDER); + } + else + { + try + { + fileInfo = fileFolderService.copy( + templateNodeRef, + newParent, + homeFolderName); + } + catch (FileNotFoundException e) + { + throw new PersonException("Invalid template to create home space"); + } + } + return fileInfo; + } + + private NodeRef createNewParentIfRequired(NodeRef root, + List homeFolderPath, FileFolderService fileFolderService) + { + if (homeFolderPath.size() > 1) + { + List parentPath = new ArrayList(homeFolderPath); + parentPath.remove(parentPath.size()-1); + return FileFolderUtil.makeFolders(fileFolderService, root, + parentPath, ContentModel.TYPE_FOLDER).getNodeRef(); + } + else + { + return root; } } } diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderProvider.java b/source/java/org/alfresco/repo/security/person/HomeFolderProvider.java index 508f2760ed..d977395b61 100644 --- a/source/java/org/alfresco/repo/security/person/HomeFolderProvider.java +++ b/source/java/org/alfresco/repo/security/person/HomeFolderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -23,6 +23,9 @@ import org.alfresco.repo.node.NodeServicePolicies; /** * Interface for home folder providers. * + * @deprecated + * Depreciated since 4.0. {@link HomeFolderProvider2} should now be used. + * * @author Andy Hind */ public interface HomeFolderProvider extends NodeServicePolicies.OnCreateNodePolicy diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderProvider2.java b/source/java/org/alfresco/repo/security/person/HomeFolderProvider2.java new file mode 100644 index 0000000000..36900ccf3d --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/HomeFolderProvider2.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.security.person; + +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Interface for home folder providers. Instances work with the + * {@link HomeFolderManager} (which performs most of the work) + * to allow it to create home folders in custom locations. + * + * The home folder may be a simple structure where all users share a root folder (See + * {@link ExistingPathBasedHomeFolderProvider2}), or all home folders are in the same root + * folder (See {@link UsernameHomeFolderProvider}) or in a tree of sub folders to + * avoids any single directory containing too many home directories which might cause + * performance issues (See {@link RegexHomeFolderProvider}).

+ * + * If the HomeFolderProvider is changed, home folders may be + * moved by using the {@link HomeFolderProviderSynchronizer} which optionally runs on + * restart. + * + * @author Andy Hind, Alan Davis (support v1 and v2 HomeFolderProviders) + */ +public interface HomeFolderProvider2 +{ + /** + * Get the name of the provider (the bean name). + */ + String getName(); + + /** + * Get the URL String of the node store that will be used. + */ + String getStoreUrl(); + + /** + * Get the root path in the store under which all home folders will be located. + */ + String getRootPath(); + + /** + * Returns a preferred path (a list of folder names) for the home folder relative to + * the root path. If all users share the root, the returned value should be an empty + * List or {@code null}. When all users have their own folder under the root + * there should be just one element in the List. Multiple elements should be returned + * when a nested folder structure is preferred. + * @param person NodeRef from which a property (normally the userName) is used as a + * hash key to create a nested directory structure. + * @return the path to be used. + */ + List getHomeFolderPath(NodeRef person); + + /** + * Returns a node to copy (a template) for the home folder. + * Only used by HomeFolderProviders that create home folders rather + * than just reference existing folders. + * @return the node to copy or {@code null} if not required. + */ + NodeRef getTemplateNodeRef(); + + /** + * Set the authority to use as the owner of all home folder nodes. + * If {@code null} the {@link ContentModel.PROP_USERNAME} value of + * the person is used. + */ + String getOwner(); + + /** + * Gets the PermissionsManager used on creating the home folder + */ + PermissionsManager getOnCreatePermissionsManager(); + + /** + * Gets the PermissionsManager used on referencing the home folder + */ + PermissionsManager getOnReferencePermissionsManager(); + + /** + * Callback from {@link HomeFolderManager} to locate or create a home folder. + * Implementations normally call {@link HomeFolderManager.getHomeFolder}. + */ + HomeSpaceNodeRef getHomeFolder(NodeRef person); +} diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java new file mode 100644 index 0000000000..9d7dfd6e07 --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizer.java @@ -0,0 +1,1051 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.security.person; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.batch.BatchProcessWorkProvider; +import org.alfresco.repo.batch.BatchProcessor; +import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorkerAdaptor; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.tenant.Tenant; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.VmShutdownListener; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; + +/** + * Called on startup to move (synchronise) home folders to the preferred + * location defined by their {@link HomeFolderProvider2} or extend the + * now depreciated {@link AbstractHomeFolderProvider}. Only users that + * use a HomeFolderProvider2 that don't provide a shared home + * folder (all user are given the same home folder) will be moved. This + * allows existing home directories to be moved to reflect changes in + * policy related to the location of home directories. Originally created + * for ALF-7797 which related to the need to move large numbers of + * existing home directories created via an LDAP import into a hierarchical + * folder structure with fewer home folder in each.

+ * + * By default no action is taken unless the the global property + * {@code home_folder_provider_synchronizer.enabled=true}.

+ * + * The home folders for internal users (such as {@code admin} and {@code + * guest}) that use {@code guestHomeFolderProvider} or {@code + * bootstrapHomeFolderProvider} are not moved, nor are any users that use + * {@link HomeFolderProviders} create shared home folders (all user are + * given the same home folder). + * + * It is also possible change the HomeFolderProvider used by all other + * users by setting the global property + * {@code home_folder_provider_synchronizer.override_provider=}.

+ * + * Warning: The LDAP synchronise process overwrites the home folder + * provider property. This is not an issue as long as the root path of + * the overwriting provider is the same as the overwritten provider or is + * not an ancestor of any of the existing home folders. This is important + * because the root directory value is used by this class to tidy up empty + * 'parent' folders under the root when a home folders are moved elsewhere. + * If you have any concerns that this may not be true, set the global + * property {@code home_folder_provider_synchronizer.keep_empty_parents=true} + * and tidy up any empty folders manually. Typically users created by the + * LDAP sync process are all placed under the same root folder so there + * will be no parent folders anyway.

+ * + * @author Alan Davis + */ +public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean +{ + private static final Log logger = LogFactory.getLog(HomeFolderProviderSynchronizer.class); + + private static final String ENABLED_PROPERTY_NAME = "home_folder_provider_synchronizer.enabled"; + private static final String OVERRIDE_PROPERTY_NAME = "home_folder_provider_synchronizer.override_provider"; + private static final String KEEP_EMPTY_PARENTS_PROPERTY_NAME = "home_folder_provider_synchronizer.keep_empty_parents"; + + private static final String GUEST_HOME_FOLDER_PROVIDER = "guestHomeFolderProvider"; + private static final String BOOTSTRAP_HOME_FOLDER_PROVIDER = "bootstrapHomeFolderProvider"; + + private final Properties properties; + private final TransactionService transactionService; + private final AuthorityService authorityService; + private final PersonService personService; + private final FileFolderService fileFolderService; + private final NodeService nodeService; + private final HomeFolderManager homeFolderManager; + private final TenantAdminService tenantAdminService; + + public HomeFolderProviderSynchronizer(Properties properties, + TransactionService transactionService, + AuthorityService authorityService, PersonService personService, + FileFolderService fileFolderService, NodeService nodeService, + HomeFolderManager homeFolderManager, + TenantAdminService tenantAdminService) + { + this.properties = properties; + this.transactionService = transactionService; + this.authorityService = authorityService; + this.personService = personService; + this.fileFolderService = fileFolderService; + this.nodeService = nodeService; + this.homeFolderManager = homeFolderManager; + this.tenantAdminService = tenantAdminService; + } + + private boolean enabled() + { + return "true".equalsIgnoreCase(properties.getProperty(ENABLED_PROPERTY_NAME)); + } + + private String getOverrideHomeFolderProviderName() + { + return properties.getProperty(OVERRIDE_PROPERTY_NAME); + } + + private boolean keepEmptyParents() + { + return "true".equalsIgnoreCase(properties.getProperty(KEEP_EMPTY_PARENTS_PROPERTY_NAME)); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // do nothing + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + if (enabled()) + { + final String overrideProviderName = getOverrideHomeFolderProviderName(); + + // Scan users in default and each Tenant + String systemUserName = AuthenticationUtil.getSystemUserName(); + scanPeople(systemUserName, TenantService.DEFAULT_DOMAIN, overrideProviderName); + + if (tenantAdminService.isEnabled()) + { + List tenants = tenantAdminService.getAllTenants(); + for (Tenant tenant : tenants) + { + if (tenant.isEnabled()) + { + systemUserName = tenantAdminService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenant.getTenantDomain()); + scanPeople(systemUserName, tenant.getTenantDomain(), overrideProviderName); + } + } + } + } + } + + /** + * Scans all {@code person} people objects and checks their home folder is located according + * to the person's home folder provider preferred default location. + * @param systemUserName String the system user name with the tenant-specific ID attached. + * @param tenantDomain String name of the tenant domain. Used to restrict the which people + * are processed. + * @param overrideProvider String the bean name of a HomeFolderProvider to be used + * in place of the all home folders existing providers. If {@code null} + * the existing provider is used. + */ + private void scanPeople(final String systemUserName, String tenantDomain, final String overrideProvider) + { + /* + * To avoid problems with existing home folders that are located in locations + * that will be needed by 'parent' folders, we need a 4 phase process. + * Consider the following user names and required structure. There would be a + * problem with the username 'ab'. + * + * abc --> ab/abc + * def /abd + * abd /ab + * ab de/def + * + * 1. Record which parent folders are needed + * 2. Move any home folders which overlap with parent folders to a temporary folder + * 3. Create parent folder structure. Done in a single thread before the move of + * home folders to avoid race conditions + * 4. Move home folders if required + * + * Alternative approaches are possible, but the above has the advantage that + * nodes are not moved if they are already in their preferred location. + */ + + // Using authorities rather than Person objects as they are much lighter + final Set authorities = getAllAuthoritiesInTxn(systemUserName); + final ParentFolderStructure parentFolderStructure = new ParentFolderStructure(); + final Map tmpFolders = new HashMap(); + + // Define the phases + final String createParentFoldersPhaseName = "createParentFolders"; + RunAsWorker[] workers = new RunAsWorker[] + { + new RunAsWorker(systemUserName, "calculateParentFolderStructure") + { + @Override + public void doWork(NodeRef person) throws Exception + { + calculateParentFolderStructure( + parentFolderStructure, person, overrideProvider); + } + }, + + new RunAsWorker(systemUserName, "moveHomeFolderThatClashesWithParentFolderStructure") + { + @Override + public void doWork(NodeRef person) throws Exception + { + moveHomeFolderThatClashesWithParentFolderStructure( + parentFolderStructure, tmpFolders, person, overrideProvider); + } + }, + + new RunAsWorker(systemUserName, createParentFoldersPhaseName) + { + @Override + public void doWork(NodeRef person) throws Exception + { + createParentFolders(person, overrideProvider); + } + }, + + new RunAsWorker(systemUserName, "moveHomeFolderIfRequired") + { + @Override + public void doWork(NodeRef person) throws Exception + { + moveHomeFolderIfRequired(person, overrideProvider); + } + } + }; + + // Run the phases + for (RunAsWorker worker: workers) + { + String name = worker.getName(); + if (logger.isDebugEnabled()) + { + logger.debug(" -- "+ + (TenantService.DEFAULT_DOMAIN.equals(tenantDomain)? "" : tenantDomain+" ")+ + name+" --"); + } + + int threadCount = (name.equals(createParentFoldersPhaseName)) ? 1 : 2; + int peoplePerTransaction = 20; + + // Use 2 threads, 20 person objects per transaction. Log every 100 entries. + BatchProcessor processor = new BatchProcessor( + "HomeFolderProviderSynchronizer", + transactionService.getRetryingTransactionHelper(), + new WorkProvider(authorities), + threadCount, peoplePerTransaction, + null, + logger, 100); + processor.process(worker, true); + if (processor.getTotalErrors() > 0) + { + logger.debug(" -- Give up after error --"); + break; + } + } + } + + // Can only use authorityService.getAllAuthorities(...) in a transaction. + private Set getAllAuthoritiesInTxn(final String systemUserName) + { + return AuthenticationUtil.runAs(new RunAsWork>() + { + public Set doWork() throws Exception + { + RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); + RetryingTransactionCallback> restoreCallback = + new RetryingTransactionCallback>() + { + public Set execute() throws Exception + { + return authorityService.getAllAuthorities(AuthorityType.USER); + } + }; + return txnHelper.doInTransaction(restoreCallback, false, true); + } + }, systemUserName); + } + + /** + * Work out the preferred parent folder structure so we will be able to work out if any + * existing home folders clash. + */ + private ParentFolderStructure calculateParentFolderStructure( + final ParentFolderStructure parentFolderStructure, + NodeRef person, String overrideProviderName) + { + new HomeFolderHandler(person, overrideProviderName) + { + @Override + protected void handleNotInPreferredLocation() + { + recordParentFolder(); + } + + @Override + protected void handleInPreferredLocation() + { + recordParentFolder(); + } + + private void recordParentFolder() + { + parentFolderStructure.recordParentFolder(root, preferredPath); + } + }.doWork(); + + return parentFolderStructure; + } + + /** + * Move any home folders (to a temporary folder) that clash with the + * new parent folder structure. + */ + private void moveHomeFolderThatClashesWithParentFolderStructure( + final ParentFolderStructure parentFolderStructure, + final Map tmpFolders, + NodeRef person, String overrideProviderName) + { + new HomeFolderHandler(person, overrideProviderName) + { + @Override + protected void handleNotInPreferredLocation() + { + moveToTmpIfClash(); + } + + @Override + protected void handleInPreferredLocation() + { + moveToTmpIfClash(); + } + + private void moveToTmpIfClash() + { + if (parentFolderStructure.clash(root, actualPath)) + { + String tmpFolder = getTmpFolderName(root); + preferredPath = new ArrayList(); + preferredPath.add(tmpFolder); + preferredPath.addAll(actualPath); + + // - providerName parameter is set to null as we don't want the + // "homeFolderProvider" reset + moveHomeFolder(person, homeFolder, root, preferredPath, originalRoot, + null, originalProviderName, actualPath); + } + } + + private String getTmpFolderName(NodeRef root) + { + synchronized(tmpFolders) + { + String tmpFolder = tmpFolders.get(root); + if (tmpFolder == null) + { + tmpFolder = createTmpFolderName(root); + tmpFolders.put(root, tmpFolder); + } + return tmpFolder; + } + } + + private String createTmpFolderName(NodeRef root) + { + // Try a few times but then give up. + for (int i = 1; i <= 100; i++) + { + String tmpFolderName = "Temporary"+i; + if (fileFolderService.searchSimple(root, tmpFolderName) == null) + { + fileFolderService.create(root, tmpFolderName, ContentModel.TYPE_FOLDER); + return tmpFolderName; + } + } + throw new PersonException("Unable to create a temporty " + + "folder into which home folders could be moved."); + } + }.doWork(); + } + + /** + * Creates the new home folder structure, before we move home folders so that + * we don't have race conditions that result in unnecessary retries. + * @param parentFolderStructure + */ + private void createParentFolders(NodeRef person, String overrideProviderName) + { + // We could short cut this process and build all the home folder from the + // ParentFolderStructure in the calling method, but that would complicate + // the code a little more and might result in transaction size problems. + // For now lets loop through all the person objects. + + new HomeFolderHandler(person, overrideProviderName) + { + @Override + protected void handleNotInPreferredLocation() + { + createNewParentIfRequired(root, preferredPath); + } + + @Override + protected void handleInPreferredLocation() + { + // do nothing + } + }.doWork(); + } + + /** + * If the home folder has been created but is not in its preferred location, the home folder + * is moved. Empty parent folder's under the old root are only removed if the old root is + * known and {@code home_folder_provider_synchronizer.keep_empty_parents=true} has not been + * set. + * @param person Person to be checked. + * @param overrideProviderName String name of a provider to use in place of + * the one currently used. Ignored if {@code null}. + */ + private void moveHomeFolderIfRequired(NodeRef person, String overrideProviderName) + { + new HomeFolderHandler(person, overrideProviderName) + { + @Override + protected void handleNotInPreferredLocation() + { + moveHomeFolder(person, homeFolder, root, preferredPath, originalRoot, + providerName, originalProviderName, actualPath); + } + + @Override + protected void handleInPreferredLocation() + { + if (logger.isDebugEnabled()) + { + logger.debug(" "+toPath(actualPath)+" is already in preferred location."); + } + } + + @Override + protected void handleSharedHomeProvider() + { + if (logger.isDebugEnabled()) + { + logger.debug(" "+userName+" "+providerName+" creates shared home folders - These are not moved."); + } + } + + + @Override + protected void handleOriginalSharedHomeProvider() + { + if (logger.isDebugEnabled()) + { + logger.debug(" "+userName+" Original "+originalProviderName+" creates shared home folders - These are not moved."); + } + } + + @Override + protected void handleNotAHomeFolderProvider2() + { + if (logger.isDebugEnabled()) + { + logger.debug(" "+userName+" "+providerName+" for is not a HomeFolderProvider2."); + } + } + + @Override + protected void handleSpecialHomeFolderProvider() + { + if (logger.isDebugEnabled()) + { + logger.debug(" "+userName+" Original "+originalProviderName+" is an internal type - These are not moved."); + } + } + + @Override + protected void handleHomeFolderNotSet() + { + if (logger.isDebugEnabled()) + { + logger.debug(" "+userName+" Home folder is not set - ignored"); + } + } + }.doWork(); + } + + /** + * @return a String for debug a folder list. + */ + private String toPath(List folders) + { + StringBuilder sb = new StringBuilder(""); + if (folders != null) + { + for (String folder : folders) + { + if (sb.length() > 0) + { + sb.append('/'); + } + sb.append(folder); + } + } + return sb.toString(); + } + + private String toPath(NodeRef root, NodeRef leaf) + { + StringBuilder sb = new StringBuilder(""); + List path = getRelativePath(root, leaf); + if (path != null) + { + for (String folder : path) + { + if (sb.length() > 0) + { + sb.append('/'); + } + sb.append(folder); + } + } + return sb.toString(); + } + + /** + * @return the relative 'path' (a list of folder names) of the {@code homeFolder} + * from the {@code root} or {@code null} if the homeFolder is not under the root + * or is the root. + */ + private List getRelativePath(NodeRef root, NodeRef homeFolder) + { + if (root == null || homeFolder == null) + { + return null; + } + + Path rootPath = nodeService.getPath(root); + Path homeFolderPath = nodeService.getPath(homeFolder); + int rootSize = rootPath.size(); + int homeFolderSize = homeFolderPath.size(); + if (rootSize >= homeFolderSize) + { + return null; + } + + // Check homeFolder is under root + for (int i=0; i < rootSize; i++) + { + if (!rootPath.get(i).equals(homeFolderPath.get(i))) + { + return null; + } + } + + // Build up path of sub folders + List path = new ArrayList(); + for (int i = rootSize; i < homeFolderSize; i++) + { + Path.Element element = homeFolderPath.get(i); + if (!(element instanceof Path.ChildAssocElement)) + { + return null; + } + QName folderQName = ((Path.ChildAssocElement) element).getRef().getQName(); + path.add(folderQName.getLocalName()); + } + return path; + } + + /** + * Move an existing home folder from one location to another, + * removing empty parent folders and reseting homeFolder and + * homeFolderProvider properties. + */ + private void moveHomeFolder(NodeRef person, NodeRef homeFolder, NodeRef root, + List preferredPath, NodeRef oldRoot, String providerName, + String originalProviderName, List actualPath) + { + try + { + // Create the new parent folder (if required) + // Code still here for completeness, but should be okay + // as the temporary folder will have been created and any + // parent folders should have been created. + NodeRef newParent = createNewParentIfRequired(root, preferredPath); + + String homeFolderName = preferredPath.get(preferredPath.size() - 1); + + // Throw our own FileExistsException before we get one that + // marks the transaction for rollback, as there is no point + // trying again. + if (nodeService.getChildByName(newParent, ContentModel.ASSOC_CONTAINS, + homeFolderName) != null) + { + throw new FileExistsException(newParent, homeFolderName); + } + + // Get the old parent before we move anything. + NodeRef oldParent = nodeService.getPrimaryParent(homeFolder) .getParentRef(); + + // Perform the move + homeFolder = fileFolderService.move(homeFolder, newParent, + homeFolderName).getNodeRef(); + + // Reset the homeFolder property + nodeService.setProperty(person, ContentModel.PROP_HOMEFOLDER, homeFolder); + + // Change provider name + if (providerName != null && !providerName.equals(originalProviderName)) + { + nodeService.setProperty(person, + ContentModel.PROP_HOME_FOLDER_PROVIDER, providerName); + } + + // Log action + if (logger.isDebugEnabled()) + { + logger.debug(" mv "+toPath(actualPath)+ + " "+ toPath(preferredPath)+ + ((providerName != null && !providerName.equals(originalProviderName)) + ? " AND reset provider to "+providerName + : "") + "."); + } + + // Tidy up + removeEmptyParentFolders(oldParent, oldRoot); + } + catch (FileExistsException e) + { + String message = "mv "+toPath(actualPath)+" "+toPath(preferredPath)+ + " failed as the target already existed."; + logger.error(" "+message); + throw new PersonException(message); + } + catch (FileNotFoundException e) + { + // This should not happen unless there is a coding error + String message = "mv "+toPath(actualPath)+" "+toPath(preferredPath)+ + " failed as source did not exist."; + logger.error(" "+message); + throw new PersonException(message); + } + } + + private NodeRef createNewParentIfRequired(NodeRef root, List homeFolderPath) + { + NodeRef parent = root; + int len = homeFolderPath.size() - 1; + for (int i = 0; i < len; i++) + { + String pathElement = homeFolderPath.get(i); + NodeRef nodeRef = nodeService.getChildByName(parent, + ContentModel.ASSOC_CONTAINS, pathElement); + if (nodeRef == null) + { + parent = fileFolderService.create(parent, pathElement, + ContentModel.TYPE_FOLDER).getNodeRef(); + } + else + { + // Throw our own FileExistsException before we get an + // exception when we cannot create a sub-folder under + // a non folder that marks the transaction rollback, as + // there is no point trying again. + if (!fileFolderService.getFileInfo(nodeRef).isFolder()) + { + throw new FileExistsException(parent, null); + } + + parent = nodeRef; + } + } + return parent; + } + + /** + * Removes the parent folder if it is empty and its parents up to but not + * including the root. + */ + private void removeEmptyParentFolders(NodeRef parent, NodeRef root) + { + // Parent folders we have created don't have an owner, were as + // home folders do, hence the 3rd test (just in case) as we really + // don't want to delete empty home folders. + if (root != null && + !keepEmptyParents() && + nodeService.getProperty(parent, ContentModel.PROP_OWNER) == null) + { + // Do nothing if root is not an ancestor of parent. + NodeRef nodeRef = parent; + while (!root.equals(nodeRef)) + { + if (nodeRef == null) + { + return; + } + nodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + } + + // Remove any empty nodes. + while (!root.equals(parent)) + { + nodeRef = parent; + parent = nodeService.getPrimaryParent(parent).getParentRef(); + + if (!nodeService.getChildAssocs(nodeRef).isEmpty()) + { + return; + } + if (logger.isDebugEnabled()) + { + logger.debug(" rm "+toPath(root, nodeRef)); + } + nodeService.deleteNode(nodeRef); + } + } + } + + // BatchProcessWorkProvider returns batches of 100 person objects from lightweight authorities. + private class WorkProvider implements BatchProcessWorkProvider + { + private static final int BATCH_SIZE = 100; + + private final VmShutdownListener vmShutdownLister = new VmShutdownListener("getHomeFolderProviderSynchronizerWorkProvider"); + private final Iterator iterator; + private final int size; + + public WorkProvider(Set authorities) + { + iterator = authorities.iterator(); + size = authorities.size(); + } + + @Override + public synchronized int getTotalEstimatedWorkSize() + { + return size; + } + + @Override + public synchronized Collection getNextWork() + { + if (vmShutdownLister.isVmShuttingDown()) + { + return Collections.emptyList(); + } + + RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); + RetryingTransactionCallback> restoreCallback = new RetryingTransactionCallback>() + { + public Collection execute() throws Exception + { + Collection results = new ArrayList(BATCH_SIZE); + while (results.size() < BATCH_SIZE && iterator.hasNext()) + { + String userName = iterator.next(); + try + { + NodeRef person = personService.getPerson(userName, false); + results.add(person); + } + catch (NoSuchPersonException e) + { + if (logger.isTraceEnabled()) + { + logger.trace("The user "+userName+" no longer exists - ignored."); + } + } + } + return results; + } + }; + return txnHelper.doInTransaction(restoreCallback, false, true); + } + } + + // BatchProcessWorker that runs work as another user. + private abstract class RunAsWorker extends BatchProcessWorkerAdaptor + { + final String userName; + final String name; + + public RunAsWorker(String userName, String name) + { + this.userName = userName; + this.name = name; + } + + public void process(final NodeRef person) throws Throwable + { + RunAsWork runAsWork = new RunAsWork() + { + @Override + public Object doWork() throws Exception + { + RunAsWorker.this.doWork(person); + return null; + } + }; + AuthenticationUtil.runAs(runAsWork, userName); + } + + public abstract void doWork(NodeRef person) throws Exception; + + public String getName() + { + return name; + } + }; + + // Obtains home folder provider and path information with call backs. + private abstract class HomeFolderHandler + { + protected final NodeRef person; + protected final String overrideProviderName; + + protected NodeRef homeFolder; + protected String userName; + protected String originalProviderName; + protected String providerName; + protected HomeFolderProvider2 provider; + protected NodeRef root; + protected List preferredPath; + protected List actualPath; + protected NodeRef originalRoot; + + public HomeFolderHandler(NodeRef person, String overrideProviderName) + { + this.person = person; + this.overrideProviderName = overrideProviderName; + } + + public void doWork() + { + homeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, + nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER)); + userName = DefaultTypeConverter.INSTANCE.convert( + String.class, nodeService.getProperty(person, ContentModel.PROP_USERNAME)); + + if (homeFolder != null) + { + originalProviderName = DefaultTypeConverter.INSTANCE.convert(String.class, + nodeService.getProperty(person, ContentModel.PROP_HOME_FOLDER_PROVIDER)); + if (!BOOTSTRAP_HOME_FOLDER_PROVIDER.equals(originalProviderName) && + !GUEST_HOME_FOLDER_PROVIDER.equals(originalProviderName)) + { + providerName = overrideProviderName != null + ? overrideProviderName + : originalProviderName; + provider = homeFolderManager.getHomeFolderProvider2(providerName); + + if (provider != null) + { + root = homeFolderManager.getRootPathNodeRef(provider); + preferredPath = provider.getHomeFolderPath(person); + + if (preferredPath == null || preferredPath.isEmpty()) + { + handleSharedHomeProvider(); + } + else + { + originalRoot = null; + HomeFolderProvider2 originalProvider = homeFolderManager.getHomeFolderProvider2(originalProviderName); + List originalPreferredPath = null; + if (originalProvider != null) + { + originalRoot = homeFolderManager.getRootPathNodeRef(originalProvider); + originalPreferredPath = originalProvider.getHomeFolderPath(person); + } + + if (originalProvider != null && + (originalPreferredPath == null || originalPreferredPath.isEmpty())) + { + handleOriginalSharedHomeProvider(); + } + else + { + actualPath = getRelativePath(root, homeFolder); + + if (preferredPath.equals(actualPath)) + { + handleInPreferredLocation(); + } + else + { + handleNotInPreferredLocation(); + } + } + } + } + else + { + handleNotAHomeFolderProvider2(); + } + } + else + { + handleSpecialHomeFolderProvider(); + } + } + else + { + handleHomeFolderNotSet(); + } + } + + protected abstract void handleInPreferredLocation(); + + protected abstract void handleNotInPreferredLocation(); + + protected void handleSharedHomeProvider() + { + } + + protected void handleOriginalSharedHomeProvider() + { + } + + protected void handleNotAHomeFolderProvider2() + { + } + + protected void handleSpecialHomeFolderProvider() + { + } + + protected void handleHomeFolderNotSet() + { + } + } + + // Gathers and checks parent folder paths. + private class ParentFolderStructure + { + // Sets of parent folders within each root node + private Map>> folders = new HashMap>>(); + + public void recordParentFolder(NodeRef root, List path) + { + Set> rootsFolders = getFolders(root); + synchronized(rootsFolders) + { + // If parent is the root, all home folders clash + int parentSize = path.size() - 1; + if (parentSize == 0) + { + // We could optimise the code a little by clearing + // all other entries and putting a contains(null) + // check just inside the synchronized(rootsFolders) + // but it might be useful to have a complete lit of + // folders. + rootsFolders.add(null); + + if (logger.isDebugEnabled()) + { + logger.debug(" Recorded root as parent"); + } + } + else + { + while (parentSize-- > 0) + { + List parentPath = new ArrayList(); + for (int j = 0; j <= parentSize; j++) + { + parentPath.add(path.get(j)); + } + + if (logger.isDebugEnabled() + && !rootsFolders.contains(parentPath)) + { + logger.debug(" Recorded parent: " + + toPath(parentPath)); + } + + rootsFolders.add(parentPath); + } + } + } + } + + /** + * @return {@code true} if the {@code path} is a parent folder + * or the parent folders includes the root itself. In + * the latter case all existing folders might clash + * so must be moved out of the way. + */ + public boolean clash(NodeRef root, List path) + { + Set> rootsFolders = getFolders(root); + synchronized(rootsFolders) + { + return rootsFolders.contains(path) || + rootsFolders.contains(null); + } + } + + private Set> getFolders(NodeRef root) + { + synchronized(folders) + { + Set> rootsFolders = folders.get(root); + if (rootsFolders == null) + { + rootsFolders = new HashSet>(); + folders.put(root, rootsFolders); + } + return rootsFolders; + } + } + } +} diff --git a/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizerTest.java b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizerTest.java new file mode 100644 index 0000000000..5d54406543 --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/HomeFolderProviderSynchronizerTest.java @@ -0,0 +1,944 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.security.person; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.tenant.Tenant; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.AuthorityService; +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.ApplicationContextHelper; +import org.alfresco.util.PropertyMap; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ComparisonFailure; +import org.junit.Test; +import org.springframework.context.ApplicationContext; + +/** + * Integration test for HomeFolderProviderSynchronizer. + * + * @author Alan Davis + */ +public class HomeFolderProviderSynchronizerTest +{ + private static final QName PROP_PARENT_PATH = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "parentPath"); + + private static ApplicationContext applicationContext; + private static ServiceRegistry serviceRegistry; + private static TransactionService transactionService; + private static FileFolderService fileFolderService; + private static PersonService personService; + private static NodeService nodeService; + private static ContentService contentService; + private static AuthorityService authorityService; + private static TenantAdminService tenantAdminService; + private static TenantService tenantService; + private static HomeFolderManager homeFolderManager; + private static Properties properties; + private static RegexHomeFolderProvider largeHomeFolderProvider; + private static String largeHomeFolderProviderName; + private static RegexHomeFolderProvider testHomeFolderProvider; + private static String testHomeFolderProviderName; + private static String storeUrl; + private static String origRootPath; + private static NodeRef rootNodeRef; + private static HomeFolderProviderSynchronizer homeFolderProviderSynchronizer; + private static boolean firstTest = true; + + private UserTransaction trans; + + @BeforeClass + public static void classSetup() throws Exception + { + applicationContext = ApplicationContextHelper.getApplicationContext(); + serviceRegistry = (ServiceRegistry) applicationContext.getBean("ServiceRegistry"); + transactionService = (TransactionService) applicationContext.getBean("transactionService"); + fileFolderService = (FileFolderService) applicationContext.getBean("fileFolderService"); + personService = (PersonService) applicationContext.getBean("personService"); + nodeService = (NodeService) applicationContext.getBean("nodeService"); + contentService = (ContentService) applicationContext.getBean("contentService"); + authorityService = (AuthorityService) applicationContext.getBean("authorityService"); + tenantAdminService = (TenantAdminService) applicationContext.getBean("tenantAdminService"); + tenantService = (TenantService) applicationContext.getBean("tenantService"); + homeFolderManager = (HomeFolderManager) applicationContext.getBean("homeFolderManager"); + largeHomeFolderProvider = (RegexHomeFolderProvider) applicationContext.getBean("largeHomeFolderProvider"); + largeHomeFolderProviderName = largeHomeFolderProvider.getName(); + storeUrl = largeHomeFolderProvider.getStoreUrl(); + origRootPath = largeHomeFolderProvider.getRootPath(); + properties = (Properties) applicationContext.getBean("global-properties"); + + personService.setCreateMissingPeople(true); + + // Create test home folder provider that gets its path from a property and the username + testHomeFolderProvider = new RegexHomeFolderProvider() + { + @Override + public List getHomeFolderPath(NodeRef person) + { + String parentPath = DefaultTypeConverter.INSTANCE.convert(String.class, nodeService.getProperty(person, PROP_PARENT_PATH)); + String propPath = ((parentPath == null || parentPath.length() == 0) ? "" : parentPath+'/')+ + homeFolderManager.getPersonProperty(person, ContentModel.PROP_USERNAME); + return Arrays.asList(propPath.split("/")); + } + }; + testHomeFolderProvider.setPropertyName(ContentModel.PROP_USERNAME.getLocalName()); + testHomeFolderProvider.setPattern("(..)"); + testHomeFolderProvider.setBeanName("testHomeFolderProvider"); + testHomeFolderProvider.setHomeFolderManager(homeFolderManager); + testHomeFolderProvider.setRootPath(origRootPath); + testHomeFolderProvider.setStoreUrl(storeUrl); + testHomeFolderProvider.setOnCreatePermissionsManager((PermissionsManager)applicationContext.getBean("defaultOnCreatePermissionsManager")); + testHomeFolderProvider.setOnCreatePermissionsManager((PermissionsManager)applicationContext.getBean("defaultOnCreatePermissionsManager")); + testHomeFolderProvider.setOnReferencePermissionsManager((PermissionsManager)applicationContext.getBean("defaultOnReferencePermissionsManager")); + testHomeFolderProviderName = testHomeFolderProvider.getName(); + homeFolderManager.addProvider(testHomeFolderProvider); + + homeFolderProviderSynchronizer = new HomeFolderProviderSynchronizer( + properties, transactionService, authorityService, + personService, fileFolderService, nodeService, + homeFolderManager, tenantAdminService); + } + + @Before + public void setUp() throws Exception + { + properties.setProperty("home_folder_provider_synchronizer.enabled", "true"); + properties.remove("home_folder_provider_synchronizer.override_provider"); + properties.remove("home_folder_provider_synchronizer.keep_empty_parents"); + + largeHomeFolderProvider.setPattern("^(..)"); + testHomeFolderProvider.setRootPath(origRootPath); + largeHomeFolderProvider.setRootPath(origRootPath); + + // Just in case we killed a test last time - tidy up + if (firstTest) + { + firstTest = false; + + AuthenticationUtil.setRunAsUserSystem(); + trans = transactionService.getUserTransaction(); + trans.begin(); + rootNodeRef = homeFolderManager.getRootPathNodeRef(largeHomeFolderProvider); + trans.commit(); + trans = null; + + tearDown(); + } + + AuthenticationUtil.setRunAsUserSystem(); + trans = transactionService.getUserTransaction(); + trans.begin(); + // System.out.println(NodeStoreInspector.dumpNode(nodeService, rootNodeRef)); + } + + @After + public void tearDown() throws Exception + { + if (trans != null) + { + try + { + trans.commit(); + } + catch (Exception e) + { + if ((trans.getStatus() == Status.STATUS_ACTIVE) || + (trans.getStatus() == Status.STATUS_MARKED_ROLLBACK)) + { + trans.rollback(); + trans = null; + } + } + } + + trans = transactionService.getUserTransaction(); + trans.begin(); + Set adminGuestUserHomeFolders = deleteNonAdminGuestUsers(); + deleteNonAdminGuestFolders(adminGuestUserHomeFolders); + deleteAllTenants(); + trans.commit(); + trans = null; + AuthenticationUtil.clearCurrentSecurityContext(); + } + + private Set deleteNonAdminGuestUsers() + { + final Set adminGuestUserHomeFolders = new HashSet(); + for (final NodeRef nodeRef : personService.getAllPeople()) + { + final String username = DefaultTypeConverter.INSTANCE.convert(String.class, + nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME)); + final String domainUsername = tenantService.getBaseNameUser(username); + String tenantDomain = tenantService.getUserDomain(username); + String systemUser = tenantAdminService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenantDomain); + boolean disabled = !TenantService.DEFAULT_DOMAIN.equals(tenantDomain) && + !tenantAdminService.isEnabledTenant(tenantDomain); + try + { + if (disabled) + { + tenantAdminService.enableTenant(tenantDomain); + } + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + deleteUser(adminGuestUserHomeFolders, nodeRef, username, domainUsername); + return null; + } + }, systemUser); + } + finally + { + if (disabled) + { + tenantAdminService.disableTenant(tenantDomain); + } + } + } + return adminGuestUserHomeFolders; + } + + // Delete users other than admin and guest. The home folders of + // admin and guest are added to internalUserHomeFolders. + private void deleteUser(final Set adminGuestUserHomeFolders, + NodeRef person, String username, String domainUsername) + { + if (!domainUsername.equals("admin") && !domainUsername.equals("guest")) + { + personService.deletePerson(person); + System.out.println("deleted user "+username); + } + else + { + NodeRef homeFolder = DefaultTypeConverter.INSTANCE.convert( + NodeRef.class, nodeService.getProperty(person, + ContentModel.PROP_HOMEFOLDER)); + adminGuestUserHomeFolders.add(homeFolder); + } + } + + private void deleteNonAdminGuestFolders(final Set adminGuestUserHomeFolders) + { + // Delete folders from under the home folder root path in case they have been left over + // from another test. Admin and Guest home folder should not be under here, but lets + // double check. + for (ChildAssociationRef childAssocs: nodeService.getChildAssocs(rootNodeRef)) + { + NodeRef nodeRef = childAssocs.getChildRef(); + if (!adminGuestUserHomeFolders.contains(nodeRef)) + { + System.out.println("TearDown remove '"+childAssocs.getQName().getLocalName()+ + "' from under the home folder root."); + nodeService.deleteNode(nodeRef); + } + } + } + + private NodeRef createUser(String parentPath, String username) throws Exception + { + return createUser(TenantService.DEFAULT_DOMAIN, parentPath, username); + } + + private NodeRef createUser(String parentPath, + String username, String homeFolderProviderName, boolean createHomeDirectory) throws Exception + { + return createUser(TenantService.DEFAULT_DOMAIN, parentPath, username, + homeFolderProviderName, createHomeDirectory); + } + + private NodeRef createUser(String tenantDomain, String parentPath, String username) throws Exception + { + return createUser(tenantDomain, parentPath, username, largeHomeFolderProviderName, true); + } + + private NodeRef createUser(String tenantDomain, final String parentPath, + final String username, final String homeFolderProviderName, + final boolean createHomeDirectory) throws Exception + { + final String domainUsername = tenantService.getDomainUser(username, tenantDomain); + String systemUser = tenantAdminService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenantDomain); + return AuthenticationUtil.runAs(new RunAsWork() + { + public NodeRef doWork() throws Exception + { + String firstName = username; + String lastName = "Smith"; + String emailAddress = String.format("%s.%s@xyz.com", firstName, + lastName); + PropertyMap properties = new PropertyMap(); + properties.put(ContentModel.PROP_USERNAME, domainUsername); + properties.put(ContentModel.PROP_FIRSTNAME, firstName); + properties.put(ContentModel.PROP_LASTNAME, lastName); + properties.put(ContentModel.PROP_EMAIL, emailAddress); + properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, testHomeFolderProviderName); + properties.put(PROP_PARENT_PATH, parentPath); + homeFolderManager.setEnableHomeFolderCreationAsPeopleAreCreated(createHomeDirectory); + NodeRef person = personService.createPerson(properties); + assertNotNull("The person nodeRef for "+domainUsername+" should have been created", person); + NodeRef homeFolder = DefaultTypeConverter.INSTANCE.convert( + NodeRef.class, nodeService.getProperty(person, + ContentModel.PROP_HOMEFOLDER)); + if (createHomeDirectory) + { + assertNotNull("The homeFolder for "+domainUsername+" should have been created", homeFolder); + } + else + { + assertNull("The homeFolder for "+domainUsername+" should NOT have been created", homeFolder); + } + + if (!testHomeFolderProviderName.equals(homeFolderProviderName)) + { + if (homeFolderProviderName == null) + { + nodeService.removeProperty(person, ContentModel.PROP_HOME_FOLDER_PROVIDER); + } + else + { + nodeService.setProperty(person, ContentModel.PROP_HOME_FOLDER_PROVIDER, + homeFolderProviderName); + } + } + return person; + } + }, systemUser); + } + + private NodeRef createFolder(String path) throws Exception + { + NodeRef parent = rootNodeRef; + if (path.length() > 0) + { + StringBuilder currentPath = new StringBuilder(); + for (String pathElement: path.split("/")) + { + if (currentPath.length() > 0) + { + currentPath.append("/"); + } + currentPath.append(pathElement); + NodeRef nodeRef = nodeService.getChildByName(parent, + ContentModel.ASSOC_CONTAINS, pathElement); + if (nodeRef == null) + { + parent = fileFolderService.create(parent, pathElement, + ContentModel.TYPE_FOLDER).getNodeRef(); + } + else + { + assertTrue("Expected "+currentPath+" to be a folder", + fileFolderService.getFileInfo(nodeRef).isFolder()); + parent = nodeRef; + } + } + } + return parent; + } + + private NodeRef createContent(String parentPath, String name) throws Exception + { + NodeRef parent = createFolder(parentPath); + PropertyMap propertyMap = new PropertyMap(); + propertyMap.put(ContentModel.PROP_CONTENT, new ContentData(null, "text/plain", + 0L, "UTF-16", Locale.ENGLISH)); + propertyMap.put(ContentModel.PROP_NAME, name); + NodeRef content = nodeService.createNode( + parent, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + ContentModel.TYPE_CONTENT, + propertyMap).getChildRef(); + ContentWriter writer = contentService.getWriter(content, ContentModel.TYPE_CONTENT, true); + writer.putContent("The cat sat on the mat."); + + // System.out.println(NodeStoreInspector.dumpNode(nodeService, rootNodeRef)); + return content; + } + + private String toPath(NodeRef root, NodeRef homeFolder) + { + if (root == null || homeFolder == null) + { + return null; + } + + Path rootPath = nodeService.getPath(root); + Path homeFolderPath = nodeService.getPath(homeFolder); + int rootSize = rootPath.size(); + int homeFolderSize = homeFolderPath.size(); + if (rootSize >= homeFolderSize) + { + return null; + } + + StringBuilder sb = new StringBuilder(""); + + // Check homeFolder is under root + for (int i=0; i < rootSize; i++) + { + if (!rootPath.get(i).equals(homeFolderPath.get(i))) + { + return null; + } + } + + // Build up path of sub folders + for (int i = rootSize; i < homeFolderSize; i++) + { + Path.Element element = homeFolderPath.get(i); + if (!(element instanceof Path.ChildAssocElement)) + { + return null; + } + QName folderQName = ((Path.ChildAssocElement) element).getRef().getQName(); + if (sb.length() > 0) + { + sb.append('/'); + } + sb.append(folderQName.getLocalName()); + } + return sb.toString(); + } + + private void createTenant(final String tenantDomain) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + if (!tenantAdminService.existsTenant(tenantDomain)) + { + tenantAdminService.createTenant(tenantDomain, + ("admin "+tenantDomain).toCharArray(), null); + } + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + private void deleteAllTenants() throws Exception + { + List tenants = tenantAdminService.getAllTenants(); + for (Tenant tenant : tenants) + { + deleteTenant(tenant.getTenantDomain()); + } + + } + + // DbNodeServiceImpl does not support deleteStore() at the moment, + // even though it supports createStore(), so just disable them for now. + private void deleteTenant(final String tenantDomain) throws Exception + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + if (tenantAdminService.existsTenant(tenantDomain)) + { + // Can't delete so disable + // tenantAdminService.deleteTenant(tenantDomain); + + if (tenantAdminService.isEnabledTenant(tenantDomain)) + { + tenantAdminService.disableTenant(tenantDomain); + } + } + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + private void assertHomeFolderLocation(String username, String expectedPath) throws Exception + { + assertHomeFolderLocation(TenantService.DEFAULT_DOMAIN, username, expectedPath); + } + + private void assertHomeFolderLocation(String tenantDomain, final String username, + final String expectedPath) throws Exception + { + try + { + final String domainUsername = tenantService.getDomainUser(username, tenantDomain); + String systemUser = tenantAdminService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenantDomain); + AuthenticationUtil.runAs(new RunAsWork() + { + public NodeRef doWork() throws Exception + { + NodeRef person = personService.getPerson(domainUsername, false); + NodeRef homeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, + nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER)); + assertNotNull("User: "+domainUsername+" home folder should exist", homeFolder); + + NodeRef rootPath = homeFolderManager.getRootPathNodeRef(largeHomeFolderProvider); + String actualPath = toPath(rootPath, homeFolder); + assertEquals("User: "+domainUsername+" home folder location", expectedPath, actualPath); + return null; + } + }, systemUser); + } + catch (RuntimeException e) + { + final Throwable cause = e.getCause(); + if (cause instanceof ComparisonFailure || cause instanceof AssertionError) + { + throw (ComparisonFailure)cause; + } + else + { + throw e; + } + } + } + + private boolean exists(String path) throws Exception + { + NodeRef parent = rootNodeRef; + boolean exists = true; + for (String pathElement: path.split("/")) + { + NodeRef nodeRef = nodeService.getChildByName(parent, + ContentModel.ASSOC_CONTAINS, pathElement); + if (nodeRef == null) + { + exists = false; + break; + } + else + { + parent = nodeRef; + } + } + return exists; + } + + private void moveUserHomeFolders() throws Exception + { + trans.commit(); + trans = null; + + homeFolderProviderSynchronizer.onBootstrap(null); + + trans = transactionService.getUserTransaction(); + trans.begin(); + } + + @Test + public void testCorrectLocation() throws Exception + { + createUser("te", "tess"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("tess", "te/tess"); + } + + @Test + public void testCreateParentFolder() throws Exception + { + createUser("", "fred"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fr/fred"); + } + + @Test + public void testNotEnabled() throws Exception + { + createUser("", "fred"); + properties.remove("home_folder_provider_synchronizer.enabled"); + + moveUserHomeFolders(); + + // If performed, the home folder will have been moved to fr/fred + // We must force the creation of the home folder as it will not + // have been done + personService.getPerson("fred"); + assertHomeFolderLocation("fred", "fred"); + } + + @Test + public void testHomeFolderNotYetCreated() throws Exception + { + NodeRef person = createUser("", "fred", largeHomeFolderProviderName, false); + + moveUserHomeFolders(); + + NodeRef homeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, + nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER)); + assertNull("The homeFolder should NOT have been created", homeFolder); + + person = personService.getPerson("fred"); + homeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, + nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER)); + assertNotNull("The homeFolder should have been created", homeFolder); + } + + @Test + public void testCreateMultipleParentFolders() throws Exception + { + largeHomeFolderProvider.setPattern("^(.?)(.?)(.?)(.?)(.?)"); + + createUser("", "fred"); + createUser("", "peter"); + createUser("", "tess"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "f/r/e/d/fred"); + assertHomeFolderLocation("peter", "p/e/t/e/r/peter"); + assertHomeFolderLocation("tess", "t/e/s/s/tess"); + } + + @Test + public void testMoveToRoot() throws Exception + { + // i.e. there are no parent folders after the sync + largeHomeFolderProvider.setPattern(""); + + createUser("fr", "fred"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fred"); + } + + @Test + public void testRemoveEmptyParents() throws Exception + { + createUser("a/bb/ccc", "peter"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("peter", "pe/peter"); + assertFalse("Expected the empty parent 'a' to have been removed.", exists("a")); + } + + @Test + public void testKeepEmptyParents() throws Exception + { + createUser("a/bb/ccc", "peter"); + properties.put("home_folder_provider_synchronizer.keep_empty_parents", "true"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("peter", "pe/peter"); + assertTrue("Expected the empty parent 'a/bb/ccc' to still exist as global " + + "property was set.", exists("a/bb/ccc")); + } + + @Test + public void testKeepNonEmptyParents() throws Exception + { + createUser("a/bb/ccc", "peter"); + createFolder("a/bb/ddd"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("peter", "pe/peter"); + assertFalse("Expected the empty parent 'a/bb/ccc' to have been removed.", exists("a/bb/ccc")); + assertTrue("Expected the non empty parent 'a/bb' to have been kept.", exists("a/bb/ddd")); + } + + @Test + public void testPathAlreadyInUseByFolder() throws Exception + { + createUser("", "fred"); + createFolder("fr"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fr/fred"); + } + + @Test + public void testPathAlreadyInUseByContent() throws Exception + { + createUser("", "fred"); + createContent("", "fr"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fred"); // unchanged + assertFalse("Did not expect there to be a folder in the prefered location.", exists("fr/fred")); + assertTrue("Expected the content to still exist.", exists("fr")); + } + + @Test + public void testPathInUseByUser() throws Exception + { + // i.e. test clash between home folder names and parent folders + // which requires a temporary folder to be created + createUser("", "fr"); + createUser("", "fred"); + createUser("", "peter"); + createUser("", "pe"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fr", "fr/fr"); + assertHomeFolderLocation("fred", "fr/fred"); + assertHomeFolderLocation("peter", "pe/peter"); + assertHomeFolderLocation("pe", "pe/pe"); + + assertFalse("The Temporary1 folder should have been removed", exists("Temporary1")); + } + + @Test + public void testUseFirstAvailableTemporaryFolder() throws Exception + { + createUser("", "fr"); + createUser("", "fred"); + createFolder("Temporary1"); + createFolder("Temporary2"); + createFolder("Temporary3"); + + // Don't delete the temporary folder + properties.put("home_folder_provider_synchronizer.keep_empty_parents", "true"); + + moveUserHomeFolders(); + + assertTrue("The existing Temporary1 folder should still exist", exists("Temporary1")); + assertTrue("The existing Temporary2 folder should still exist", exists("Temporary2")); + assertTrue("The existing Temporary3 folder should still exist", exists("Temporary3")); + assertTrue("The existing Temporary4 folder should still exist", exists("Temporary4")); + } + + @Test + public void testException() throws Exception + { + // Force the need for a temporary folder + createUser("", "fr"); + createUser("", "fred"); + + // Use up all possible temporary folder names + for (int i=1; i<=100; i++) + { + createFolder("Temporary"+i); + } + + moveUserHomeFolders(); + + // normally would have changed to fr/fred if there had not been an exception + assertHomeFolderLocation("fred", "fred"); + } + + @Test + public void testMultipleRoots() throws Exception + { + createFolder("root"); + String rootPath = origRootPath + "/cm:root"; + testHomeFolderProvider.setRootPath(rootPath); + + createUser("a/b/c", "tess", testHomeFolderProviderName, true); + createUser("a/b/c", "fred", largeHomeFolderProviderName, true); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fr/fred"); + assertHomeFolderLocation("tess", "root/a/b/c/tess"); + } + + @Test + public void testPathNotUnderRoot() throws Exception + { + createUser("a/b/c", "fred"); + + createFolder("root"); + String rootPath = origRootPath + "/cm:root"; + largeHomeFolderProvider.setRootPath(rootPath); + + assertHomeFolderLocation("fred", null); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fr/fred"); + } + + @Test + public void testMultipleUsers() throws Exception + { + // Tried 2000 users and the HomeFolderProviderSynchronizer.onBootstrap(null) + // took 33 seconds. The setup and tear down takes a while too. + + // Use a value larger than the batch size of 20 and log every 100. + int userCount = 110; + for (int i=1; i<=userCount; i++) + { + String name = "f"+i+"red"; + createUser("", name); + } + + moveUserHomeFolders(); + + for (int i=1; i<=userCount; i++) + { + String name = "f"+i+"red"; + assertHomeFolderLocation(name, name.substring(0,2)+'/'+name); + } + } + + @Test + public void testOverrideProvider() throws Exception + { + NodeRef person = createUser("a/b/c", "fred"); + moveUserHomeFolders(); + assertHomeFolderLocation("fred", "fr/fred"); + + properties.put("home_folder_provider_synchronizer.override_provider", + testHomeFolderProviderName); + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "a/b/c/fred"); + String providerName = (String) nodeService.getProperty(person, + ContentModel.PROP_HOME_FOLDER_PROVIDER); + assertEquals(testHomeFolderProviderName , providerName); + } + + @Test + public void testNoOriginalProvider() throws Exception + { + createUser("a/b/c", "fred", null, true); + properties.put("home_folder_provider_synchronizer.override_provider", + largeHomeFolderProviderName); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fr/fred"); // unchanged + assertTrue("Expected the empty parent 'a/b/c' to still exist as original " + + "root was unknown, because the original home folder provider was not set.", + exists("a/b/c")); + } + + @Test + @SuppressWarnings("deprecation") + public void testVersion1HomeFolderProvider() throws Exception + { + // Should just log a message to say it can't do anything + final String name = "v1Provider"; + HomeFolderProvider v1Provider = new HomeFolderProvider() + { + @Override + public void onCreateNode(ChildAssociationRef childAssocRef) + { + } + + @Override + public String getName() + { + return name; + } + }; + homeFolderManager.addProvider(v1Provider); + + createUser("a/b/c", "fred"); + + properties.put("home_folder_provider_synchronizer.override_provider", name); + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "a/b/c/fred"); + } + + @Test + @SuppressWarnings("deprecation") + public void testExtendsAbstractHomeFolderProvider() throws Exception + { + // Should work through the V2Adaptor + final String name = "v1Provider"; + AbstractHomeFolderProvider v1Provider = new UIDBasedHomeFolderProvider(); + v1Provider.setBeanName(name); + v1Provider.setHomeFolderManager(homeFolderManager); + v1Provider.setOnCreatePermissionsManager(largeHomeFolderProvider.getOnCreatePermissionsManager()); + v1Provider.setOnReferencePermissionsManager(largeHomeFolderProvider.getOnReferencePermissionsManager()); + v1Provider.setOwnerOnCreate(largeHomeFolderProvider.getOwner()); + v1Provider.setPath(largeHomeFolderProvider.getRootPath()); + v1Provider.setServiceRegistry(serviceRegistry); + v1Provider.setStoreUrl(largeHomeFolderProvider.getStoreUrl()); + v1Provider.setTenantService(tenantService); + v1Provider.afterPropertiesSet(); + + createUser("a/b/c", "fred"); + + properties.put("home_folder_provider_synchronizer.override_provider", name); + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fred"); + } + + @Test + public void testTenantService() throws Exception + { + // Only test if running multi-tenant + if (tenantAdminService.isEnabled()) + { + long time = System.currentTimeMillis(); + final String tenant1 = time+".tenant1"; + final String tenant2 = time+".tenant2"; + + createTenant(tenant1); + createTenant(tenant2); + + createUser("", "fred"); + createUser(tenant1, "", "fred"); + createUser(tenant2, "", "fred"); + + moveUserHomeFolders(); + + assertHomeFolderLocation("fred", "fr/fred"); + assertHomeFolderLocation(tenant1, "fred", "fr/"+tenantService.getDomainUser("fred", tenant1)); + assertHomeFolderLocation(tenant2, "fred", "fr/"+tenantService.getDomainUser("fred", tenant2)); + } + } +} diff --git a/source/java/org/alfresco/repo/security/person/RegexHomeFolderProvider.java b/source/java/org/alfresco/repo/security/person/RegexHomeFolderProvider.java new file mode 100644 index 0000000000..72159da171 --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/RegexHomeFolderProvider.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.security.person; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.FileNameValidator; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation that returns a tree structure for a home folder based on a property (typically userName) + * from the supplied person. The parent folder names are derived from regular expression groups matched + * against the property value. The final folder name is the full property value.

+ * + * For example, given the value "adavis" and the regular expression "^(..)" the + * resulting home folder path would be {@code "/ad/adavis"}. However with the regular expression + * "^(.)(.?)" the home folder path would be {@code "/a/d/adavis"}. If any group matches a zero + * length string, it is just ignored.

+ * + * Note: In order to choose an efficient distribution scheme, be aware that, when m users are + * distributed into n leaf folders, when m >> n log n the statistical maximum load is + * m/n + O( sqrt((m log n)/n)), w.h.p + * + * @author Romain Guinot, Alan Davis + */ +public class RegexHomeFolderProvider extends UsernameHomeFolderProvider +{ + private static Log logger = LogFactory.getLog(RegexHomeFolderProvider.class); + + private QName propertyName; + private Pattern pattern; + private List groupOrder; + + /** + * @param propertyName String the cm:person property used as the key, such as userName + * or organizationId. + */ + public void setPropertyName(String propertyName) + { + this.propertyName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, propertyName); + } + + /** + * @param patternString the regex pattern against the cm:person property value. Regex + * groups define the parent folder structure. + */ + public void setPattern(String patternString) + { + pattern = getPattern(patternString); + } + + /** + * @param groupOrderString String the order (as a comma separated list) in which the + * regex pattern groups should be assembled into folders (such as {@code 2,1}). + * The default ordering is as they appear. + */ + public void setGroupOrder(String groupOrderString) + { + groupOrder = getGroupOrder(groupOrderString); + } + + private Pattern getPattern(String patternString) + { + if (patternString == null || patternString.trim().length() == 0) + return null; + + Pattern pattern; + try + { + pattern = Pattern.compile(patternString); + logger.debug("Successfully compiled patternString : " + patternString); + } catch (PatternSyntaxException pse) + { + throw new PersonException("Pattern string :" + patternString + " does not compile", pse); + } + return pattern; + } + + private List getGroupOrder(String groupOrderString) + { + if (groupOrderString == null || groupOrderString.trim().length() == 0) + return Collections.emptyList(); + + String[] groupOrderStrings = groupOrderString.split(","); + ListgroupOrder = new ArrayList(groupOrderStrings.length); + for (String group : groupOrderStrings) + { + Integer i; + try + { + i = Integer.valueOf(group); + } + catch (NumberFormatException nfe) + { + throw new PersonException("groupOrdering value " + groupOrderString + " is invalid.", nfe); + } + if (groupOrder.contains(i) || i < 0) + { + throw new PersonException("groupOrdering value " + groupOrderString + " is invalid."); + } + groupOrder.add(i); + } + return groupOrder; + } + + @Override + public List getHomeFolderPath(NodeRef person) + { + List path = new ArrayList(); + String key = FileNameValidator.getValidFileName( + getHomeFolderManager().getPersonProperty(person, propertyName)); + if (pattern != null) + { + Matcher matcher = pattern.matcher(key); + + if (matcher.find()) + { + int groupCount = matcher.groupCount(); + if (!groupOrder.isEmpty()) + { + for (int group : groupOrder) + { + if (group > groupCount) + { + throw new PersonException("groupOrdering value " + + group + " is out of range."); + } + addFolderToPath(path, matcher, group); + } + } + else // "natural" group ordering, i.e as they appear in the regex + { + for (int group = 1; group <= groupCount; group++) + { + addFolderToPath(path, matcher, group); + } + } + } + } + path.add(key); + + if (logger.isDebugEnabled()) + { + logger.debug("returning "+path+" for key: "+key); + } + + return path; + } + + private void addFolderToPath(List path, Matcher matcher, int group) + { + String folder = matcher.group(group); + if (folder.length() > 0) + { + path.add(folder); + } + } +} diff --git a/source/java/org/alfresco/repo/security/person/UIDBasedHomeFolderProvider.java b/source/java/org/alfresco/repo/security/person/UIDBasedHomeFolderProvider.java index b1dd702095..f3bd0dcfc6 100644 --- a/source/java/org/alfresco/repo/security/person/UIDBasedHomeFolderProvider.java +++ b/source/java/org/alfresco/repo/security/person/UIDBasedHomeFolderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,19 +18,18 @@ */ package org.alfresco.repo.security.person; +import java.util.ArrayList; +import java.util.List; + import org.alfresco.model.ContentModel; -import org.alfresco.service.cmr.model.FileFolderService; -import org.alfresco.service.cmr.model.FileInfo; -import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.util.FileNameValidator; /** - * Create home spaces based on the UID of the user. + * Creates home folders directly under the root path, based on the username of the user. * - * If a suitable space is found it is reused, if not it will be made. + * @deprecated + * Depreciated since 4.0. {@link UsernameHomeFolderProvider} should now be used. * * @author Andy Hind */ @@ -40,86 +39,30 @@ public class UIDBasedHomeFolderProvider extends ExistingPathBasedHomeFolderProvi private NodeRef templateNodeRef; - public UIDBasedHomeFolderProvider() - { - super(); - } - public void setTemplatePath(String templatePath) { this.templatePath = templatePath; } - protected HomeSpaceNodeRef getHomeFolder(NodeRef person) - { - FileFolderService fileFolderService = getServiceRegistry().getFileFolderService(); - NodeService nodeService = getServiceRegistry().getNodeService(); - - NodeRef existingHomeFolder = DefaultTypeConverter.INSTANCE.convert( - NodeRef.class, nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER)); - if (existingHomeFolder == null) - { - String uid = DefaultTypeConverter.INSTANCE.convert( - String.class, - nodeService.getProperty(person, ContentModel.PROP_USERNAME)); - - if((uid == null) || (uid.length() == 0)) - { - throw new PersonException("Can not create a home space when the uid is null or empty"); - } - - // ETHREEOH-1612: Convert the username to file- and folder-safe names - String homeFolderName = FileNameValidator.getValidFileName(uid); - - FileInfo fileInfo; - - // Test if it already exists - - NodeRef exising = fileFolderService.searchSimple(getPathNodeRef(), homeFolderName); - if (exising != null) - { - fileInfo = fileFolderService.getFileInfo(exising); - } - else - { - if (templatePath == null) - { - fileInfo = fileFolderService.create( - getPathNodeRef(), - homeFolderName, - ContentModel.TYPE_FOLDER); - } - else - { - try - { - fileInfo = fileFolderService.copy( - getTemplateNodeRef(), - getPathNodeRef(), - homeFolderName); - } - catch (FileNotFoundException e) - { - throw new PersonException("Invalid template to create home space"); - } - } - } - NodeRef homeFolderNodeRef = fileInfo.getNodeRef(); - return new HomeSpaceNodeRef(homeFolderNodeRef, HomeSpaceNodeRef.Status.CREATED); - } - else - { - return new HomeSpaceNodeRef(existingHomeFolder, HomeSpaceNodeRef.Status.VALID); - } - } - protected synchronized NodeRef getTemplateNodeRef() { - if (templateNodeRef == null) + if (templateNodeRef == null && templatePath != null) { templateNodeRef = resolvePath(templatePath); } return templateNodeRef; } + public List getHomeFolderPath(NodeRef person) + { + List path = new ArrayList(1); + path.add(FileNameValidator.getValidFileName( + getHomeFolderManager().getPersonProperty(person, ContentModel.PROP_USERNAME))); + return path; + } + + protected HomeSpaceNodeRef getHomeFolder(NodeRef person) + { + return getHomeFolderManager().getHomeFolder(getV2Adaptor(), person, false); + } } diff --git a/source/java/org/alfresco/repo/security/person/UsernameHomeFolderProvider.java b/source/java/org/alfresco/repo/security/person/UsernameHomeFolderProvider.java new file mode 100644 index 0000000000..55be54cebb --- /dev/null +++ b/source/java/org/alfresco/repo/security/person/UsernameHomeFolderProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.security.person; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.FileNameValidator; + +/** + * Creates home folders directly under the root path, based on the username of the user. + * + * @author Alan Davis (based on UIDBasedHomeFolderProvider) + */ +public class UsernameHomeFolderProvider extends AbstractHomeFolderProvider2 +{ + private String templatePath; + + private NodeRef templateNodeRef; + + public void setTemplatePath(String templatePath) + { + this.templatePath = templatePath; + } + + public synchronized NodeRef getTemplateNodeRef() + { + if (templateNodeRef == null && templatePath != null) + { + templateNodeRef = getHomeFolderManager().resolvePath(this, templatePath); + } + return templateNodeRef; + } + + public List getHomeFolderPath(NodeRef person) + { + List path = new ArrayList(1); + path.add(FileNameValidator.getValidFileName( + getHomeFolderManager().getPersonProperty(person, ContentModel.PROP_USERNAME))); + return path; + } + + public HomeSpaceNodeRef getHomeFolder(NodeRef person) + { + return getHomeFolderManager().getHomeFolder(this, person, false); + } +} diff --git a/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoAssignment.java b/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoAssignment.java index d824eee8ee..bee49a070e 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoAssignment.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoAssignment.java @@ -87,7 +87,7 @@ public class AlfrescoAssignment extends JBPMSpringAssignmentHandler if (actorValStr.startsWith("#{")) { String expression = actorValStr.substring(2, actorValStr.length() -1); - Object eval = AlfrescoJavaScript.executeScript(executionContext, services, expression, null); + Object eval = AlfrescoJavaScript.executeScript(executionContext, services, expression, null, null); if (eval == null) { throw new WorkflowException("actor expression '" + actorValStr + "' evaluates to null"); @@ -128,7 +128,7 @@ public class AlfrescoAssignment extends JBPMSpringAssignmentHandler if (pooledactorValStr.startsWith("#{")) { String expression = pooledactorValStr.substring(2, pooledactorValStr.length() -1); - Object eval = AlfrescoJavaScript.executeScript(executionContext, services, expression, null); + Object eval = AlfrescoJavaScript.executeScript(executionContext, services, expression, null, null); if (eval == null) { throw new WorkflowException("pooledactors expression '" + pooledactorValStr + "' evaluates to null"); diff --git a/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScript.java b/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScript.java index 1c20075093..f44f26a282 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScript.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScript.java @@ -20,16 +20,17 @@ package org.alfresco.repo.workflow.jbpm; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.Repository; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.ScriptService; import org.alfresco.service.cmr.workflow.WorkflowException; import org.dom4j.Element; @@ -65,16 +66,19 @@ public class AlfrescoJavaScript extends JBPMSpringActionHandler private static JpdlXmlReader jpdlReader = new JpdlXmlReader((InputSource)null); private ServiceRegistry services; + private NodeRef companyHome; private Element script; private String runas; - /* (non-Javadoc) - * @see org.alfresco.repo.workflow.jbpm.JBPMSpringActionHandler#initialiseHandler(org.springframework.beans.factory.BeanFactory) + /** + * {@inheritDoc} */ @Override protected void initialiseHandler(BeanFactory factory) { - services = (ServiceRegistry)factory.getBean(ServiceRegistry.SERVICE_REGISTRY); + this.services = (ServiceRegistry)factory.getBean(ServiceRegistry.SERVICE_REGISTRY); + Repository repositoryHelper = (Repository)factory.getBean("repositoryHelper"); + this.companyHome = repositoryHelper.getCompanyHome(); } /* (non-Javadoc) @@ -122,7 +126,7 @@ public class AlfrescoJavaScript extends JBPMSpringActionHandler String user = AuthenticationUtil.getFullyAuthenticatedUser(); if (runas == null && user !=null) { - return executeScript(executionContext, services, expression, variableAccesses); + return executeScript(executionContext, services, expression, variableAccesses, companyHome); } else { @@ -134,21 +138,20 @@ public class AlfrescoJavaScript extends JBPMSpringActionHandler { validateRunAsUser(); } - return executeScriptAs(runAsUser, expression, executionContext, services, variableAccesses); + return executeScriptAs(runAsUser, expression, executionContext, variableAccesses); } } - private static Object executeScriptAs(String runAsUser, + private Object executeScriptAs(String runAsUser, final String expression, final ExecutionContext executionContext, - final ServiceRegistry services, final List variableAccesses) { // execute as specified runAsUser return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { public Object doWork() throws Exception { - return executeScript(executionContext, services, expression, variableAccesses); + return executeScript(executionContext, services, expression, variableAccesses,companyHome); } }, runAsUser); } @@ -252,11 +255,12 @@ public class AlfrescoJavaScript extends JBPMSpringActionHandler * @param services Alfresco service registry * @param expression script to execute * @param variableAccesses (optional) list of jBPM variables to map into script (all, if not supplied) + * @param companyHome TODO * @return script result */ - public static Object executeScript(ExecutionContext context, ServiceRegistry services, String expression, List variableAccesses) + public static Object executeScript(ExecutionContext context, ServiceRegistry services, String expression, List variableAccesses, NodeRef companyHome) { - Map inputMap = createInputMap(context, services, variableAccesses); + Map inputMap = createInputMap(services, companyHome, context, variableAccesses); ScriptService scriptService = services.getScriptService(); scriptService.buildCoreModel(inputMap); Object result = scriptService.executeScriptString(expression, inputMap); @@ -329,26 +333,25 @@ public class AlfrescoJavaScript extends JBPMSpringActionHandler * Construct map of arguments to pass to script * * Based on the elements of the action configuration. - * + * @param companyHome TODO * @param executionContext the execution context * @param variableAccesses the variable configuration + * * @return the map of script arguments */ - private static Map createInputMap(ExecutionContext executionContext, ServiceRegistry services, List variableAccesses) + private static Map createInputMap(ServiceRegistry services, NodeRef companyHome, ExecutionContext executionContext, List variableAccesses) { - Map inputMap = new HashMap(); + ScriptService scriptService = services.getScriptService(); // initialise global script variables - JBPMNode personNode = getPersonNode(executionContext, services); - if (personNode != null) + NodeRef person = getPersonNode(services); + NodeRef userHome = null; + if (person != null) { - inputMap.put("person", personNode ); - NodeRef homeSpace = (NodeRef)services.getNodeService().getProperty(personNode.getNodeRef(), ContentModel.PROP_HOMEFOLDER); - if (homeSpace != null) - { - inputMap.put("userhome", new JBPMNode(homeSpace, services)); - } + NodeService nodeService = services.getNodeService(); + userHome = (NodeRef)nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER); } + Map inputMap = scriptService.buildDefaultModel(person, companyHome, userHome, null, null, null); // initialise process variables Token token = executionContext.getToken(); @@ -405,15 +408,12 @@ public class AlfrescoJavaScript extends JBPMSpringActionHandler } - private static JBPMNode getPersonNode(ExecutionContext executionContext, ServiceRegistry services) { + private static NodeRef getPersonNode(ServiceRegistry services) { String userName = AuthenticationUtil.getFullyAuthenticatedUser(); if(userName != null) { NodeRef person = services.getPersonService().getPerson(userName); - if(person !=null) - { - return new JBPMNode(person, services); - } + return person; } return null; } diff --git a/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScriptIntegrationTest.java b/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScriptIntegrationTest.java index fcf05711c0..a8b95a99f5 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScriptIntegrationTest.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/AlfrescoJavaScriptIntegrationTest.java @@ -1,12 +1,16 @@ package org.alfresco.repo.workflow.jbpm; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.Serializable; import java.util.HashMap; import org.alfresco.model.ContentModel; +import org.alfresco.repo.jscript.ScriptNode; import org.alfresco.repo.model.Repository; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.scripts.ScriptException; @@ -30,6 +34,8 @@ import org.jbpm.context.exe.ContextInstance; import org.jbpm.graph.exe.ExecutionContext; import org.jbpm.graph.exe.Token; import org.jbpm.taskmgmt.exe.TaskInstance; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; public class AlfrescoJavaScriptIntegrationTest extends BaseAlfrescoSpringTest { @@ -193,6 +199,39 @@ public class AlfrescoJavaScriptIntegrationTest extends BaseAlfrescoSpringTest assertEquals(docLibB, nodeService.getPrimaryParent(doc).getParentRef()); } + public void testScopeVariables() throws Exception + { + String admin = AuthenticationUtil.getAdminUserName(); + AuthenticationUtil.setFullyAuthenticatedUser(admin); + NodeRef person = personService.getPerson(admin); + Serializable userHome = nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER); + + AlfrescoJavaScript scriptHandler = new AlfrescoJavaScript(); + String key = "result"; + + // Check person node set. + Element script = buildScript("executionContext.setVariable('" + key + "', person)"); + scriptHandler.setScript(script); + scriptHandler.execute(context); + ScriptNode value = (ScriptNode) variables.get(key); + assertEquals(person, value.getNodeRef()); + + // Check user home set. + script = buildScript("executionContext.setVariable('" + key + "', userhome)"); + scriptHandler.setScript(script); + scriptHandler.execute(context); + value = (ScriptNode) variables.get(key); + assertEquals(userHome, value.getNodeRef()); + + // Check company home set. + NodeRef companyHome = repository.getCompanyHome(); + script = buildScript("executionContext.setVariable('" + key + "', companyhome)"); + scriptHandler.setScript(script); + scriptHandler.execute(context); + value = (ScriptNode) variables.get(key); + assertEquals(companyHome, value.getNodeRef()); + } + private Element buildScript(String expression) { Element script = DocumentHelper.createElement("script"); script.setText(expression); @@ -203,7 +242,8 @@ public class AlfrescoJavaScriptIntegrationTest extends BaseAlfrescoSpringTest @SuppressWarnings("deprecation") protected void onSetUp() throws Exception { super.onSetUp(); - services = (ServiceRegistry) applicationContext.getBean("ServiceRegistry"); + + this.services = (ServiceRegistry) applicationContext.getBean("ServiceRegistry"); repository = (Repository) applicationContext.getBean("repositoryHelper"); personService = services.getPersonService(); createUser(BASIC_USER); @@ -214,7 +254,25 @@ public class AlfrescoJavaScriptIntegrationTest extends BaseAlfrescoSpringTest when(context.getContextInstance()).thenReturn(contextInstance); variables = new HashMap(); when(contextInstance.getVariables()).thenReturn(variables); - when(contextInstance.getVariables((Token) any())).thenReturn(variables); + when(contextInstance.getVariables( any(Token.class))).thenReturn(variables); + when(context.getVariable(anyString())).thenAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) throws Throwable + { + String key = (String)invocation.getArguments()[0]; + return variables.get(key); + } + }); + doAnswer(new Answer() + { + public Void answer(InvocationOnMock invocation) throws Throwable + { + String key = (String)invocation.getArguments()[0]; + Object value= invocation.getArguments()[1]; + variables.put(key, value); + return null; + } + }).when(context).setVariable(anyString(), any()); } private void createUser(String userName) @@ -236,9 +294,9 @@ public class AlfrescoJavaScriptIntegrationTest extends BaseAlfrescoSpringTest public static class TestUserStore { private String runAsUser; private String fullUser; - private JBPMNode person = null; + private ScriptNode person = null; - public void storeUsers(JBPMNode user) + public void storeUsers(ScriptNode user) { fullUser = AuthenticationUtil.getFullyAuthenticatedUser(); runAsUser = AuthenticationUtil.getRunAsUser(); diff --git a/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java b/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java index b3811c6a6b..1e22a01fb2 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java @@ -148,7 +148,7 @@ public class ForEachFork extends JBPMSpringActionHandler private Collection evaluateForEachExpression(final ExecutionContext executionContext, String forEachText) { String expression = forEachText.substring(2, forEachText.length() -1); - Object result = AlfrescoJavaScript.executeScript(executionContext, services, expression, null); + Object result = AlfrescoJavaScript.executeScript(executionContext, services, expression, null, null); if (result == null) { throw new WorkflowException("forEach expression '" + forEachText + "' evaluates to null"); diff --git a/source/java/org/alfresco/wcm/WCMAspectTest.java b/source/java/org/alfresco/wcm/WCMAspectTest.java new file mode 100644 index 0000000000..244359da5e --- /dev/null +++ b/source/java/org/alfresco/wcm/WCMAspectTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.wcm; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Property; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.wcm.asset.AssetInfo; +import org.alfresco.wcm.asset.AssetService; +import org.alfresco.wcm.sandbox.SandboxInfo; +import org.alfresco.wcm.sandbox.SandboxService; +import org.alfresco.wcm.webproject.WebProjectInfo; +import org.alfresco.wcm.webproject.WebProjectService; + +public class WCMAspectTest extends AbstractWCMServiceImplTest +{ + + private AssetService assetService = null; + private WebProjectService wpService = null; + private SandboxService sbService = null; + private DictionaryDAO dictionaryDAO = null; + + private final static int SIZE = 1000; + private final static String ADMIN = "admin"; + + private static String TEST_TYPE_NAMESPACE = "http://www.alfresco.org/model/testaspectmodel/1.0"; + private static QName TEST_ASPECT_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "Aspect"); + private static QName PROP_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "applications"); + + @Override + protected void setUp() throws Exception + { + super.setUp(); + + wpService = (WebProjectService) ctx.getBean("WebProjectService"); + sbService = (SandboxService) ctx.getBean("SandboxService"); + assetService = (AssetService) ctx.getBean("AssetService"); + dictionaryDAO = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + + } + + public void testAspect() throws Exception + { + try + { + AuthenticationUtil.setFullyAuthenticatedUser(ADMIN); + + WebProjectInfo wpInfo = wpService.createWebProject(TEST_WEBPROJ_DNS + "-aspectSimple", TEST_WEBPROJ_NAME + "-aspectSimple", TEST_WEBPROJ_TITLE, + TEST_WEBPROJ_DESCRIPTION, TEST_WEBPROJ_DEFAULT_WEBAPP, TEST_WEBPROJ_DONT_USE_AS_TEMPLATE, null); + + String wpStoreId = wpInfo.getStoreId(); + String defaultWebApp = wpInfo.getDefaultWebApp(); + + SandboxInfo sbInfo = sbService.getAuthorSandbox(wpStoreId); + String authorSandboxId = sbInfo.getSandboxId(); + + String authorSandboxPath = sbInfo.getSandboxRootPath() + "/" + defaultWebApp; + + assetService.createFile(authorSandboxId, authorSandboxPath, "myFile", null); + + AssetInfo assetInfo = assetService.getAsset(authorSandboxId, authorSandboxPath + "/" + "myFile"); + attachAspect(assetInfo); + checkAspect(assetInfo); + } + finally + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + } + + @SuppressWarnings("unchecked") + private void checkAspect(AssetInfo assetInfo) + { + assertTrue(assetService.hasAspect(assetInfo, TEST_ASPECT_QNAME)); + + Map properties = assetService.getAssetProperties(assetInfo); + + List list = (List) properties.get(PROP_QNAME); + assertEquals(list.size(), SIZE); + } + + private void attachAspect(final AssetInfo assetInfo) + { + M2Model model = M2Model.createModel("custom:custom"); + model.createNamespace(TEST_TYPE_NAMESPACE, "custom"); + model.createImport(NamespaceService.DICTIONARY_MODEL_1_0_URI, NamespaceService.DICTIONARY_MODEL_PREFIX); + model.createImport(NamespaceService.SYSTEM_MODEL_1_0_URI, NamespaceService.SYSTEM_MODEL_PREFIX); + model.createImport(NamespaceService.CONTENT_MODEL_1_0_URI, NamespaceService.CONTENT_MODEL_PREFIX); + + M2Aspect testMandatoryAspect = model.createAspect("custom:" + TEST_ASPECT_QNAME.getLocalName()); + + M2Property prop = testMandatoryAspect.createProperty("custom:" + PROP_QNAME.getLocalName()); + prop.setType("d:" + DataTypeDefinition.TEXT.getLocalName()); + prop.setMultiValued(true); + prop.setIndexed(true); + + dictionaryDAO.putModel(model); + + final Map aspectValues = new HashMap(); + List applications = new ArrayList(); + + for (int i = 0; i < SIZE; i++) + { + applications.add("Adding " + i); + } + + aspectValues.put(PROP_QNAME, (Serializable) applications); + + // takes about 150 milliseconds to commit + transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + + @Override + public Object execute() throws Throwable + { + assetService.addAspect(assetInfo, TEST_ASPECT_QNAME, aspectValues); + return null; + } + + }); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/wcm/WCMTestSuite.java b/source/java/org/alfresco/wcm/WCMTestSuite.java index aa208cf59d..eb758dbe91 100644 --- a/source/java/org/alfresco/wcm/WCMTestSuite.java +++ b/source/java/org/alfresco/wcm/WCMTestSuite.java @@ -43,6 +43,7 @@ public class WCMTestSuite extends TestSuite { TestSuite suite = new TestSuite(); + suite.addTestSuite(WCMAspectTest.class); suite.addTestSuite(WebProjectServiceImplTest.class); suite.addTestSuite(AssetServiceImplTest.class); suite.addTestSuite(SandboxServiceImplTest.class);