mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-07 18:25:23 +00:00
43628: Fix for ALF-16299 - On full user profile, in the 'Sites' tab, the site descriptions are not aligned when a long site description is provided. 43639: ALF-16701: use newly released Spring Surf 1.2.0-M1 rather than a SNAPSHOT 43644: ALF-16527: webscript person.lib.ftl does not include all attributes for users out of the box. - added missing organizationId attribute. 43649: ALF-16756: WebDAV: An error occurs on drag&drop content from local machine to alfresco when inbound move rule configured. 43651: ALF-16756: Fixed typos - I took this code in good faith! 43659: ALF-16006: MT: Document Library is absent after upgrade from 3.4.x to 4.1.x (eg. 3.4.10 -> 4.1.1) - More patch dependencies required 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43675: Merged V4.1 to V4.1-BUG-FIX 43674: Merged PATCHES/V4.1.1 to V4.1 43673: Merged V3.4-BUG-FIX to PATCHES/V4.1.1 43672: ALF-16834, ALF-16833, MNT-187: Fix unit test failures and 'fine tune' logic to handle multiple updates within a single transaction creating a node 43676: ALF-12324: Can't delete site with WQS content - Don't try to add nodes to the publish queue on site deletion 43689: Fix for ALF-14207 - cm:link not correctly handled in Share's doclist when users don't have permission 43690: Fix for ALF-16538 - Wrong label on change group permission message 43696: ALF-16348 (Inconsistent error message when latest manager leaves a site) 43719: ALF-16031: "CMISChangeLog does not log Created events" 43720: ALF-14137: "When calling CMIS getAllVersions method using the OpenCMIS Browser the PreparedStatement is executed multiple times" 43722: ALF-16352: Re-instated manage permissions action to document list view 43745: Fix for ALF-15351. ContentModelFormProcessor had a TODO to handle associations defined on aspects other than those aspects already on the node for which a form is being submitted. I've added code that, when an association is added to a node, will check if the association is defined on any aspect in the system and if it is, it will let the association be created. 43757: ALF-16171: If a password for admin user is specified with a space in the end during the installation Alfresco startup fails - Added validation that will not allow the specified password to contain leading to trailing white space 43760: Fixed ALF-16317 "Labels not displaying full text when creating a rule on a folder with IE8" part 2 43769: Fix for ALF-13461. Merged PATCHES/V3.4.8 to BRANCHES/DEV/V4.1-BUG-FIX: 43344: MNT-162: Merged DEV to PATCHES/V3.4.8 43254: MNT-162: CLONE - Hidden folder "Comments" is available to searching - Removed last '/' from COMMENT_QNAMEPATH constant. 43770: ALF-15616: Merged V3.4-BUG-FIX (3.4.12) to V4.1-BUG-FIX (4.1.3) 43768: Merged Dev to V3.4-BUG-FIX (3.4.12) 43743: ALF-11956: WCM accessibility The problem with absence of initial focus on the first Web form element has been resolved in 'alfresco.xforms.XForm._loadHandler()'. This method creates all controls, defined in the form. 'alfresco.xforms.FocusResolver._findControl()' has been renamed to a public-like method ('alfresco.xforms.FocusResolver.findControl()'). Also it has been modified to introduce a possibility of searching elements, using root XForms widgets container. Some other minor fixes. 43772: Fixed ALF-16497 "Selected Items drop down box is expanded to the right in IE9" 43787: Merged in Philippe's fix for ALF-16313:unmakeTranslation() called on pivot language gives exception 43794: ALF-16155: "Notification digest accumulates and is repeatedly sent if 1 or more notification email fails" 43805: ALF-16212: Fixes double encoding issue. 43835: Fixed ALF-13595: Patches third party library date.js to prevent infinite loop. 43836: ALF-16480: Merged PATCHES/V4.1.1 to V4.1-BUG-FIX 43252: MNT-166: Document lock not removed after the lock expiry date has been reached - Changed evaluator "evaluator.doclib.metadata.isLocked" to use "evaluator.doclib.indicator.nodeLocked" and "evaluator.doclib.indicator.lockOwner" evaluator. 43253: MNT-165: "Cancel Editing" does not completely remove lock from document - Added documentLibrary cancel editing action for locked documents. 43300: MNT-171: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 (modified) 43096: Fix for ALF-16283 - When document is checked out, 'Edit Online' and 'Upload New Version' options should not be visible on the original document. 43311: MNT-165: "Cancel Editing" does not completely remove lock from document - Fix for page refresh problem when cancel editing on details page 43421: MNT-186: 4.1.1.7 HF: Webscipt error on doclib page, containing locked by other users files - Change evaluator.doclib.metadata.isLocked to break circular dependency 43755: MNT-202: Upload New Version not available for a document that has been edited offline - Upload New Version is now available if editable by user (respecting locks, type of checkout, etc). 43844: ALF-16696: Merged DEV to V4.1-BUG-FIX 43734: Share nodebrowser is unable to access node of a document with MS residual properties - Use localname if no prefix is registered for a namespace uri 43864: Fixed ALF-16320 "Properties side panel collapses after editing properties in document preview window with IE8" 43866: Fixed ALF-16320 "Properties side panel collapses after editing properties in document preview window with IE8" part 2 - Checking using YAHOO.util.Event.getTarget instead 43867: Fixed ALF-16276 "'"Web View' dahslet displays 2 scrollbars in IE8." 43872: Merged V4.1 to V4.1-BUG-FIX 43622: ALF-16757: Sharepoint doesn't work correct with SSO - Fix by Pavel 43633: Latest translations from Gloria (r43623) 43636: Merged PATCHES/V4.1.1 to V4.1 43301: ALF-16811 / MNT-173: SOLR tracking spending too long evaluating paths - Too much time was being spent fetching the individual nodes in each path and there could be potentially thousands - Now we traverse all the ancestor parent associations in the cache before switching shared cache reads off, then bulk load them in one shot! 43303: ALF-16812 / MNT-174: "dictionaryDAO.init() called, yet no namespace registry for domain" after node rejoins cluster - namespaceRegistryCache is secondary to the clustered dictionaryRegistryCache, so can be a non-clustered cache. Synchronization and thread locals already in use so still thread safe. - dictionaryDAO.init() now has sole responsibility of establishing the NamespaceRegistry threadlocal within its locks, so no more risk of cyclic dependencies, race conditions, partial initialization, or registries dropping out of the cache - To avoid being confused by the 'consistent read' behaviour of a transactional cache, DictionaryDAOImpl now reads / writes directly to a shared cache. Again locks and thread locals still used so still thread safe 43334: ALF-16812 / MNT-174: Fixed failing unit tests - Because DictionaryDAOImpl now reads directly from the shared cache we need to reset it on initialization to avoid problems in multi-context unit tests 43337: ALF-16811 / MNT-173: Fix test failure. Cope with IDs of deleted nodes in getCachedAncestors() 43356: ALF-16811 / MNT-173: If we are disabling shared cache reads for the transaction, convert all existing reads and updates to avoid 'consistent read' behaviour giving us a potentially out of date node already accessed - Existing read buckets are simply thrown away - Updates are converted to removes to avoid any assumptions about existing shared cache content - New entries are left alone as they haven't come from the shared cache anyway 43410: ALF-16813 / MNT-185: Web Scripts are being endlessly re-registering in clustered environment - Due to 'consistent read' behaviour of transactional cache - As accesses are regulated by RW locks we can read straight through to the shared cache instead 43565: ALF-16814 / MNT-190: Bmlab Solr Node 2 threw unhandled NullPointerException (and possibly made solr unresponsive) - Added missing return statement 43646: ALF-15755: Rationalization of WebDAVMethod.checkNode() 43681: Merged HEAD to V4.1 43656: Fix for ALF-16683 CMIS: cannot navigate to original document was created through CMIS with CHECKED OUT version state. 43698: Latest Russian translations from Gloria 43838: ALF-16875: Merged V4.1-BUG-FIX to V4.1 43836: ALF-16480: Merged PATCHES/V4.1.1 to V4.1-BUG-FIX 43252: MNT-166: Document lock not removed after the lock expiry date has been reached - Changed evaluator "evaluator.doclib.metadata.isLocked" to use "evaluator.doclib.indicator.nodeLocked" and "evaluator.doclib.indicator.lockOwner" evaluator. 43253: MNT-165: "Cancel Editing" does not completely remove lock from document - Added documentLibrary cancel editing action for locked documents. 43300: MNT-171: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 (modified) 43096: Fix for ALF-16283 - When document is checked out, 'Edit Online' and 'Upload New Version' options should not be visible on the original document. 43311: MNT-165: "Cancel Editing" does not completely remove lock from document - Fix for page refresh problem when cancel editing on details page 43421: MNT-186: 4.1.1.7 HF: Webscipt error on doclib page, containing locked by other users files - Change evaluator.doclib.metadata.isLocked to break circular dependency 43755: ALF-16890 / MNT-202: Upload New Version not available for a document that has been edited offline - Upload New Version is now available if editable by user (respecting locks, type of checkout, etc). 43868: Reverse merging r43838 - Merge in wrong direction introducing duplicate fragment into share-documentlibrary-config.xml 43871: ALF-16890: Merged PATCHES/V4.1.1 to V4.1 43755: MNT-202: Upload New Version not available for a document that has been edited offline - Upload New Version is now available if editable by user (respecting locks, type of checkout, etc). 43873: Merged V4.1 to V4.1-BUG-FIX (RECORD ONLY) 43602: ALF-16254: Merged V4.1-BUG-FIX to V4.1 43598: Merged HEAD to BRANCHES/DEV/V4.1-BUG-FIX 41906: ALF-11378: REST API has been modified to return extra information about a user whether s/he belongs to a group or not. 43612: ALF-16598: Merged V4.1-BUG-FIX to V4.1 43252: MNT-166: Document lock not removed after the lock expiry date has been reached - Changed evaluator "evaluator.doclib.metadata.isLocked" to use "evaluator.doclib.indicator.nodeLocked" and "evaluator.doclib.indicator.lockOwner" evaluator. 43254: MNT-165: "Cancel Editing" does not completely remove lock from document - Added documentLibrary cancel editing action for locked documents. 43300: MNT-171: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 (modified) 43096: Fix for ALF-16283 - When document is checked out, 'Edit Online' and 'Upload New Version' options should not be visible on the original document. 43311: MNT-165: "Cancel Editing" does not completely remove lock from document - Fix for page refresh problem when cancel editing on details page 43421: MNT-186: 4.1.1.7 HF: Webscipt error on doclib page, containing locked by other users files - Change evaluator.doclib.metadata.isLocked to break circular dependency 43615: ALF-16794: Merged V4.1-BUG-FIX to V4.1 43478: MNT-181: Now WebDAV will ALWAYS preserve the original metadata and versions of ANY node that is temporarily 'moved out' in ANY kind of 'shuffle' operation - To make the source node temporarily invisible to WebDAV the client specific HIDDEN aspect features are used - WebDAVHelper.isRenameShuffle() method introduced, to parallel ALF-3856 CIFS fix and using similar system.webdav.renameShufflePattern global property to detect the start of a shuffle - WebDAVHelper converted to use proper dependency injection - CopyMethod has become a simple subclass of MoveMethod as all the hidden aspect munging is done by it - DeleteMethod now preserves hidden nodes - PropFindMethod now ignores hidden nodes - Listing methods will hide hidden nodes from WebDAV 43483: MNT-181: Corrected typo 43523: MNT-181: Corrections - WebDAVLockService.unlock() made 'harmless' to call on already-unlocked nodes - Delete method hides rather than deletes versioned nodes and working copes in case it is called by OSX Finder during a 'replace' operation 43524: MNT-181: Correction - PutMethod now 'unhides' hidden nodes and behaves as though it created them 43570: MNT-181: More corrections researched by Valery - Don't treat all moves to temporary locations as copies - just those from non-temporary locations. Avoids initial upload leaving lots of hidden files around. - Only copy the content, not the whole node including aspects to avoid versioning temporary files! - Don't version on changes to sys:clientVisibilityMask - avoids 'double versioning' - Recognize Mac .TemporaryItems folder and ._ files as temporary 43586: MNT-181: Final correction researched by Valery - Corrected system.webdav.renameShufflePattern so that it matches .TemporaryItems folder and ._ files as a full match 43616: ALF-15755: Merged V4.1-BUG-FIX to V4.1 43591: ALF-16772: If the WebDAV path of a document exceeds 255 characters, documents opened in MSOffice cannot be saved back - Interpret null nodeLockToken as not locked. 43629: Merged V4.1-BUG-FIX to V4.1 (4.1.2) 43498: Fix for ALF-16648 - Alfresco Enterprise artifacts in artifacts.alfresco.com do not provide POM files / dependencies declarations: Merged HEAD to V4.1-BUG-FIX (4.1.2) 43380: -- added site content for alfresco-platform-distribution POM 43379: -- added site documentation for alfresco-platform-distribution POM 43378: -- added site documentation for alfresco-platform-distribution POM -- deployed site for 4.2.b Community at https://artifacts.alfresco.com/nexus/content/repositories/alfresco-docs/alfresco-platform-distribution/latest/index.html -- created repository for Enterprise docs and added url in the appropriate edition properties 43273: Use property to define POI version 42966: ALF-14353 - Added platform distribution POM to standard maven-deploy procedure 42965: ALF-14353 - added alfresco-platform-distribution to provide a Maven release descriptor (dependencyManagement) per each Community / Enterprise release -- moved maven-ant-tasks not to be in the runtime lib -- added platform distribution pom in the SDK folder -- updated maven.xml to deploy filter and deploy the appropriate platform-distribution POM per each releae -- in maven.xml moved configure-release and configure-snapshot goals to maven-env-prerequisites -- updated sdk readme to explain the presence of alfresco-platform-distribution POM 42912: -- updated README header on the POM specifying it's NOT usable to build Alfresco -- make a clear reference to the POMs that get deployed by pom-experimental.xml being usable for development 42842: ALF-14353: Fix artifactId alfresco-jlan -> alfresco-jlan-embed 41883: ALF-14353 - fixed multiple Maven build issues. Now mvn clean install -f pom-experimental.xml works fine. Also deployed Spring Surf 1.2.0-SNAPSHOT so proper Surf version is retrieved 41882: added pre-requisites to build POMs successfully with mvn clean install -f pom-experimental.xml 43634: Merged V4.1-BUG-FIX to V4.1 43386: ALF-13091: Prevent bean post processor propagation to child application contexts. Remove Jsr250BeanPostPorcessor from the CXF configuration, to prevent strange interaction with component scanning. 43641: Merged V4.1-BUG-FIX to V4.1 (4.1.2) 43639: ALF-16701: use newly released Spring Surf 1.2.0-M1 in POM files rather than a SNAPSHOT 43645: Merged V4.1-BUG-FIX to V4.1 43644: ALF-16527: webscript person.lib.ftl does not include all attributes for users out of the box. - added missing organizationId attribute. 43660: Merged V4.1-BUG-FIX to V4.1 43659: ALF-16006: MT: Document Library is absent after upgrade from 3.4.x to 4.1.x (eg. 3.4.10 -> 4.1.1) - More patch dependencies required 43669: Merged V4.1-BUG-FIX to V4.1 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43697: Merged V4.1-BUG-FIX to V4.1 43689: Fix for ALF-14207 - cm:link not correctly handled in Share's doclist when users don't have permission 43761: Merged V4.1-BUG-FIX to V4.1 43760: Fixed ALF-16317 "Labels not displaying full text when creating a rule on a folder with IE8" part 2 43796: Merged V4.1-BUG-FIX to V4.1 43795: Fix for ALF-16254 - "Leave Site" behaviour for group based site membership: Blah, Blah, Blah! 43883: Fix for ALF-12711. Separated preparation of email message from sending of email message as described in analysis in JIRA. Did not merge the fix provided as MailActionExecuter.java.diff but reimplemented a fix based on that. 43888: ALF-16781: Merged V3.4-BUG-FIX (3.4.12) to V4.1-BUG-FIX (4.1.3) 43887: ALF-16898 CLONE - Transformation Server history shows incorrect transformation "To" type for images - ImageTransformActionExecuter ("Transform and Copy Image") re-factored to use ContentService rather than hard coded to ImageMagick 43900: Merged V4.1 to V4.1-BUG-FIX 43898: Merged PATCHES/V4.1.1 to V4.1 43708: ALF-16903 / MNT-203: ACL changes not propagated between two non clustered Alfresco instances hitting same Solr and Database - Although SOLRTrackingComponentImpl was not trusting the cache when tracking node transactions, it was still trusting the cache for ACL change sets - This mean that when the SOLR node is using an Alfresco out of the cluster (which we are supposed to support) it could see stale ACLs - Fixed this by using the same strategy we use in AbstractNodeDAO - Now the main acl CRUD cache is set to ignore the shared cache during SOLR tracking - All secondary ACL caches are keyed by ACL ID AND version, so it prevents the possibility of retrieving a stale cached ACL for an old version 43713: ALF-16903 / MNT-203: ACL changes not propagated between two non clustered Alfresco instances hitting same Solr and Database - Fix test failures - deleteAccessControlEntries must 'touch' all the ACLs it affects to keep caches valid - createAccessControlList should return the newly-versioned ACL 43736: ALF-16904 / MNT-204: Index showing no progress on Solr server - SAP's tracking thread got stuck indefinitely waiting on a content response after sending a request, probably due to some misbehaving proxy or balancer - Now we make it possible to recover from this - A new alfresco.socketTimeout parameter is now supported in solrcore.properties - It specifies the number of milliseconds SOLR will wait before giving up waiting for data on an HTTP connection - The default is still zero which means wait indefinitely 43759: ALF-16904 / MNT-204: Fixed compilation error 43899: Merged PATCHES/V4.1.1 to V4.1 (RECORD ONLY) 43667: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43901: Merged V3.4-BUG-FIX to V4.1-BUG-FIX 43571: Merged DEV to V3.4-BUG-FIX 43569: ALF-16222: It's impossible to delete a file/message via IMAP using Microsoft Entourage 2008 in MacOSX 10.8 fixed: UID failed.Existing file or folder error on attempt to delete file if deleted items already contains file with the same name. modified: AttachmentExtractor modified to avoid code dublication. 43599: Fix for ALF-16505 - Discussion topics are sorted in ascending order (oldest first and newest last) when selecting 'All topics' 43610: Merged Dev to V3.4-BUGFIX (3.4.12) 43416: ALF-16470: SPP:Meeting recurent events are displayed incorrect in share calendar in all tabs (Day, Week, Month) Incorrect lucene query to search for events (it was search for events ONLY after fromDate, that gets from browser query. And recursive events, that have startDate before browser's query date, wasn't included into summary query for search for events. Start date is incorrect and is sets two times. Interval for month of end date is to small. Recursive event, that was started in previous month, and ends in current month, isn't included into result of search. For events, that ends on next date after start date, displays (view by month) only start date. 43625: ALF-11817: Cope with incomplete lock token headers from Microsoft-WebDAV-MiniRedir without the enclosing <> by just consuming the whole string 43670: ALF-11817: Prevent auto-hidden dot underscore files from reappearing on a put by checking for a shuffle path before 'unhiding' 43746: Merged DEV to V3.4-BUG-FIX (with corrections) 43692: ALF-16808 Webdav: Two versions of document have been added after the document has been rewritten once more via drag and drop action. 1. Checked whether the current content property is empty. 2. Disabled the versionable aspect. 3. Added the new content to the node. 43763: Fix for ALF-14828 - Incorrect behavior on delete action (WCMQS site) 43771: Fix for ALF-12752 - Custom form appearance parameters not accounted for causing ovverlapped textareas xforms changing their height 43773: Merged DEV to V3.4-BUG-FIX (3.4.12) 42010: ALF-14040: Event start/end time displays incorrect on MS Outlook Calendar and Calendar of created Meeting workspace 1) Send a date for "Site Calendar" dashlet in ISO-8601 format (like in v4.1.1), then after transformation to client's time zone it is displayed correct 2) For "My Calendar" dashlet we need to take into account that if the event is "all day event", then date should be used without time zone transformation 43804: ALF-12326 HomeFolderProviderSynchronizer fails to move any user space which has a rule configured on it 43837: Fixes ALF-12145: Date.js patches merged to 3.4 Merged BRANCHES/DEV/V4.0-BUG-FIX to BRANCHES/DEV/V3.4-BUG-FIX: 36202: ALF-13483: Japanese: Incorrect date handle in a date Input filed Merged BRANCHES/DEV/V4.1-BUG-FIX to BRANCHES/DEV/V3.4-BUG-FIX: 43835: Fixed ALF-13595: Patches third party library date.js to prevent infinite loop. 43839: ALF-16869: Merged PATCHES/V3.4.8 to V3.4-BUG-FIX 43344: MNT-162: Merged DEV to PATCHES/V3.4.8 43254: MNT-162: CLONE - Hidden folder "Comments" is available to searching - Removed last '/' from COMMENT_QNAMEPATH constant. 43902: Merged V3.4-BUG-FIX to V4.1-BUG-FIX (RECORD ONLY) 43177: Merged DEV to V3.4-BUG-FIX 43087: ALF-16474: Records Management groups were not deleted after removing RM site - Delete Records Management groups on ASPECT_RECORDS_MANAGEMENT_ROOT delete - Backport of RM-190 from RM 2.0 43228: ALF-16266: Merged HEAD to V3.4-BUG-FIX 32846: Fixes: ALF-10519: Issues with translation of roles. 31413: Fixes: ALF-10519 - Internationalises the role names for the repo browser's manage permissions page & makes these available to other pages through common.properties 43229: ALF-16266: Fix conflict data left in the properties file. 43624: ALF-11817: Merged PATCHES/V4.0.2 to V3.4-BUG-FIX 43587: Merged BRANCHES/V4.1-BUG-FIX to PATCHES/V4.0.2 42363: ALF-16213: renaming versioned file results in file being deleted. 43478: MNT-181: Now WebDAV will ALWAYS preserve the original metadata and versions of ANY node that is temporarily 'moved out' in ANY kind of 'shuffle' operation - To make the source node temporarily invisible to WebDAV the client specific HIDDEN aspect features are used - WebDAVHelper.isRenameShuffle() method introduced, to parallel ALF-3856 CIFS fix and using similar system.webdav.renameShufflePattern global property to detect the start of a shuffle - WebDAVHelper converted to use proper dependency injection - CopyMethod has become a simple subclass of MoveMethod as all the hidden aspect munging is done by it - DeleteMethod now preserves hidden nodes - PropFindMethod now ignores hidden nodes - Listing methods will hide hidden nodes from WebDAV 43483: MNT-181: Corrected typo 43523: MNT-181: Corrections - WebDAVLockService.unlock() made 'harmless' to call on already-unlocked nodes - Delete method hides rather than deletes versioned nodes and working copies in case it is called by OSX Finder during a 'replace' operation 43524: MNT-181: Correction - PutMethod now 'unhides' hidden nodes and behaves as though it created them 43570: MNT-181: More corrections researched by Valery - Don't treat all moves to temporary locations as copies - just those from non-temporary locations. Avoids initial upload leaving lots of hidden files around. - Only copy the content, not the whole node including aspects to avoid versioning temporary files! - Don't version on changes to sys:clientVisibilityMask - avoids 'double versioning' - Recognize Mac .TemporaryItems folder and ._ files as temporary 43586: MNT-181: Final correction researched by Valery - Corrected system.webdav.renameShufflePattern so that it matches .TemporaryItems folder and ._ files as a full match 43671: ALF-16834: Merged V4.1-BUG-FIX to V3.4-BUG-FIX 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43842: Merged V4.0-BUG-FIX to V3.4-BUG-FIX 33387: ALF-12492 - Email with empty subject sent to Alfresco by SMTP cause Null pointer Exception 43843: ALF-16717: Merged V4.1-BUG-FIX to V3.4-BUG-FIX 43314: ALF-16575 - Email server does not accept email where Subject ends with a period git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@43914 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
1298 lines
49 KiB
Java
1298 lines
49 KiB
Java
/*
|
|
* Copyright (C) 2005-2012 Alfresco Software Limited.
|
|
*
|
|
* This file is part of Alfresco
|
|
*
|
|
* Alfresco is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Alfresco is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.alfresco.repo.security.person;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.TreeSet;
|
|
|
|
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.<p>
|
|
*
|
|
* By default no action is taken unless the the global property
|
|
* {@code home_folder_provider_synchronizer.enabled=true}.<p>
|
|
*
|
|
* 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=<providerBeanName>}.<p>
|
|
*
|
|
* <b>Warning:</b> 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.<p>
|
|
*
|
|
* @author Alan Davis
|
|
*/
|
|
public class HomeFolderProviderSynchronizer extends AbstractLifecycleBean
|
|
{
|
|
private static final Log logger = LogFactory.getLog(HomeFolderProviderSynchronizer.class);
|
|
private static final Log batchLogger = LogFactory.getLog(HomeFolderProviderSynchronizer.class+".batch");
|
|
|
|
private static final String GUEST_HOME_FOLDER_PROVIDER = "guestHomeFolderProvider";
|
|
private static final String BOOTSTRAP_HOME_FOLDER_PROVIDER = "bootstrapHomeFolderProvider";
|
|
|
|
private final TransactionService transactionService;
|
|
private final AuthorityService authorityService;
|
|
private final PersonService personService;
|
|
private final FileFolderService fileFolderService;
|
|
private final NodeService nodeService;
|
|
private final PortableHomeFolderManager homeFolderManager;
|
|
private final TenantAdminService tenantAdminService;
|
|
|
|
private boolean enabled;
|
|
private String overrideHomeFolderProviderName;
|
|
private boolean keepEmptyParents;
|
|
|
|
public HomeFolderProviderSynchronizer(
|
|
TransactionService transactionService,
|
|
AuthorityService authorityService, PersonService personService,
|
|
FileFolderService fileFolderService, NodeService nodeService,
|
|
PortableHomeFolderManager homeFolderManager,
|
|
TenantAdminService tenantAdminService)
|
|
{
|
|
this.transactionService = transactionService;
|
|
this.authorityService = authorityService;
|
|
this.personService = personService;
|
|
this.fileFolderService = fileFolderService;
|
|
this.nodeService = nodeService;
|
|
this.homeFolderManager = homeFolderManager;
|
|
this.tenantAdminService = tenantAdminService;
|
|
}
|
|
|
|
public void setEnabled(String enabled)
|
|
{
|
|
this.enabled = "true".equalsIgnoreCase(enabled);
|
|
}
|
|
|
|
private boolean enabled()
|
|
{
|
|
return enabled;
|
|
}
|
|
|
|
public void setOverrideHomeFolderProviderName(String overrideHomeFolderProviderName)
|
|
{
|
|
this.overrideHomeFolderProviderName = overrideHomeFolderProviderName;
|
|
}
|
|
|
|
private String getOverrideHomeFolderProviderName()
|
|
{
|
|
return overrideHomeFolderProviderName;
|
|
}
|
|
|
|
public void setKeepEmptyParents(String keepEmptyParents)
|
|
{
|
|
this.keepEmptyParents = "true".equalsIgnoreCase(keepEmptyParents);
|
|
}
|
|
|
|
private boolean keepEmptyParents()
|
|
{
|
|
return keepEmptyParents;
|
|
}
|
|
|
|
@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<Tenant> 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.
|
|
*
|
|
* Also needed to change the case of parent folders.
|
|
*/
|
|
|
|
// Using authorities rather than Person objects as they are much lighter
|
|
final Set<String> authorities = getAllAuthoritiesInTxn(systemUserName);
|
|
final ParentFolderStructure parentFolderStructure = new ParentFolderStructure();
|
|
final Map<NodeRef,String> tmpFolders = new HashMap<NodeRef,String>();
|
|
|
|
// 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.isInfoEnabled())
|
|
{
|
|
logger.info(" -- "+
|
|
(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<NodeRef> processor = new BatchProcessor<NodeRef>(
|
|
"HomeFolderProviderSynchronizer",
|
|
transactionService.getRetryingTransactionHelper(),
|
|
new WorkProvider(authorities),
|
|
threadCount, peoplePerTransaction,
|
|
null,
|
|
batchLogger, 100);
|
|
processor.process(worker, true);
|
|
if (processor.getTotalErrors() > 0)
|
|
{
|
|
logger.info(" -- Give up after error --");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Can only use authorityService.getAllAuthorities(...) in a transaction.
|
|
private Set<String> getAllAuthoritiesInTxn(final String systemUserName)
|
|
{
|
|
return AuthenticationUtil.runAs(new RunAsWork<Set<String>>()
|
|
{
|
|
public Set<String> doWork() throws Exception
|
|
{
|
|
RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
|
|
RetryingTransactionCallback<Set<String>> restoreCallback =
|
|
new RetryingTransactionCallback<Set<String>>()
|
|
{
|
|
public Set<String> execute() throws Exception
|
|
{
|
|
// Returns a sorted set (using natural ordering) rather than a hashCode
|
|
// so that it is more obvious what the order is for processing users.
|
|
Set<String> result = new TreeSet<String>();
|
|
result.addAll(authorityService.getAllAuthorities(AuthorityType.USER));
|
|
return result;
|
|
}
|
|
};
|
|
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<NodeRef,String> 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<String>();
|
|
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.
|
|
String temporary = "Temporary-";
|
|
int from = 1;
|
|
int to = 100;
|
|
for (int i = from; i <= to; i++)
|
|
{
|
|
String tmpFolderName = temporary+i;
|
|
if (fileFolderService.searchSimple(root, tmpFolderName) == null)
|
|
{
|
|
fileFolderService.create(root, tmpFolderName, ContentModel.TYPE_FOLDER);
|
|
return tmpFolderName;
|
|
}
|
|
}
|
|
String msg = "Unable to create a temporary " +
|
|
"folder into which home folders will be moved. " +
|
|
"Tried creating " + temporary + from + " .. " + temporary + to +
|
|
". Remove these folders and try again.";
|
|
logger.error(" # "+msg);
|
|
throw new PersonException(msg);
|
|
}
|
|
}.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.isInfoEnabled())
|
|
{
|
|
logger.info(" # "+toPath(actualPath)+" is already in preferred location.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void handleSharedHomeProvider()
|
|
{
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" # "+userName+" "+providerName+" creates shared home folders - These are not moved.");
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
protected void handleOriginalSharedHomeProvider()
|
|
{
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" # "+userName+" Original "+originalProviderName+" creates shared home folders - These are not moved.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void handleRootOrAbove()
|
|
{
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" # "+userName+" has a home folder that is the provider's root directory (or is above it). " +
|
|
"This is normally for users that origanally had an internal provider or a provider that uses " +
|
|
"shared home folders - These are not moved.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void handleNotAHomeFolderProvider2()
|
|
{
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" # "+userName+" "+providerName+" for is not a HomeFolderProvider2.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void handleSpecialHomeFolderProvider()
|
|
{
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" # "+userName+" Original "+originalProviderName+" is an internal type - These are not moved.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void handleHomeFolderNotSet()
|
|
{
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" # "+userName+" Home folder is not set - ignored");
|
|
}
|
|
}
|
|
}.doWork();
|
|
}
|
|
|
|
/**
|
|
* @return a String for debug a folder list.
|
|
*/
|
|
private String toPath(List<String> folders)
|
|
{
|
|
return toPath(folders, (folders == null) ? 0 : folders.size()-1);
|
|
}
|
|
|
|
private String toPath(List<String> folders, int depth)
|
|
{
|
|
StringBuilder sb = new StringBuilder("");
|
|
if (folders != null)
|
|
{
|
|
if (folders.isEmpty())
|
|
{
|
|
sb.append('.');
|
|
}
|
|
else
|
|
{
|
|
for (String folder : folders)
|
|
{
|
|
if (sb.length() > 0)
|
|
{
|
|
sb.append('/');
|
|
}
|
|
sb.append(folder);
|
|
if (depth-- <= 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.append("<notUnderSameRoot>");
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
private String toPath(NodeRef root, NodeRef leaf)
|
|
{
|
|
StringBuilder sb = new StringBuilder("");
|
|
List<String> path = getRelativePath(root, leaf);
|
|
if (path != null)
|
|
{
|
|
if (path.isEmpty())
|
|
{
|
|
sb.append('.');
|
|
}
|
|
else
|
|
{
|
|
for (String folder : path)
|
|
{
|
|
if (sb.length() > 0)
|
|
{
|
|
sb.append('/');
|
|
}
|
|
sb.append(folder);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.append("<notUnderSameRoot>");
|
|
}
|
|
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. An empty list is returned if the homeFolder is the same as the
|
|
* root or the root is below the homeFolder.
|
|
*/
|
|
private List<String> getRelativePath(NodeRef root, NodeRef homeFolder)
|
|
{
|
|
if (root == null || homeFolder == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (root.equals(homeFolder))
|
|
{
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
Path rootPath = nodeService.getPath(root);
|
|
Path homeFolderPath = nodeService.getPath(homeFolder);
|
|
int rootSize = rootPath.size();
|
|
int homeFolderSize = homeFolderPath.size();
|
|
if (rootSize >= homeFolderSize)
|
|
{
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
// 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<String> path = new ArrayList<String>();
|
|
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<String> preferredPath, NodeRef oldRoot, String providerName,
|
|
String originalProviderName, List<String> 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);
|
|
|
|
// If the preferred home folder already exists, append "-N"
|
|
homeFolderManager.modifyHomeFolderNameIfItExists(root, preferredPath);
|
|
String homeFolderName = preferredPath.get(preferredPath.size() - 1);
|
|
|
|
// Get the old parent before we move anything.
|
|
NodeRef oldParent = nodeService.getPrimaryParent(homeFolder) .getParentRef();
|
|
|
|
// Log action
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" mv "+toPath(actualPath)+
|
|
" "+ toPath(preferredPath)+
|
|
((providerName != null && !providerName.equals(originalProviderName))
|
|
? " # AND reset provider to "+providerName
|
|
: ""));
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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<String> 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);
|
|
String path = toPath(homeFolderPath, i);
|
|
if (nodeRef == null)
|
|
{
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" mkdir "+path);
|
|
}
|
|
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())
|
|
{
|
|
if (logger.isErrorEnabled())
|
|
{
|
|
logger.error(" # cannot create folder " + path +
|
|
" as content with the same name exists. " +
|
|
"Move the content and try again.");
|
|
}
|
|
throw new FileExistsException(parent, path);
|
|
}
|
|
|
|
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.isInfoEnabled())
|
|
{
|
|
logger.info(" rm "+toPath(root, nodeRef));
|
|
}
|
|
nodeService.deleteNode(nodeRef);
|
|
}
|
|
}
|
|
}
|
|
|
|
// BatchProcessWorkProvider returns batches of 100 person objects from lightweight authorities.
|
|
private class WorkProvider implements BatchProcessWorkProvider<NodeRef>
|
|
{
|
|
private static final int BATCH_SIZE = 100;
|
|
|
|
private final VmShutdownListener vmShutdownLister = new VmShutdownListener("getHomeFolderProviderSynchronizerWorkProvider");
|
|
private final Iterator<String> iterator;
|
|
private final int size;
|
|
|
|
public WorkProvider(Set<String> authorities)
|
|
{
|
|
iterator = authorities.iterator();
|
|
size = authorities.size();
|
|
}
|
|
|
|
@Override
|
|
public synchronized int getTotalEstimatedWorkSize()
|
|
{
|
|
return size;
|
|
}
|
|
|
|
@Override
|
|
public synchronized Collection<NodeRef> getNextWork()
|
|
{
|
|
if (vmShutdownLister.isVmShuttingDown())
|
|
{
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
|
|
RetryingTransactionCallback<Collection<NodeRef>> restoreCallback = new RetryingTransactionCallback<Collection<NodeRef>>()
|
|
{
|
|
public Collection<NodeRef> execute() throws Exception
|
|
{
|
|
Collection<NodeRef> results = new ArrayList<NodeRef>(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<NodeRef>
|
|
{
|
|
@Override
|
|
public void beforeProcess() throws Throwable
|
|
{
|
|
AuthenticationUtil.pushAuthentication();
|
|
AuthenticationUtil.setFullyAuthenticatedUser(userName);
|
|
}
|
|
|
|
@Override
|
|
public void afterProcess() throws Throwable
|
|
{
|
|
AuthenticationUtil.popAuthentication();
|
|
}
|
|
|
|
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<Object> runAsWork = new RunAsWork<Object>()
|
|
{
|
|
@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<String> preferredPath;
|
|
protected List<String> actualPath;
|
|
protected NodeRef originalRoot;
|
|
|
|
public HomeFolderHandler(NodeRef person, String overrideProviderName)
|
|
{
|
|
this.person = person;
|
|
this.overrideProviderName =
|
|
(overrideProviderName == null || overrideProviderName.trim().isEmpty())
|
|
? null
|
|
: 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<String> 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 (actualPath != null && actualPath.isEmpty())
|
|
{
|
|
handleRootOrAbove();
|
|
}
|
|
else
|
|
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 handleRootOrAbove()
|
|
{
|
|
}
|
|
|
|
protected void handleNotAHomeFolderProvider2()
|
|
{
|
|
}
|
|
|
|
protected void handleSpecialHomeFolderProvider()
|
|
{
|
|
}
|
|
|
|
protected void handleHomeFolderNotSet()
|
|
{
|
|
}
|
|
}
|
|
|
|
// Records the parents of the preferred folder paths (the leaf folder are not recorded)
|
|
// and checks actual paths against these.
|
|
private class ParentFolderStructure
|
|
{
|
|
// Parent folders within each root node
|
|
private Map<NodeRef, RootFolder> folders = new HashMap<NodeRef, RootFolder>();
|
|
|
|
public void recordParentFolder(NodeRef root, List<String> path)
|
|
{
|
|
RootFolder rootsFolders = getFolders(root);
|
|
synchronized(rootsFolders)
|
|
{
|
|
rootsFolders.add(path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks to see if there is a clash between the preferred paths and the
|
|
* existing folder structure. If there is a clash, the existing home folder
|
|
* (the leaf folder) is moved to a temporary structure. This allows any
|
|
* parent folders to be tidied up (if empty), so that the new preferred
|
|
* structure can be recreated.<p>
|
|
*
|
|
* 1. There is no clash if the path is null or empty.
|
|
*
|
|
* 2. There is a clash if there is a parent structure included the root
|
|
* folder itself.<p>
|
|
*
|
|
* 3. There is a clash if the existing path exists in the parent structure.
|
|
* This comparison ignores case as Alfresco does not allow duplicates
|
|
* regardless of case.<p>
|
|
*
|
|
* 4. There is a clash if any of the folders in the existing path don't
|
|
* match the case of the parent folders.
|
|
*
|
|
* 5. There is a clash there are different case versions of the parent
|
|
* folders themselves or other existing folders.
|
|
*
|
|
* When 4 takes place, we will end up with the first one we try to recreate
|
|
* being used for all.
|
|
*/
|
|
public boolean clash(NodeRef root, List<String> path)
|
|
{
|
|
if (path == null || path.isEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
RootFolder rootsFolders = getFolders(root);
|
|
synchronized(rootsFolders)
|
|
{
|
|
return rootsFolders.clash(path);
|
|
}
|
|
}
|
|
|
|
private RootFolder getFolders(NodeRef root)
|
|
{
|
|
synchronized(folders)
|
|
{
|
|
RootFolder rootsFolders = folders.get(root);
|
|
if (rootsFolders == null)
|
|
{
|
|
rootsFolders = new RootFolder();
|
|
folders.put(root, rootsFolders);
|
|
}
|
|
return rootsFolders;
|
|
}
|
|
}
|
|
|
|
// Records the parents of the preferred folder paths (the leaf folder are not recorded)
|
|
// and checks actual paths against these BUT only for a single root.
|
|
private class RootFolder extends Folder
|
|
{
|
|
private boolean includesRoot;
|
|
|
|
public RootFolder()
|
|
{
|
|
super(null);
|
|
}
|
|
|
|
// Adds a path (but not the leaf folder) if it does not already exist.
|
|
public void add(List<String> path)
|
|
{
|
|
if (!includesRoot)
|
|
{
|
|
int parentSize = path.size() - 1;
|
|
if (parentSize == 0)
|
|
{
|
|
includesRoot = true;
|
|
children = null; // can discard children as all home folders now clash.
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" # Recorded root as parent - no need to record other parents as all home folders will clash");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
add(path, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See description of {@link ParentFolderStructure#clash(NodeRef, List)}.<p>
|
|
*
|
|
* Performs check 2 and then calls {@link Folder#clash(List, int)} to
|
|
* perform 3, 4 and 5.
|
|
*/
|
|
public boolean clash(List<String> path)
|
|
{
|
|
// Checks 2.
|
|
return includesRoot ? false : clash(path, 0);
|
|
}
|
|
}
|
|
|
|
private class Folder
|
|
{
|
|
// Case specific name of first folder added.
|
|
String name;
|
|
|
|
// Indicates if there is another preferred name that used different case.
|
|
boolean duplicateWithDifferentCase;
|
|
|
|
List<Folder> children;
|
|
|
|
public Folder(String name)
|
|
{
|
|
this.name = name;
|
|
}
|
|
|
|
/**
|
|
* Adds a path (but not the leaf folder) if it does not already exist.
|
|
* @param path the full path to add
|
|
* @param depth the current depth into the path starting with 0.
|
|
*/
|
|
protected void add(List<String> path, int depth)
|
|
{
|
|
int parentSize = path.size() - 1;
|
|
String name = path.get(depth);
|
|
Folder child = getChild(name);
|
|
if (child == null)
|
|
{
|
|
child = new Folder(name);
|
|
if (children == null)
|
|
{
|
|
children = new LinkedList<Folder>();
|
|
}
|
|
children.add(child);
|
|
if (logger.isInfoEnabled())
|
|
{
|
|
logger.info(" " + toPath(path, depth));
|
|
}
|
|
}
|
|
else if (!child.name.equals(name))
|
|
{
|
|
child.duplicateWithDifferentCase = true;
|
|
}
|
|
|
|
// Don't add the leaf folder
|
|
if (++depth < parentSize)
|
|
{
|
|
add(path, depth);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See description of {@link ParentFolderStructure#clash(NodeRef, List)}.<p>
|
|
*
|
|
* Performs checks 3, 4 and 5 for a single level and then recursively checks
|
|
* lower levels.
|
|
*/
|
|
protected boolean clash(List<String> path, int depth)
|
|
{
|
|
String name = path.get(depth);
|
|
Folder child = getChild(name); // Uses equalsIgnoreCase
|
|
if (child == null)
|
|
{
|
|
// Negation of check 3.
|
|
return false;
|
|
}
|
|
else if (child.duplicateWithDifferentCase) // if there folders using different case!
|
|
{
|
|
// Check 5.
|
|
return true;
|
|
}
|
|
else if (!child.name.equals(name)) // if the case does not match
|
|
{
|
|
// Check 4.
|
|
child.duplicateWithDifferentCase = true;
|
|
return true;
|
|
}
|
|
|
|
// If a match (including case) has been made to the end of the path
|
|
if (++depth == path.size())
|
|
{
|
|
// Check 3.
|
|
return true;
|
|
}
|
|
|
|
// Check lower levels.
|
|
return clash(path, depth);
|
|
}
|
|
|
|
/**
|
|
* Returns the child folder with the specified name (ignores case).
|
|
*/
|
|
private Folder getChild(String name)
|
|
{
|
|
if (children != null)
|
|
{
|
|
for (Folder child: children)
|
|
{
|
|
if (name.equalsIgnoreCase(child.name))
|
|
{
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|