mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-06-30 18:15:39 +00:00
33116: Allow multiple deferred requests per oplock break, next level of fix for ALF-11935. 33136: Fix for ALF-12200: "Content type is not recognized on creating document from source" 33137: Activities feed generator: change info log messages to debug log messages 33139: ALF-12262 View in Source Repository works incorrect if the folder was created on target side at first 33141: Fix for ALF-12178 "Bulk import - status page shows broken link (Initiate another in-place import)" 33144: Fixes ALF-11119: The 2 tranformations didn't work - Segmentation fault in ffmpeg - it looks like a bug with ffmpeg and it was decided that compile/bug fixing ffmpeg is not a priority at the moment. The 2 entries have been commented out. If they are required, the user should consult the ffmpeg documentation for the correct version & o/s at the time. 33146: ALF-11345 Patch from Pavel Yurkevich to fix another VTI/SPP problem with site names that start with Alfresco 33147: FTP implemented set modification date/time command (MFMT). ALF-12105. 33148: ALF-12063 Pull some of the VTI list type definitions out to a common base class 33150: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33076: ALF-12285: Allow dumping of schema to XML via JMX 33151: Fix problems with FTP and UTF-8. JLAN-81. When using the Java6 Normalizer use the NFC form. 33158: Fix NFS server swallows exceptions. ALF-11667. Startup exception details are now saved. 33160: Improve the Vti/SPP exception message for the case of the Vti port being already in use 33161: ALF-12063 Additional SPP/Vti list info required for Mac Office 2011 support 33162: Fixes: ALF-10322: Edit Dialogue gets out of sync if event moved using FullCalendar (extends FullCalendar to provide a callback after an event changes, which enables us to keep our event object up to date). 33163: Fixes: ALF-10248: Grey Placeholder image for unauthorised channels has now been replaced with yellow one. 33164: Fixes: ALF-11562; Refactors My Calendar dashlet to use timezone aware ISO8601 dates from updated Calendar API & removes obsolete properties from userevents calendar API 33165: Fixes: ALF-10645; i18n label doesn't appear in property bundle. 33167: Fix for ALF-11970 33168: Fix for ALF-10565 "Category manager in admin console needs query not search" - i18n'd the strings 33178: Publishing: Fixes: ALF-11552; Inline edit icon alignment issue fixed 33183: Minor fix to exception string in extendBuffer(). 33194: ALF-10545: NodeServicePolicies#onUpdateNodePolicy not adequate for NodeService#setType * Added beforeSetNodeType and onSetNodeType policies * Both callbacks have old and new types as parameters 33204: Fixes: ALF-11230, publishing tracking link opens in repository, not Doc Lib. Also fixes a couple of other minor bugs: - balloon pop up didn't appear when published from Doc Details page - tracking link appears black (on black) when hovered over. - adds defensive code to prevent an error if the expected element for the notification balloon isn't there. 33212: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33211: ALF-12384: Failed schema dump can cause failure of repository start up 33217: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 31840: Fix for ALF-10282 - Web Browser freezes with large xml files Web form transformation 31987: Proper fix for ALF-11489: 'patch.sitesSpacePermissions' failed on upgrade 2.2.8 -> 3.4.6 - Just handle missing defined ACLs 32341: Fix for ALF-9883 - WCM Forms: Changing 'abstract' type carries previously-added elements 32911: Add a (currently disabled) unit test for ALF-10466 - The HTML to Text transformer (not Tika based) should take account of the content encoding 32912: Merged BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2011_10_13 to BRANCHES/DEV/V3.4-BUG-FIX with changes + unit testing: 31742: ALF-10466 - The HTML to Text converter needs to take account of the Encoding set on the Content Property, to be able to correctly index MBCS text in wiki pages (and others) 32946: ALF-12161: Merged PATCHES/V3.4.5 to V3.4-BUG-FIX 32921: Merged DEV/TEMPORARY to PATCHES/V3.4.5 32913: ALF-11440: Content Manager unable to edit content from another user sandbox In order to allow a Content Manager to edit a locked document in other user's sandbox, it is needed to modify the AVMLockingAwareService.grabLock(). The 'lockState' variable could be set to 'LOCK_OWNER', if a user is ContentManager to bypass the check. 32964: Fixes ALF-11054: Sharepoint - Wrong sorting by date - incorporated patch from investigation team 32967: Merged BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2011_10_13 to BRANCHES/DEV/V3.4-BUG-FIX: 31828: Fixes ALF-10720: Webform performance improvement Minimize database usage by adding new variable to FormWrapper that holds a form's name. 32969: Fixes ALF-10471: Cannot correctly remove users from email notification rule list 32980: Merged DEV/TEMPORARY to V3.4-BUG-FIX 32961: ALF-12132: Set "common-placeholder-configurer" as parent for "lotusWSPlaceholderConfigurer" bean. 32996: ALF-12184: SchemaBootstrap must use same assumptions as PatchServiceImpl when deciding whether an alternative patch succeeded - Fixes regression introduced by r31972 / ALF-11489 33068: Added suggested fix to commit any current transactions in the NFS file expiry thread. ALF-11827. 33077: ALF-10142: Allow TinyMCE to accept <meta> element when editing HTML files inline in Share. 33094: Fix for ACT #15024-37148 (no JIRA yet) - issue where in a load balanced Share environment (multiple web-tiers behind a reverse proxy) the modification to the template layout selection for a site or user dashboard would not be reflected in all servers. 33118: ALF-12278: Prevent the copying over of headers specific to a POST request on to the touch GET request 33138: Upgraded SpringSurf to 1.0.0 rev 968 33140: Added missing json-simple jar to 3rd party eclipse classpath, which bizarrely is used to generate the JUnit cmd line unit test classpath, no really. 33145: Fix for native FTP timestamps returned in GMT timezone format. ALF-11986. 33175: ALF-12366: Cope with read committed DB behaviour in AbstractReindexComponent.reindexTransaction() 33179: ALF-12344 CLONE - Copyright notice shows Alfresco Software, Inc. © 2005-2011 All rights reserved.... should now be to 2012 as that is when we will release 3.4.7 - Being done in 3.4.8 not 3.4.7 33190: Latest SpringSurf libs: - much improved handling of multiple connections and connection reuse in RemoteClient - improves connection reuse generally, but also much more stable under load balancing condition with multiple Share web-tiers behind a reverse proxy 33193: ALF-12344 CLONE - Copyright notice shows Alfresco Software, Inc. © 2005-2011 All rights reserved.... should now be to 2012 as that is when we will release 3.4.7 - Found a few more having followed previous date changes and searches 33203: Fix for HttpClient issue: Error status 500 Unbuffered entity enclosing request can not be repeated. 33206: Fix to FormUIGet - no need to manually patch up the JSON request since rev 33138 (SpringSurf 1.0.0) 33218: Fix for ALF-11868 "CMIS: removeAcl() function doesn't work via atompub." 33220: Merged PATCHES/V3.4.6 to V4.0-BUG-FIX 32405: Merged V3.4.6 (3.4.6.1) to V3.4.1 (3.4.1.24) 32404: ALF-11727 CLONE - Pending Invite Search doesn't return anything if there's more than 1000 pending invites across all sites. Removed read only transaction from invites.get.desc.xml as it broke InviteServiceTest testRejectInvite 32397: ALF-11727 CLONE - Pending Invite Search doesn't return anything if there's more than 1000 pending invites across all sites. Return first 200 invitations (similar to 4.0 paging) Transaction used by the invites.get is now read only so does not force a flush of caches. 32503: Merged PATCHES/V3.4.6 to PATCHES/V3.4.1 32501: ALF-11727: Reinstated read-only transaction around invites.get and prevented it from trying to lazily create persons from rejected invites that had previously been deleted by InviteHelper.cleanUpStaleInviteeResources! 32650: ALF-11872: When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete Put back in cut off at 200 invites (removed in last merge) for UI, unless we know that we need all of them internally 32775: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete Even more changes: - reduce number of queries required to list pending invites to a site (uses moderated and nominated caches and only looks up IDs if possible) - change hibernate cache and flush modes (to avoid cache and the related slow flush), for queries and cancel of workflows - modified js which was making a query for each person in order to work out if they were already in a pending invites list 32838: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - JBPMEngine now supports batch cancelWorkflows() method for canceling multiple workflows at the same time (e.g. on deleting a site) - Manual flushes only used at two points in the batch to minimize dirty checking overhead and yet avoid FK errors - Performance implications still to be checked but at least functionally correct - Corrected JPDL source jar 32857: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - Search for Pending invites was slow on sites with > 0 pending invites (it was getting invites for all sites) 32861: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - JBPMEngine uses an abstract list to 'lazily' convert hibernate objects on demand and avoid batch loading too many objects 32868: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - Rationalization of batch fetching at hibernate layer 32881: ALF-11872: The saga goes on! Corrected empty list handling in InvitationServiceImpl.searchInvitation(). 32927: ALF-11872: Fix parameter validation in InvitationServiceImpl.getInvitationTasks() 32936: ALF-11872: A site with 1200 pending invites can now be deleted without the UI timing out - Pending invitation workflows are cancelled in an asynchronous action - The asynchronous action completes about 3 minutes later, due to the massive number of individual delete statements being run by Hibernate - Creating the rows in the first place took 20 minutes! 32956: ALF-11872: Corrected filtering in InvitationServiceImpl.getInvitationTasks() to only include start tasks - plus recautionary sleep() in InviteServiceTest.tearDown() to ensure asynchronous invite deletions complete 33169: ALF-12312 'org.hibernate.LazyInitializationException: could not initialize proxy - no Session' when clicking on a Pending Invite workflow task in JSF - Follow on from ALF-11872: Only use lazyloaded WorkflowTasks from JBPMEngine.getWorkflowTasks() when we are using the same session (currently only done from InvitationService). The fallback is to assume it is not the same session and return a normal list of Workflows. 33221: Merged V3.4-BUG-FIX to V4.0-BUG-FIX (RECORD ONLY) 30463: L10N Updates from Gloria (based on r30332): Fixes ALF-8211 and new string updates 30473: Merged HEAD to V3.4-BUG-FIX 30468: Fixed ALF-10280: Slow to report ® Duplicate entry ¯ in database. - DuplicateChildNodeNameException implements DoNotRetryException 30685: Merged HEAD to V3.4-BUG-FIX 30679: Unit test for ALF-1017 - Non site content in the Sites Space 30683: ALF-1017 Remove EVERYONE Contributor permissions from /Company Home/Sites/, to avoid misc nodes being created in there by mistake by users, and update the SiteService to runAsSystem when creating the Site node 30693: Merged HEAD to V3.4-BUG-FIX 30692: Fix ALF-1017 specific test following ALF-1017 changes to permissions 30808: Fixes: ALF-10485 (minor text update) 30873: Latest L10N update from Gloria (based on r30698): - Adds Web Quick Start translations (back port from 4.0) - Adds/updates new or previously missing strings 31019: Merged HEAD to V3.4-BUG-FIX 28974: added double-checks for associations when a potential failure is detected 31018: Fixed ALF-9591: Integrity check: Association source multiplicity checking is incorrect - Drop checks for source multiplicity when no associations are pointing to a type/aspect instance 31045: Merged HEAD to V3.4-BUG-FIX 31044: Performance improvements for PATH queries relating to Share dashboard dashlets and document library. - tweaks to generation of PATH queries - hugely improves performance when dealing with 1000's site memberships 31160: Merged HEAD to V3.4-BUG-FIX 31156: Various Share search related fixes as spotted by Andy: - increased resultset size that is used to retrieve raw results from query before Share specific results are filtered - this means sensible results are now shown from large repository wide and sorted queries, previously results would be "missing" if they dropped out of the resultset prefiltering - fix to Share Search component to correctly display if more than N results were found in the repository - fix to add default TYPE clause to generated Share search if no other TYPE is specified - this reduces masses of potentially matches results from repository wide searches that would otherwise need to be post-filtered 31345: Merged HEAD to BRANCHES/DEV/V3.4-BUG-FIX: 31330: Fixed WCM bulkImport's importDirectory 31442: Merged HEAD to V3.4-BUG-FIX 31441: Fixed ALF-11014: Content output stream close errors are absorbed silently - Found while testing XAMcontentStore - Pulled stream copy code into AbstractContentWriter - OutputStream closure (write-side) exception is rethrown to allow full rollback, etc 31444: Merged HEAD to BRANCHES/DEV/V3.4-BUG-FIX: 31383: MLPropertyInterceptor performance improvements: 31758: Merged V3.3 to V3.4-BUG-FIX 31757: ALF-11279: Fixed RetryingTransactionInterceptor so that it actually behaves like an interceptor and doesn't throw away the rest of the interceptor chain! 31798: Merged V3.3 to V3.4-BUG-FIX 31773: ALF-11279: Further RetryingTransactionInterceptor fixes - must do mark for rollback on propagating transactions 32051: ALF-7195: Merge HEAD (4.0) to V3.4-BUG-FIX (3.4.7) Merge was simply to take HEAD version to pick up changes made by Derek to DisableAuditableBehaviourInterceptor 32047: ALF-8882 Edit Online: Modifier and Modified date are changed even no changes were applied - needed to turn off ASPECT_AUDITABLE on removeProperty which is called on unlock - added code to not enable this aspect early if nested calls were made (this is not done, but is safer this way) 32088: Merging HEAD to 3.4-BUG-FIX: r32063: ALF-10947 Fixed issue where repeating JBPM timer was causing an infinite loop if an exception was thrown within the timer event. 32475: ALF-11727 Improved performance of pending invites search. 32512: Incremented version revision for 3.4.8 32917: ALF-12133: Merged HEAD to V3.4-BUG-FIX 32906: ALF-12068 - Zimbra desktop - corrected InternalDateAsString. 32923: ALF-12133: Merged HEAD to V3.4-BUG-FIX 32918: ALF-12133 - Attempt 2 to get Imap internal date correct. 33021: ALF-9878: Merge V3.4.1 (3.4.1.25) to V3.4-BUG-FIX (3.4.8) 32956: ALF-11872: Corrected filtering in InvitationServiceImpl.getInvitationTasks() to only include start tasks - plus recautionary sleep() in InviteServiceTest.tearDown() to ensure asynchronous invite deletions complete 32936: ALF-11872: A site with 1200 pending invites can now be deleted without the UI timing out - Pending invitation workflows are cancelled in an asynchronous action - The asynchronous action completes about 3 minutes later, due to the massive number of individual delete statements being run by Hibernate - Creating the rows in the first place took 20 minutes! 32927: ALF-11872: Fix parameter validation in InvitationServiceImpl.getInvitationTasks() 32881: ALF-11872: The saga goes on! Corrected empty list handling in InvitationServiceImpl.searchInvitation(). 32868: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - Rationalization of batch fetching at hibernate layer 32861: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - JBPMEngine uses an abstract list to 'lazily' convert hibernate objects on demand and avoid batch loading too many objects 32857: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - Search for Pending invites was slow on sites with > 0 pending invites (it was getting invites for all sites) 32838: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - JBPMEngine now supports batch cancelWorkflows() method for canceling multiple workflows at the same time (e.g. on deleting a site) - Manual flushes only used at two points in the batch to minimize dirty checking overhead and yet avoid FK errors - Performance implications still to be checked but at least functionally correct - Corrected JPDL source jar 32775: ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete Even more changes: - reduce number of queries required to list pending invites to a site (uses moderated and nominated caches and only looks up IDs if possible) - change hibernate cache and flush modes (to avoid cache and the related slow flush), for queries and cancel of workflows - modified js which was making a query for each person in order to work out if they were already in a pending invites list 32650: ALF-11872: When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete Put back in cut off at 200 invites (removed in last merge) for UI, unless we know that we need all of them internally 33143: Merged BRANCHES/V3.4-TEAM to BRANCHES/DEV/V3.4-BUG-FIX 25103: ALF-6613 - SpringSurf improvements to allow easier refactoring of Document Details page - removed manual request level caching of remote calls responses in web-tier components - now completely automatic 25138: Flattening of user preferences remote calls - ensures /preferences hits the RequestCachingConnector - reduces no. of remote calls by 3 for the doclib and by 4 for a site dashboard. 33216: Merged PATCHES/V3.4.1 to V3.4-BUG-FIX (3.4.8) 32405: Merged V3.4.6 (3.4.6.1) to V3.4.1 (3.4.1.24) 32404: ALF-9878 / ALF-11727 CLONE - Pending Invite Search doesn't return anything if there's more than 1000 pending invites across all sites. Removed read only transaction from invites.get.desc.xml as it broke InviteServiceTest testRejectInvite 32397: ALF-9878 / ALF-11727 CLONE - Pending Invite Search doesn't return anything if there's more than 1000 pending invites across all sites. Return first 200 invitations (similar to 4.0 paging) Transaction used by the invites.get is now read only so does not force a flush of caches. 32503: Merged PATCHES/V3.4.6 to PATCHES/V3.4.1 32501: ALF-9878 / ALF-11727: Reinstated read-only transaction around invites.get and prevented it from trying to lazily create persons from rejected invites that had previously been deleted by InviteHelper.cleanUpStaleInviteeResources! 32641: ALF-12387 / ALF-11872: Merged V3.4-BUG-FIX (3.4.8) to V3.4.1 (3.4.1.25) 32475: ALF-11727 Improved performance of pending invites search. (N Smith changes to do with only using ONE search value - faster as multiple are taken as ORs rather than ANDs) 32650: ALF-12387 / ALF-11872: When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete Put back in cut off at 200 invites (removed in last merge) for UI, unless we know that we need all of them internally 32775: ALF-12387 / ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete Even more changes: - reduce number of queries required to list pending invites to a site (uses moderated and nominated caches and only looks up IDs if possible) - change hibernate cache and flush modes (to avoid cache and the related slow flush), for queries and cancel of workflows - modified js which was making a query for each person in order to work out if they were already in a pending invites list 32838: ALF-12387 / ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - JBPMEngine now supports batch cancelWorkflows() method for canceling multiple workflows at the same time (e.g. on deleting a site) - Manual flushes only used at two points in the batch to minimize dirty checking overhead and yet avoid FK errors - Performance implications still to be checked but at least functionally correct - Corrected JPDL source jar 32857: ALF-12387 / ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - Search for Pending invites was slow on sites with > 0 pending invites (it was getting invites for all sites) 32861: ALF-12387 / ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - JBPMEngine uses an abstract list to 'lazily' convert hibernate objects on demand and avoid batch loading too many objects 32868: ALF-12387 / ALF-11872 When there are a lot of pending invites, deletion of a site causes high and prolonged CPU activity and can take a long time to complete - Rationalization of batch fetching at hibernate layer 32881: ALF-12387 / ALF-11872: The saga goes on! Corrected empty list handling in InvitationServiceImpl.searchInvitation(). 32927: ALF-12387 / ALF-11872: Fix parameter validation in InvitationServiceImpl.getInvitationTasks() 32936: ALF-12387 / ALF-11872: A site with 1200 pending invites can now be deleted without the UI timing out - Pending invitation workflows are cancelled in an asynchronous action - The asynchronous action completes about 3 minutes later, due to the massive number of individual delete statements being run by Hibernate - Creating the rows in the first place took 20 minutes! 32956: ALF-12387 / ALF-11872: Corrected filtering in InvitationServiceImpl.getInvitationTasks() to only include start tasks - plus recautionary sleep() in InviteServiceTest.tearDown() to ensure asynchronous invite deletions complete 33169: ALF-12382 / ALF-12312 'org.hibernate.LazyInitializationException: could not initialize proxy - no Session' when clicking on a Pending Invite workflow task in JSF - Follow on from ALF-11872: Only use lazyloaded WorkflowTasks from JBPMEngine.getWorkflowTasks() when we are using the same session (currently only done from InvitationService). The fallback is to assume it is not the same session and return a normal list of Workflows. 33224: Fix for ALF-12230 "Bootstrap re-encryption failed" 33225: Fix for ALF-12349 Transformations need to handle failures due to server being busy. Change to system behaviour: We want to support transient failures of thumbnail creations. Primary example now is the Polymorph Transformation Server which can fail transformations because it is under load and wishes to decline transform requests. Prior to this check-in, such failures would be interpreted by Alfresco as 'real' failures and the content node would be marked as FailedToTransform. The transformers' state data would also be affected by the failure. We need to allow transformers to fail and NOT trigger any negative consequences such as above. Changes in code: New exception type ActionServiceTransientException when thrown from an ActionExecuter will NOT trigger any compensating action that has been configured. This allows actions to fail 'normally' or due to some transient condition with the implication that if rerun later the action may succeed. Additional end-state for Actions in the ActionTrackingService: ActionStatus.Declined. The ActionExecuter has declined to execute the action at this time. New exception type ContentServiceTransientException which means that a content operation (currently only a transformation) has failed due to a transient condition. CreateThumbnailActionExecuter catches this exception type and throws the ActionServiceTransientException. Tests at the ActionServiceImpl, ActionTrackingServiceImpl and ThumbnailServiceImpl APIs & various test config & other changes. 33231: ALF-10581 - MySQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails ... fk_alf_cass_cnode - unexpected non-null child node id when creating a new node (see also THOR-906) - for now genericise ALF-10153 (to include at least MySQL in addition to MS SQL Server) 33237: Fixing JUnit jar's src attachment. 33238: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33222: ALF-12351: Separate schema descriptor files for table prefixes: alf_, avm_, JBPM_, act_ 33235: ALF-12351: Separate schema descriptor files for table prefixes: alf_, avm_, JBPM_, act_ 33239: partial fix for: ALF-10446 - Upgraded FullCalendar to support ISO8601 Zulu timezone & told it not to ignore the timezone information. 33240: Implementation of improvement ALF-12404. This fix has been OK'd by Andy Hind. 33246: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33244: ALF-12352: Add JMX support to trigger a schema check manually 33252: Merged V3.4-BUG-FIX to V4.0-BUG-FIX (RECORD ONLY) 33245: ALF-12366: Delete a node by deleting its row and inserting a new one with the deleted flag set - Forces a new ID to be generated for the deleted node - Makes it impossible to simultaneously delete a node and add new children in two concurrent transactions - Can't be merged to 4.0 due to SOLR - needs something more clever! 33250: ALF-12366: Derek code review 33254: Merge V3.4-BUG-FIX to V4.0 BUG-FIX 33249 - ALF-12342 Excel 2003 Patterns. 33256: ALF-12386 Change the simpler projects to use explicit dependencies, rather than blindly importing all of 3rd-party, to make generating downstream Maven POMs easier 33266: ALF-12163 - mail.parameters.from does not use the from address provided 33283: ALF-12185 SPP/Vti Mac Office checkin correction for Collaborators (Patch from Pavel Yurkevich) 33295: Fix to SMTP authentication implementation. 33296: Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/V4.0-BUG-FIX 33285: Fix for ALF-12336 - Share loses performance if noncachableObjectTypes are defined (page & component) - New and improved mechanism for dealing with cache invalidation in Share load balancing and clustering based on the Hazelcast messaging system - See http://www.hazelcast.com/docs.jsp - and specifically http://www.hazelcast.com/docs/1.9.4/manual/single_html/#SpringIntegration - Sample per node cluster config provided in custom-slingshot-application-context.xml.sample - Tested with an Alfresco cluster plus a Share cluster both balanced via Apache server instances - Significantly improved Share stability with latest SpringSurf libs and individual node performance back up to non-clustered node speed Modified Alfresco JLan build to use newer hazelcast JAR and also modified existing build file references to use new jar location. 33297: Merge V3.4-BUG-FIX (3.4.8) to V4.0-BUG-FIX (4.0.1) (RECORD ONLY) 33292: ALF-12457: Merge V3.4.4 (3.4.4.8) to V3.4-BUG-FIX (3.4.8) 33287: ALF-12400: Merged DEV/BELARUS/V3.4-BUG-FIX-2012_01_10 to V3.4.4 (3.4.4.8) 33284: ALF-12400 CLONE - Security risk in Web View dashlet The check for user role was added to Web View and Site Links dashlets to disallow to configure Web View for Consumer, Contributor, and Collaborator roles and disallow to add Site Links for Consumer role. 33286: ALF-9514 I18N: Model constraint values need localized display names - Applied diff file attached to JIRA issue (with a minor compiler error correction). It appeared to be a merge of: DEV/SWIFT r27643, r27645, r27692, r27846 and HEAD r28405, r29364 33298: ALF-12461 If the OOXML file contains a thumbnail image, use this for the document thumbnail, plus improve the iWorks analoguous transformer 33305: Fix for ALF-12463 Error querying database was detected during upgrade process from 3.1 to 4.0.0. 33306: Experiment - adding an sdk-extras target for the enterprise specific bits of the SDK. 33308: Merged V3.4-BUG-FIX (3.4.8) to V4.0-BUG-FIX (4.0.1) 33242: ALF-5830 show_audit.ftl template doesn't work anymore - ISO9075 encode the node path 33271: ALF-9659 In auditing, nodeNameValue extractor never works on a deletion event. - As pre call audit is not discarded on transaction rollback, we need to be able to have access to pre call values in the post call audit application. One such value is the nodeName for the post call of NodeService.deleteNode() 33274: Merged DEV to V3.4-BUG-FIX 33273: ALF-12314: Failed to create content due to error: lockOwner is a mandatory parameter It is necessary to pass the correct 'lockOwner' of a rendition, for example, to use 'rr.getLockOwner()' instead of 'lockOwner' 33277: ALF-12436: Merged PATCHES/V3.4.6 to V3.4-BUG-FIX 33275: ALF-12426: Correction to joins in select_ContentDataByNodeIds - inner join to alf_node_properties first 33279: ALF-12366: Merged PATCHES/V3.4.6 to V3.4-BUG-FIX 33278: ALF-12393: Another indexing race condition when MySQL read committed is enabled, this time in the debug diagnostics! 33282: Merged V3.4 to V3.4-BUG-FIX 32979: ALF-12114: Reverse merged ALF-10282 and related changes from V3.4-BUG-FIX, which have caused serious regressions 31840: Fix for ALF-10282 - Web Browser freezes with large xml files Web form transformation 32341: Fix for ALF-9883 - WCM Forms: Changing 'abstract' type carries previously-added elements 32998: (RECORD ONLY) Merged V3.4-BUG-FIX to V3.4 32996: ALF-12184: SchemaBootstrap must use same assumptions as PatchServiceImpl when deciding whether an alternative patch succeeded - Fixes regression introduced by r31972 / ALF-11489 33084: Merged BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2011_12_06 to V3.4 (3.4.7) 33069: ALF-12266 NPE creating content using the web form Locale language can be passed not only as lang_country but as only country code as well. 33257: ALF-10340: patch.db-V2.2-CleanNodeStatuses must now have patch.db-V2.2-Upgrade-From-2.2SP1 as an alternative to allow upgrade from 2.2.8 - That's because patch.db-V2.2-Upgrade-From-2.2SP1 exists in 2.2.8 and once run it's too late to run patch.db-V2.2-CleanNodeStatuses 33260: Set failonany=true on distribute-installers parallel task so that the build actually fails if an installer fails to build 33301: ALF-12464: Merged PATCHES/V3.4.5 to V3.4-BUG-FIX 33299: ALF-12281: Memory leak in ReferenceCountingReadOnlyIndexReaderFactory - The diagnostic code I added to track memory leaks was actually causing some! - For some reason I was recording a new reference when the index reader was dereferenced rather than clearing it - Would affect scenarios where the main index is long-lived and there are no writes to it and lots of searches - Spotted by Pavel - Too much late night coding! 33303: ALF-12464: Merged PATCHES/V3.4.5 to V3.4-BUG-FIX 33302: ALF-12281: Correction to previous checkin - deal with the initial reference created by the constructor and cleared by closeIfRequired() 33315: ALF-11214 - IMAP subsystem is not successfully restarted after incorrect modification of IMAP properties via Admin Console 33321: Remove /hazelcast lib dir include which is no longer required as libs are now at lib root folder 33322: ALF-2550 - Enterprise SDK files do not contain enterprise repository project. 33323: SDK - Added Enterprise third party libs. 33327: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/V4.0-BUG-FIX: 33324: Fix for THOR-941. Some MIME types appear as Unknown in metadata form. The inconsistencies in the edit mimetype form were slightly different on V4.0.1 so I tidied them up. 33330: ALF-12487 In Mimetype Detection, if Tika detects a generic type of text/plain or XML, defer to the Alfresco filename based type (as we already do for octet stream) 33335: Quick build fix - comment out SDK enterprise docs while I work out what's wrong. 33353: Added more projects to Enterprise Generate Docs. 33357: When installing a module the tool reads the war's version.properties file and will not install if the war version is outside the repoVersionMin or repoVersionMax 33361: Merged DEV/GETHIN/FINDBUGS to V4.0-BUG-FIX 32962: Findbugs fix: Suspicious comparison of Integer references 32963: Findbugs fix: Call to equals() compares different types 32968: hashcode should be hashCode 32970: Findbugs fix: Suspicious comparison of Integer references 32972: Findbugs fix: Suspicious comparison of Integer or Long references 32976: Findbugs fix: .remove() incompatible with expected argument type String 32977: Findbugs fix: String is incompatible with expected argument type 32978: Findbugs fix: Call to equals() compares different types I prefer this fix to the previous one I did 32982: Findbugs fix: Call to equals() compares different types classDefinition.getName().equals instead of classDefinition.equals 32983: Findbugs fix: String is incompatible with expected argument type java.util.Locale 32984: Findbugs fix: An apparent infinite loop 32985: Findbugs fix: bad month value of 12 passed to new java.util.GregorianCalendar This code passes a constant month value outside the expected range of 0..11 to a method. 32986: Findbugs fix: authenticationComponent masks field from superclass 32987: Findbugs fix: Invocation of toString on Array Now uses Arrays.toString() 32988: Findbugs fix: Incorrect lazy initialization of static field Now uses static initialization block 32989: Findbugs fix: Dead code: A null pointer would have been thrown before these lines 32990: Findbugs fix: possible null pointer dereference Just made the code a little clearer 32991: Findbugs fix: class defines a clone() method but the class doesn't implement Cloneable. 32992: Findbugs fix: All equals() methods should return false if passed a null value. 32993: Findbugs fix: Invocation of toString on Array Now uses Arrays.toString() 32994: Findbugs fix: If the multiplication is done using long arithmetic, you can avoid the possibility that the result will overflow. 33006: Findbugs fix: possible null Just made the code a little clearer 33007: Findbugs fix: Invocation of toString on Array Now uses Arrays.toString() 33008: Findbugs fix: Call to String.equals(Character) Explicitly using String 33009: Findbugs fix: int converted to long and passed as absolute time to new java.util.Date(long) Now works after the year 2037! 33014: Findbugs fix: Invocation of toString on Array Now uses Arrays.toString() 33023: Findbugs fix: There is an apparent infinite recursive loop No longer 33025: Findbugs fix: possible null Just made the code a little clearer 33026: Findbugs fix: impossible null check Changed && to || 33029: Findbugs fix: Minor change to Integer.valueOf 33126: Findbugs fix: Comparison of String objects using == now used .equals 33127: Findbugs fix: Unwritten field. All reads of it will return the default value. Now sets values in the constructor 33128: Findbugs fix: Removed try/catch 33129: Findbugs fix: Added an assertTrue to the unit test 33130: Findbugs fix: Passes null for nonnull parameter Mocked the serviceReg and now the tests work! 33131: Findbugs fix: Call to a collection method contains an argument with an incompatible class from that of the collection's parameter 33133: Findbugs fix: Deadly embrace between inner class and thread local - not eligible for garbage collection. Made inner class static 33182: Findbugs fix: possible null Now intialises documentPaths correctly 33184: Findbugs fix: possible null pointer (no more) 33185: Findbugs fix: Comparison of itself Changed variable name to fixedValue 33186: Findbugs fix: Possible null pointer dereference of nodePair Now continues 33187: Findbugs fix: This code seems to be using non-short-circuit logic 33188: Findbugs fix: Possible null pointer dereference of entry Discussed with Andy. If there's no entry then throw the exception, we can't continue 33189: Findbugs fix: Call to a collection method contains an argument with an incompatible class from that of the collection's parameter It must want to remove the user 33199: Modified the end of line character 33209: Changed tabs for spaces 33210: I removed the "if" because it was never called, however Brian suggested moving it to a place where it would be called! 33363: Fix for ALF-12374 - Share sample 'share-config-custom.xml' is missing an endpoint 'activiti-admin' 33364: Merge V3.4-BUG-FIX to V4.0-BUG-FIX 33362 : ALF-12448 - Missing jars in enterprise SDK 33376: Merged (RECORD ONLY) V3.4-BUG-FIX (3.4.8) to V4.0-BUG-FIX (4.0.1) 33375: ALF-12154: Merged HEAD to V3.4-BUG-FIX (3.4.8) Requested to RECORD ONLY this change when merging back to HEAD/V4.0-BUG-FIX etc. - Removed duplicate 'List constraint display labels' values from bpm-messages*.properties and dictionarydaotest_model.properties. May have been introduced by ALF-9514 changes in the same area, which was also RECORD ONLY - Manual merge of JSON propertyLabels that exist in HEAD back into 3.4.8 32724: OPEN : ALF-11176: Untranslated strings in Group Review and Approve Task form Activiti has one default transition "Next". If there is no transition then the model builder was not finding a translation for the task outcome (and just using the english word, e.g. "Approve"). Now it looks up the translation workflowtask.outcome.[wf:outcome property] 32943: FIXED : ALF-11176: Untranslated stings in Group Review and Approve Task form I've changed the way task descriptions are retrieved. You can now enter translations for them. 33377: ALF-12509: ibooks Format Added quick.ibooks file 33378: ALF-12207 IMAP: Empty file is not opened/downloaded (using IMAP Content Links) if it was uploaded to Share via fileserver 33379: Add the TIFF mimetype 33380: Improve the stream to Tika conversion code, following review for THOR-952 33385: Upgrade to the latest Tika and POI, for recent bug fixes 33387: ALF-12492 - Email with empty subject sent to Alfresco by SMTP cause Null pointer Exception 33396: ALF-12497 - Opening and closing (not save) MS Exel 2003 file via CIFS adds new version 33397: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33259: ALF-12354: DB2 reference files. 33261: ALF-12354: MySQL reference files. 33268: ALF-12354: Oracle reference files 33310: ALF-12354: Create schema reference files for MySQL, PostgreSQL, Oracle, DB2 33349: ALF-12354: Create schema reference files for MySQL, PostgreSQL, Oracle, DB2 33366: ALF-12412: Schema reference files should contain schema version number 33367: ALF-12412: Schema reference files should contain schema version number 33373: ALF-12354: Added comment to DB2 file regarding intermittent comparison failures. 33389: ALF-12516: Produce XML schema definition file (XSD) for schemacomp reference files. 33390: ALF-12516: added missing file (the actual XSD!) 33392: ALF-7260 RINF 03: Automate DB schema validation 33401: FindFindbugs fix: Maybe Derek expected null after all 33406: Latest SpringSurf libs - fixed use of java.util.UUID which blocks 33407: Merged BRANCHES\DEV\V3.4-BUG-FIX to BRANCHES\DEV\V4.0-BUG-FIX 33399: Fix for ALF-11962 Lucene queries searching on metadata (not on cm:content) with stopwords returns wrong results 33414: ALF-11746 Webscript to expose the repository mimetypes, along with their user facing display names and their extensions 33415: ALF-11746 Mimetype information can be available to all 33442: Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/V4.0-BUG-FIX 33441: Latest SpringSurf libs - performance and thread safety improvements. 33447: Fix to email test - query error exposed by recent change. 33451: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33446: ALF-12354: fixed DB2 intermittent unique index creation problems. 33448: ALF-7260: removed redundant code. 33454: Merged (RECORD ONLY) V3.4-BUG-FIX (3.4.8) to V4.0-BUG-FIX (4.0.1) 33388: Removed L10N that no longer have a default. 33437: ALF-9514 I18N: Model constraint values need localized display names - Undo changes to webclient_ja.properties that were made by r33286 for this issue. There were no changes in webclient.properties that did not already exist in webclient_ja.properties This should simplify translations. 33453: ALF-9514 I18N: Model constraint values need localized display names - Tidy up mess to do with the initial r33286 revision for this issue. Lots of L10N property changes. I think some of my trial merges from 4.0 did not get reverted before I applied the diff file. Reverse merged r33437 and r33286. Note r33388 and r33375 (for ALF-12154) already included changes to try and fix the initial revision. 33456: Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/V4.0-BUG-FIX: 33455: ALF-12410: JMX Dumps taking very long to finish 33457: SDK build fix 33463: Add in Enterprise Docs to SDK (again) 33464: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 33312: ALF-12448: Merged HEAD to V3.4-BUG-FIX (3.4.8) 33304: ALF-7542 SDK is missing spring-test.jar - ant script modified to copy org.springframework.test*.jar files into root\build\assemble\sdk\lib\server\dependencies 33382: ALF-10239 Form validation bug when content becomes invalid after XSD change - override isValidForSubmit function in alfresco.xforms.TextField as alfresco.xforms.Widget version does not check the max length 33383: Fix for ALF-11791 - Multiple search on category in Share Advanced Search return no result Merged HEAD to BRANCHES/DEV/V3.4-BUG-FIX 29710: SVC15: Contribution: Alfresco Share Adv Search Enhancement: Allow advanced search on category to include sub-categories in query by checking a checkbox as in Alfresco Explorer (ALF-7157) A new "showSubCategoriesOption" has been added to the "category.ftl" form control, that when set to true (as shown in the example config snippet below) will display a checkbox allowing the user to request all sub categories be searched as well as the selected ones. <field id="cm:categories"> <control> <control-param name="compactMode">true</control-param> <control-param name="showSubCategoriesOption">true</control-param> </control> </field> 30572: Fix for ALF-7008 - Double category in Share Advanced Search return no result 33384: Fix for ALF-12469 - Change method BaseAssociationEditor.generateFormSubmit to protected 33400: ALF-12366: Merged PATCHES/V3.4.6 to V3.4-BUG-FIX 33354: ALF-12393: Parent assocs must be cached with a txn ID, even when a node has no parents - Use left outer join in parent assocs query 33355: ALF-12393: Fixed typo in SQL + allowed assocIndex to be null 33413: ALF-12219: Fix CommandServlet to not mess up Document List and My Spaces List dashlets on Websphere 33416: ALF-12411: Merged DEV to V3.4-BUG-FIX (with corrections) 33404: Fix that introduces full Unicode character support into jBPM 3.3.1: - new patch that alters columns of the jBPM 3.3.1 tables to change 'TEXT' datatype to 'NVARCHAR(MAX)' (SQL Server and Generic dialects); - 'jbpm-upgrade.sql' patch modified to alter as in the new patch; - 'AlfrescoSQLServerDialect' fixed to allow create tables with 'CLOB' columns as 'NVARCHAR(MAX)' columns - devious chain of alternatives (suggested by Derek) introduced to make sure the right patch executes under the right circumstances 33418: ALF-9507: Fixed possible LDAP security hole Now we force RFC 2254 escaping of the user DN resolution query using argument substitution, as described here http://docs.oracle.com/javase/jndi/tutorial/ldap/search/search.html 33419: ALF-9658: Corrected AFTER_INACTIVITY cache update behaviour in InMemoryTicketComponentImpl - Now the key is preserved on entry update and not accidentally regenerated 33421: Merged DEV to V3.4-BUG-FIX 32042: ALF-11448: ArrayIndexOutOfBoundsException caused by unsynchronized call in org.alfresco.repo.webdav.WebDAV.formatModifiedDate Creation of SimpleDateFormatter was moved to a method. 33422: ALF-12302: /api/tags API returns badly-formed JSON - Resolved as suggested by MH 33423: ALF-10312: Parameter Based Redirection - Now we validate that the Explorer login page redirect URL is within the context path of the application (/alfresco) 33424: Merged DEV to V3.4-BUG-FIX 33358: ALF-11719: Webscript fails due to colon in password Split basic authentication header by first colon. Remaining part is user's password. 33425: Merged DEV to V3.4-BUG-FIX 33359: ALF-12071: Windows 7 cannot open files stored on Alfresco mounted as a webdav network drive if the filename contains + (plus) character For Windows 7 we SHOULD decode the file name gotten from GET request taking into account that "+" is not encoded as "%2B" for GET request. 33426: Merged DEV to V3.4-BUG-FIX (with corrections) 33374: ALF-10713: Remaining dependencies repo.remote.url from outboundSMTP-context.xml and activities-feed-context.xml was removed and SysAdminParams bean was injected instead. - MailActionExecuter.URLHelper returns url to alfresco using SysAdminParams. 33427: ALF-10713: repo.remote.url no longer used in 3.4.8 so removed altogether 33429: Merged HEAD to BRANCHES\DEV\V3.4-BUG-FIX 31191: First fix for ALF-10741 TAG field does not support wildcard, prefix, fuzzy queries etc -> wildcard searches entered by users will fail (for ALF-12162) 33433: ALF-12411: Fixed postUpdateScriptPatches declaration 33435: ALF-11719: Fix Authorization and AuthorizationTest 33436: ALF-12411: Fixed patch script paths 33445: Fixes: ALF-12389; internationalises the tool tip strings for changing the data list sort order. 33449: ALF-12411: Fixes from Dmitry - Corrected ID of patch.db-V3.4-Upgrade-JBPM - dependsOn property has no effect on a SchemaUpgradeScriptPatch - order controlled by schemaBootstrap.postUpdateScriptPatches 33459: ALF-9811: SSOAuthenticationFilter now supports basic auth as well. 33471: ALF-12297 Emailing to document via inbound SMTP causes integrity violation 33473: Reference schema files moved on from 5025 to 5026 33478: Fix for ALF-12515 33479: Fix for ALF-11116 33481: Fix for ALF-12099 33487: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33485: ALF-12598: Incorrect column order on indexes and primary keys must be reported to user 33496: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33494: ALF-12412: Schema reference files should contain schema version number 33508: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33507: ALF-12412: Added missing files 33515: Merge (Record Only) V3.4-BUG-FIX to V4.0-BUG-FIX 33495 : SDK Build fix - do not merge to V4.0 33517: V4.0 version of the fix for ALF-12393 - The node caching structure in 4.0 is version-based and there was already a check to ensure that in-memory vs database versions matched for all calls to get parent associations. - Added an additional version check for cases where the node returns no parent associations - Removed right outer joins associated with the 3.4 fixes (minor complexity that is no longer required) 33555: Merged (RECORD ONLY) V3.4-BUG-FIX (3.4.8) to V4.0-BUG-FIX (4.0.1) 33554: Merged V3.4 (3.4.8) to V3.4-BUG-FIX (3.4.9) 33512: GERMAN: L10N Updates, fixes: ALF-12154, ALF-9514, ALF-12389 33513: SPANISH: L10N Updates, fixes: ALF-12154, ALF-9514, ALF-12389 33514: FRENCH: L10N Updates, fixes: ALF-12154, ALF-9514, ALF-12389 33516: ITALIAN: L10N Updates, fixes: ALF-12154, ALF-9514, ALF-12389 33526: JAPANESE: L10N Updates, fixes: ALF-12154, ALF-9514, ALF-12389 33544: SPANISH: Fixes additional Spanish translation bugs 33552: JAPANESE: Adds previously missing workflow related translations 33560: Fix the line endings to be consistent 33561: DOC-335 Provide a commented out example of SPP/Vti SSL Configuration in the Module 33563: Merge (Record Only) V3.4-Bug-FIX to V4.0-BUG-FIX 33328 : ALF-12098 Issue ALF-4010 not fully resolved. ftp transfer of a previously moved file copies it to the destination folder of the previous move 33566: fixed a comment. 33567: Small change to debug logging 33568: Removed deleteConfirm flag. Not neccessary as events are fired postCommit. 33570: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 33550: Fixed ALF-10895 "Links, documents and folders: Unable to delete comments" - Made sure no "content" is sent when using HTTP DELETE to avoid proxy issues 33574: Merged (RECORD ONLY) V3.4-BUG-FIX (3.4.8) to V4.0-BUG-FIX (4.0.1) 33573: Merged V3.4 (3.4.8) to V3.4-BUG-FIX (3.4.9) 33557: JAPANESE: Removes redundant string 33576: ALF-12634 When building the edit online link to SPP/Vti, don't assume that the protocol (http/https) is the same as Share, but instead make that a module property (similar to how the port and hostname are set) 33577: Remove un-used imports 33584: ALF-12363: Protect against attempt to reference undefined rawPerms variable in folder-permissions WebScript controller 33586: ALF-12405: Always show delete site icon on My Sites dashlet when viewed on IE7 33605: Fixes: ALF-12408: Script Error when hovering on a row. 33616: Merged BRANCHES\DEV\V3.4-BUG-FIX to BRANCHES\DEV\V4.0-BUG-FIX 33500: Fix for ALF-12162 Searching for words with german umlaut does not show expected results 33624: ALF-12488 - CIFS error occurs if Hazelcast Config is enabled. 33637: ALF-11594: Disable comment field on upload dialog after version update 33643: ALF-12243: Fixed create HTML content via HTML editor 33649: Minor: tabs 33650: ALF-12657: We need to set maxSavePostSize for tomcat connectors to support SSL (e.g. ModelsGet from SOLR will truncate JSON to 4096 characters) 33662: Fix for ALF-12460 33663: Fix for ALF-12460 (part 2) 33664: Fix for ALF-12460 (part 2) 33665: Fix for ALF-12443 33672: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 33468: Merged BRANCHES/V3.4 to BRANCHES/DEV/V3.4-BUG-FIX 33467: Enterprise overlay update for MessagesWebScript 33470: Merged BRANCHES/V3.4 to BRANCHES/DEV/V3.4-BUG-FIX 33469: Revert rev 33467 - not required 33482: Fixed ALF-12373 "IE7 Specific: incorrect displaying of "Link to Rule Set" window in Alfresco Share" - also fixed for ie6 removed javascript error thrown on click 33580: Minor formatting for easier debug stepping 33581: Fixed ALF-12638: No username in an audit context after an error 33589: ALF-12650: Merged V3.4.1 to V3.4-BUG-FIX (3.4.9) 33588: ALF-12620 Regression. Since 3.4.1.25 / 3.4.6.7 a user can be invited to a site multiple times - correction to js contains function. Bug introduced in r32775 33597: Merge DEV to V3.4-BUG-FIX 33465 : ALF-11193 - Consumer role cannot Unscribe/subscribe the IMAP folders. 33602: Merged BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2011_12_06 to BRANCHES/DEV/V3.4-BUG-FIX: 32551: ALF-10133: "Doc folder" for calendar events does not clear/reset itself 33630: Merge Dev to V3.4-BUG-FIX 33626 : ALF-4896 - Lock icon displayed for documents with expired lock 33633: Merge V3.4.7 (3.4.7.1) to V3.4-BUG-FIX (3.4.9) 33609: ALF-12589 CLONE - Content Manager unable to edit content from another user sandbox - Hot Fix for 3.4.7 needed Changed the permissions on the user's 'preview' store so that the group of ContentManagers was granted the ContentManager permission. It had been granting the permission to all current users in the group individually. As a result new ContentManagers could not FLATTEN or WRITE to the preview store. This had already been done for the main user store a few years back. As the managers parameter was no longer needed the was removed. 33647: Merged DEV to V3.4-BUG-FIX 33629: ALF-12585: Manage System Users shows "Change Password" icon for LDAP users in search results Sets "isMutable" property using addPropertyResolver in UsersDialog. 33648: Merged DEV to V3.4-BUG-FIX 33623: ALF-10586: CMIS: Trying to delete a multilingual document w/o translations via webscript There is no need to delete associations in CMISServicesImpl.deleteObject(). 33659: Merged V3.4 to V3.4-BUG-FIX 33594: Merged BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2011_12_06 to BRANCHES/V3.4: 32551: ALF-10133: "Doc folder" for calendar events does not clear/reset itself 33595: Reverse merge of the following. Should have been committed to V3.4-BUG-FIX 33594: Merged BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2011_12_06 to BRANCHES/V3.4: 32551: ALF-10133: "Doc folder" for calendar events does not clear/reset itself 33604: ALF-12597: WCMQS doesn't work out of the box 33654: Merged DEV to V3.4 33622: ALF-12655: Configure link isn't presented in Web View dashlet(My Dashboard page) User userIsSiteManager is true by default in webview.get.js. This allows to configure Web View dashlet on user's dashbord. 33651: ALF-12655: Configure link isn't presented in Web View dashlet(My Dashboard page) Sets userIsSiteManager=false before remote call, so if remote call fails it won't give site manager role to the user. 33655: ALF-12366: Merged PATCHES/V3.4.6 to V3.4 33548: ALF-12393: More changes to cope with read committed DB behaviour in AbstractReindexComponent - Reverted r33278, 33354, 33355 and introduced more generic solution - NodeRefs resolving to deleted nodes in the cache cause cache cleaning and transaction retry if they resolve OK in the database - Cached parent assocs are thrown away for non-deleted nodes if they are empty - Removes the need for the outer join and special case exception handling all over the place 33562: ALF-12393: Further improvments - Lookup of NodeRef to a deleted node must always result in cache clearing and transaction retrying (due to possible read committed behaviour half way through transaction) - Detection and correction of stale cached negative results (VALUE_NOT_FOUND) in node cache 33583: ALF-12393: Rework to getNodePair(NodeRef) to have less impact on existing code but still cope with read committed - Look ups of deleted nodes still cause InvalidNodeRefExceptions but these have a retryable cause so that both handlers can handle and retrying transactions can recover - Corrections to cached negative results (because the nodesCache remembers negative results) are written straight through and the transaction does not need to be retried 33660: Merged V3.4 to V3.4-BUG-FIX (RECORD ONLY) 33634: ALF-12161: Merge V3.4-BUG-FIX (3.4.9) to V3.4 (3.4.8) 33633: Merge V3.4.7 (3.4.7.1) to V3.4-BUG-FIX (3.4.9) 33609: ALF-12589 CLONE - Content Manager unable to edit content from another user sandbox - Hot Fix for 3.4.7 needed Changed the permissions on the user's 'preview' store so that the group of ContentManagers was granted the ContentManager permission. It had been granting the permission to all current users in the group individually. As a result new ContentManagers could not FLATTEN or WRITE to the preview store. This had already been done for the main user store a few years back. As the managers parameter was no longer needed the was removed. 33657: ALF-12650: Merged PATCHES/V3.4.6 to V3.4 33590: ALF-12620: Merged V3.4-BUG-FIX (3.4.9) to V3.4.6 (3.4.6.10) 33589: ALF-12650: Merged V3.4.1 to V3.4-BUG-FIX (3.4.9) 33588: ALF-12620 Regression. Since 3.4.1.25 / 3.4.6.7 a user can be invited to a site multiple times - correction to js contains function. Bug introduced in r32775 33673: Fixed compilation error 33678: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 33677: Merged V3.4 to V3.4-BUG-FIX 33676: ALF-12436: Increase content data cache sizes to match node properties cache sizes to allow bulk loading without overflowing 33679: Merged V3.4-BUG-FIX to V4.0-BUG-FIX (RECORD ONLY) 33486: Merge V4.0-BUG-FIX to V3.4-BUG-FIX 33306 - experiment to add sdk-extras 33489: Merged V4.0-BUG-FIX to V3.4-BUG-FIX 33322 : Enterprise SDK 33323 33335 33353 33457 33463 33495: SDK Build fix - do not merge to V4.0. 33680: Merged DEV to V4.0-BUG-FIX 33675: ALF-12379: WebLogic: alfresco fails to start: java.lang.NoSuchMethodError: org.apache.commons.lang.mutable.MutableInt.increment()V org.apache.commons.* package should be used as application's preferrable for WebLogic. 33683: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 33682: Merged V3.4 to V3.4-BUG-FIX 33681: ALF-12132: Fix Bitrock's copy of the custom lotus context. Yuck! 33689: Fix for ALF-12437 Switching from Lucene to Solr caused subsystems to be in inconsistent state and repository to hang - filter out changes to read only properties when set in bulk - ie they are ignored 33691: Fix for ALF-12667 33693: Fix for ALF-12695 SOLR should not appear to work with AVM - it should not silently fail. - AVM search and indexing will raise exceptions when used with SOLR - removed the AVM site bootstrap associated with Share in older versions of the product. 33701: Merged DEV to V4.0-BUG-FIX 33697: ALF-12691: Alfresco Explorer doesn't work: java.lang.NoClassDefFoundError: org/apache/commons/lang/builder/HashCodeBuilder Shared library for WAS was fixed. commons-lang-2.6.jar is used. 33704: ALF-12299 - NFS subsystem enable/disable needs a different attribute name 33714: ALF-10229: DOS Voodoo to set ALF_HOME to the parent parent directory of the apply_amps script - for /D %%D IN (%~dp0..\) do set ALF_HOME=%%~dpD - Yuck! 33721: Merged BRANCHES/DEV/mward/schemacomp to BRANCHES/DEV/V4.0-BUG-FIX: 33720: Merged BRANCHES/DEV/BELARUS/V4.0-BUG-FIX-2012_01_20 to BRANCHES/DEV/mward/schemacomp: 33631: LF-12355 : Create schema reference files for SQL Server 33727: Test fix - fallout from removing the bootstrap for the "sitestore" AVM store 33729: Fixes: ALF-12575 - missing i18n strings. (translations pending) 33738: SPANISH: Translation updates based on EN r33523 33739: Merged DEV to V4.0-BUG-FIX 33723: ALF-10229: apply_amps.bat doesn't work properly from alfresco-enterprise-4.0.0a.zip Fix for apply_amps.sh to set ALF_HOME to the parent parent directory of the apply_amps script 33746: ALF-10656 SOLR: Patches execute search during bootstrap causing deadlock - Part 1: - SOLR query use during bootstrap will throw an exception - tidy up some patch beans - ignore unused AVM "sitestore" in WCMPostPermissionSnapshotPatch 33753: Remove upgrade installers from build targets git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@33758 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2794 lines
109 KiB
Java
2794 lines
109 KiB
Java
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.alfresco.repo.node.db;
|
|
|
|
import java.io.Serializable;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.Stack;
|
|
|
|
import org.alfresco.error.AlfrescoRuntimeException;
|
|
import org.alfresco.model.ContentModel;
|
|
import org.alfresco.repo.domain.node.ChildAssocEntity;
|
|
import org.alfresco.repo.domain.node.Node;
|
|
import org.alfresco.repo.domain.node.NodeDAO;
|
|
import org.alfresco.repo.domain.node.NodeDAO.ChildAssocRefQueryCallback;
|
|
import org.alfresco.repo.domain.node.NodeExistsException;
|
|
import org.alfresco.repo.domain.qname.QNameDAO;
|
|
import org.alfresco.repo.node.AbstractNodeServiceImpl;
|
|
import org.alfresco.repo.node.StoreArchiveMap;
|
|
import org.alfresco.repo.node.archive.NodeArchiveService;
|
|
import org.alfresco.repo.node.index.NodeIndexer;
|
|
import org.alfresco.repo.policy.BehaviourFilter;
|
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
|
import org.alfresco.repo.transaction.RetryingTransactionHelper;
|
|
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
|
|
import org.alfresco.repo.transaction.TransactionListenerAdapter;
|
|
import org.alfresco.repo.transaction.TransactionalResourceHelper;
|
|
import org.alfresco.service.cmr.dictionary.AspectDefinition;
|
|
import org.alfresco.service.cmr.dictionary.AssociationDefinition;
|
|
import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition;
|
|
import org.alfresco.service.cmr.dictionary.ClassDefinition;
|
|
import org.alfresco.service.cmr.dictionary.InvalidAspectException;
|
|
import org.alfresco.service.cmr.dictionary.InvalidTypeException;
|
|
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
|
|
import org.alfresco.service.cmr.dictionary.TypeDefinition;
|
|
import org.alfresco.service.cmr.repository.AssociationExistsException;
|
|
import org.alfresco.service.cmr.repository.AssociationRef;
|
|
import org.alfresco.service.cmr.repository.ChildAssociationRef;
|
|
import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException;
|
|
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
|
|
import org.alfresco.service.cmr.repository.InvalidStoreRefException;
|
|
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.StoreRef;
|
|
import org.alfresco.service.cmr.repository.NodeRef.Status;
|
|
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
|
|
import org.alfresco.service.namespace.QName;
|
|
import org.alfresco.service.namespace.QNamePattern;
|
|
import org.alfresco.service.namespace.RegexQNamePattern;
|
|
import org.alfresco.util.EqualsHelper;
|
|
import org.alfresco.util.GUID;
|
|
import org.alfresco.util.Pair;
|
|
import org.alfresco.util.ParameterCheck;
|
|
import org.alfresco.util.PropertyMap;
|
|
import org.apache.commons.logging.Log;
|
|
import org.apache.commons.logging.LogFactory;
|
|
import org.springframework.extensions.surf.util.I18NUtil;
|
|
|
|
/**
|
|
* Node service using database persistence layer to fulfill functionality
|
|
*
|
|
* @author Derek Hulley
|
|
*/
|
|
public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|
{
|
|
private final static String KEY_PRE_COMMIT_ADD_NODE = "DbNodeServiceImpl.PreCommitAddNode";
|
|
private final static String KEY_DELETED_NODES = "DbNodeServiceImpl.DeletedNodes";
|
|
|
|
private static Log logger = LogFactory.getLog(DbNodeServiceImpl.class);
|
|
|
|
private QNameDAO qnameDAO;
|
|
private NodeDAO nodeDAO;
|
|
private StoreArchiveMap storeArchiveMap;
|
|
private NodeService avmNodeService;
|
|
private NodeIndexer nodeIndexer;
|
|
private BehaviourFilter policyBehaviourFilter;
|
|
private boolean enableTimestampPropagation;
|
|
|
|
public DbNodeServiceImpl()
|
|
{
|
|
storeArchiveMap = new StoreArchiveMap(); // in case it is not set
|
|
}
|
|
|
|
public void setQnameDAO(QNameDAO qnameDAO)
|
|
{
|
|
this.qnameDAO = qnameDAO;
|
|
}
|
|
|
|
public void setNodeDAO(NodeDAO nodeDAO)
|
|
{
|
|
this.nodeDAO = nodeDAO;
|
|
}
|
|
|
|
public void setStoreArchiveMap(StoreArchiveMap storeArchiveMap)
|
|
{
|
|
this.storeArchiveMap = storeArchiveMap;
|
|
}
|
|
|
|
public void setAvmNodeService(NodeService avmNodeService)
|
|
{
|
|
this.avmNodeService = avmNodeService;
|
|
}
|
|
|
|
/**
|
|
* @param nodeIndexer the indexer that will be notified of node additions,
|
|
* modifications and deletions
|
|
*/
|
|
public void setNodeIndexer(NodeIndexer nodeIndexer)
|
|
{
|
|
this.nodeIndexer = nodeIndexer;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param policyBehaviourFilter component used to enable and disable behaviours
|
|
*/
|
|
public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter)
|
|
{
|
|
this.policyBehaviourFilter = policyBehaviourFilter;
|
|
}
|
|
|
|
/**
|
|
* Set whether <b>cm:auditable</b> timestamps should be propagated to parent nodes
|
|
* where the parent-child relationship has been marked using <b>propagateTimestamps<b/>.
|
|
*
|
|
* @param enableTimestampPropagation <tt>true</tt> to propagate timestamps to the parent
|
|
* node where appropriate
|
|
*/
|
|
public void setEnableTimestampPropagation(boolean enableTimestampPropagation)
|
|
{
|
|
this.enableTimestampPropagation = enableTimestampPropagation;
|
|
}
|
|
|
|
/**
|
|
* Performs a null-safe get of the node
|
|
*
|
|
* @param nodeRef the node to retrieve
|
|
* @return Returns the node entity (never null)
|
|
* @throws InvalidNodeRefException if the referenced node could not be found
|
|
*/
|
|
private Pair<Long, NodeRef> getNodePairNotNull(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
ParameterCheck.mandatory("nodeRef", nodeRef);
|
|
|
|
Pair<Long, NodeRef> unchecked = nodeDAO.getNodePair(nodeRef);
|
|
if (unchecked == null)
|
|
{
|
|
Status nodeStatus = nodeDAO.getNodeRefStatus(nodeRef);
|
|
throw new InvalidNodeRefException("Node does not exist: " + nodeRef + "(" + nodeStatus + ")", nodeRef);
|
|
}
|
|
return unchecked;
|
|
}
|
|
|
|
public boolean exists(StoreRef storeRef)
|
|
{
|
|
return nodeDAO.exists(storeRef);
|
|
}
|
|
|
|
public boolean exists(NodeRef nodeRef)
|
|
{
|
|
ParameterCheck.mandatory("nodeRef", nodeRef);
|
|
return nodeDAO.exists(nodeRef);
|
|
}
|
|
|
|
public Status getNodeStatus(NodeRef nodeRef)
|
|
{
|
|
ParameterCheck.mandatory("nodeRef", nodeRef);
|
|
NodeRef.Status status = nodeDAO.getNodeRefStatus(nodeRef);
|
|
return status;
|
|
}
|
|
|
|
@Override
|
|
public NodeRef getNodeRef(Long nodeId)
|
|
{
|
|
Pair<Long, NodeRef> nodePair = nodeDAO.getNodePair(nodeId);
|
|
return nodePair == null ? null : nodePair.getSecond();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public List<StoreRef> getStores()
|
|
{
|
|
// Get the ADM stores
|
|
List<Pair<Long, StoreRef>> stores = nodeDAO.getStores();
|
|
List<StoreRef> storeRefs = new ArrayList<StoreRef>(50);
|
|
for (Pair<Long, StoreRef> pair : stores)
|
|
{
|
|
StoreRef storeRef = pair.getSecond();
|
|
if (storeRef.getProtocol().equals(StoreRef.PROTOCOL_DELETED))
|
|
{
|
|
// Ignore
|
|
continue;
|
|
}
|
|
storeRefs.add(storeRef);
|
|
}
|
|
// Now get the AVMStores.
|
|
List<StoreRef> avmStores = avmNodeService.getStores();
|
|
storeRefs.addAll(avmStores);
|
|
// Return them all.
|
|
return storeRefs;
|
|
}
|
|
|
|
/**
|
|
* Defers to the typed service
|
|
* @see StoreDaoService#createWorkspace(String)
|
|
*/
|
|
public StoreRef createStore(String protocol, String identifier)
|
|
{
|
|
StoreRef storeRef = new StoreRef(protocol, identifier);
|
|
|
|
// invoke policies
|
|
invokeBeforeCreateStore(ContentModel.TYPE_STOREROOT, storeRef);
|
|
|
|
// create a new one
|
|
Pair<Long, NodeRef> rootNodePair = nodeDAO.newStore(storeRef);
|
|
NodeRef rootNodeRef = rootNodePair.getSecond();
|
|
|
|
// invoke policies
|
|
invokeOnCreateStore(rootNodeRef);
|
|
|
|
// Index
|
|
ChildAssociationRef assocRef = new ChildAssociationRef(null, null, null, rootNodeRef);
|
|
nodeIndexer.indexCreateNode(assocRef);
|
|
|
|
// Done
|
|
return storeRef;
|
|
}
|
|
|
|
/**
|
|
* @throws UnsupportedOperationException Always
|
|
*/
|
|
public void deleteStore(StoreRef storeRef) throws InvalidStoreRefException
|
|
{
|
|
// Delete the index
|
|
nodeIndexer.indexDeleteStore(storeRef);
|
|
// Rename the store
|
|
StoreRef deletedStoreRef = new StoreRef(StoreRef.PROTOCOL_DELETED, GUID.generate());
|
|
nodeDAO.moveStore(storeRef, deletedStoreRef);
|
|
|
|
// Done
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Marked store for deletion: " + storeRef + " --> " + deletedStoreRef);
|
|
}
|
|
}
|
|
|
|
public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException
|
|
{
|
|
Pair<Long, NodeRef> rootNodePair = nodeDAO.getRootNode(storeRef);
|
|
if (rootNodePair == null)
|
|
{
|
|
throw new InvalidStoreRefException("Store does not exist: " + storeRef, storeRef);
|
|
}
|
|
// done
|
|
return rootNodePair.getSecond();
|
|
}
|
|
|
|
@Override
|
|
public Set<NodeRef> getAllRootNodes(StoreRef storeRef)
|
|
{
|
|
return nodeDAO.getAllRootNodes(storeRef);
|
|
}
|
|
|
|
/**
|
|
* @see #createNode(NodeRef, QName, QName, QName, Map)
|
|
*/
|
|
public ChildAssociationRef createNode(
|
|
NodeRef parentRef,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
QName nodeTypeQName)
|
|
{
|
|
return this.createNode(parentRef, assocTypeQName, assocQName, nodeTypeQName, null);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public ChildAssociationRef createNode(
|
|
NodeRef parentRef,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
QName nodeTypeQName,
|
|
Map<QName, Serializable> properties)
|
|
{
|
|
ParameterCheck.mandatory("parentRef", parentRef);
|
|
ParameterCheck.mandatory("assocTypeQName", assocTypeQName);
|
|
ParameterCheck.mandatory("assocQName", assocQName);
|
|
ParameterCheck.mandatory("nodeTypeQName", nodeTypeQName);
|
|
if(assocQName.getLocalName().length() > QName.MAX_LENGTH)
|
|
{
|
|
throw new IllegalArgumentException("Localname is too long");
|
|
}
|
|
|
|
// Get the parent node
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
|
|
StoreRef parentStoreRef = parentRef.getStoreRef();
|
|
|
|
// null property map is allowed
|
|
if (properties == null)
|
|
{
|
|
properties = Collections.emptyMap();
|
|
}
|
|
|
|
// get an ID for the node
|
|
String newUuid = generateGuid(properties);
|
|
|
|
/**
|
|
* Check the parent node has not been deleted in this txn.
|
|
*/
|
|
if(isDeletedNodeRef(parentRef))
|
|
{
|
|
throw new InvalidNodeRefException("The parent node has been deleted", parentRef);
|
|
}
|
|
|
|
// Invoke policy behaviour
|
|
invokeBeforeCreateNode(parentRef, assocTypeQName, assocQName, nodeTypeQName);
|
|
|
|
// check the node type
|
|
TypeDefinition nodeTypeDef = dictionaryService.getType(nodeTypeQName);
|
|
if (nodeTypeDef == null)
|
|
{
|
|
throw new InvalidTypeException(nodeTypeQName);
|
|
}
|
|
|
|
// Ensure child uniqueness
|
|
String newName = extractNameProperty(properties);
|
|
|
|
// Get the thread's locale
|
|
Locale locale = I18NUtil.getLocale();
|
|
|
|
// create the node instance
|
|
ChildAssocEntity assoc = nodeDAO.newNode(
|
|
parentNodePair.getFirst(),
|
|
assocTypeQName,
|
|
assocQName,
|
|
parentStoreRef,
|
|
newUuid,
|
|
nodeTypeQName,
|
|
locale,
|
|
newName,
|
|
properties);
|
|
ChildAssociationRef childAssocRef = assoc.getRef(qnameDAO);
|
|
Pair<Long, NodeRef> childNodePair = assoc.getChildNode().getNodePair();
|
|
|
|
addAspectsAndProperties(
|
|
childNodePair,
|
|
nodeTypeQName,
|
|
null,
|
|
Collections.<QName>emptySet(),
|
|
Collections.<QName, Serializable>emptyMap(),
|
|
Collections.<QName>emptySet(),
|
|
properties,
|
|
true,
|
|
false);
|
|
|
|
Map<QName, Serializable> propertiesAfter = nodeDAO.getNodeProperties(childNodePair.getFirst());
|
|
|
|
// Propagate timestamps
|
|
propagateTimeStamps(childAssocRef);
|
|
|
|
// Invoke policy behaviour
|
|
invokeOnCreateNode(childAssocRef);
|
|
invokeOnCreateChildAssociation(childAssocRef, true);
|
|
Map<QName, Serializable> propertiesBefore = PropertyMap.EMPTY_MAP;
|
|
invokeOnUpdateProperties(
|
|
childAssocRef.getChildRef(),
|
|
propertiesBefore,
|
|
propertiesAfter);
|
|
|
|
untrackDeletedNodeRef(childAssocRef.getChildRef());
|
|
|
|
// Index
|
|
nodeIndexer.indexCreateNode(childAssocRef);
|
|
|
|
// Ensure that the parent node has the required aspects
|
|
addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false);
|
|
|
|
// done
|
|
return childAssocRef;
|
|
}
|
|
|
|
|
|
/**
|
|
* Track a deleted node
|
|
*
|
|
* The deleted node set is used to break an infinite loop which can happen when adding a new node into a path containing a
|
|
* deleted node. This transactional list is used to detect and prevent that from
|
|
* happening.
|
|
*
|
|
* @param nodeRef the deleted node to track
|
|
* @return <tt>true</tt> if the node was not already tracked
|
|
*/
|
|
private boolean trackDeletedNodeRef(NodeRef deletedNodeRef)
|
|
{
|
|
Set<NodeRef> deletedNodes = TransactionalResourceHelper.getSet(KEY_DELETED_NODES);
|
|
return deletedNodes.add(deletedNodeRef);
|
|
}
|
|
|
|
/**
|
|
* Untrack a deleted node ref
|
|
*
|
|
* Used when a deleted node is restored.
|
|
*
|
|
* @param deletedNodeRef
|
|
*/
|
|
private void untrackDeletedNodeRef(NodeRef deletedNodeRef)
|
|
{
|
|
Set<NodeRef> deletedNodes = TransactionalResourceHelper.getSet(KEY_DELETED_NODES);
|
|
if (deletedNodes.size() > 0)
|
|
{
|
|
deletedNodes.remove(deletedNodeRef);
|
|
}
|
|
}
|
|
|
|
private boolean isDeletedNodeRef(NodeRef deletedNodeRef)
|
|
{
|
|
Set<NodeRef> deletedNodes = TransactionalResourceHelper.getSet(KEY_DELETED_NODES);
|
|
return deletedNodes.contains(deletedNodeRef);
|
|
}
|
|
|
|
/**
|
|
* loose interest in tracking a node ref
|
|
*
|
|
* for example if its been deleted or moved
|
|
* @param nodeRef the node ref to untrack
|
|
*/
|
|
private void untrackNewNodeRef(NodeRef nodeRef)
|
|
{
|
|
Set<NodeRef> newNodes = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_ADD_NODE);
|
|
if (newNodes.size() > 0)
|
|
{
|
|
newNodes.remove(nodeRef);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds all the aspects and properties required for the given node, along with mandatory aspects
|
|
* and related properties.
|
|
* Existing values will not be overridden. All required pre- and post-update notifications
|
|
* are sent for missing aspects.
|
|
*
|
|
* @param nodePair the node to which the details apply
|
|
* @param classQName the type or aspect QName for which the defaults must be applied.
|
|
* If this is <tt>null</tt> then properties and aspects are only applied
|
|
* for 'extra' aspects and 'extra' properties.
|
|
* @param existingAspects the existing aspects or <tt>null</tt> to have them fetched
|
|
* @param existingProperties the existing properties or <tt>null</tt> to have them fetched
|
|
* @param extraAspects any aspects that should be added to the 'missing' set (may be <tt>null</tt>)
|
|
* @param extraProperties any properties that should be added the the 'missing' set (may be <tt>null</tt>)
|
|
* @param overwriteExistingProperties <tt>true</tt> if the extra properties must completely overwrite
|
|
* the existing properties
|
|
* @return <tt>true</tt> if properties or aspects were added
|
|
*/
|
|
private boolean addAspectsAndProperties(
|
|
Pair<Long, NodeRef> nodePair,
|
|
QName classQName,
|
|
Set<QName> existingAspects,
|
|
Map<QName, Serializable> existingProperties,
|
|
Set<QName> extraAspects,
|
|
Map<QName, Serializable> extraProperties,
|
|
boolean overwriteExistingProperties)
|
|
{
|
|
return addAspectsAndProperties(nodePair, classQName, null, existingAspects, existingProperties, extraAspects, extraProperties, overwriteExistingProperties, true);
|
|
}
|
|
|
|
private boolean addAspectsAndPropertiesAssoc(
|
|
Pair<Long, NodeRef> nodePair,
|
|
QName assocTypeQName,
|
|
Set<QName> existingAspects,
|
|
Map<QName, Serializable> existingProperties,
|
|
Set<QName> extraAspects,
|
|
Map<QName, Serializable> extraProperties,
|
|
boolean overwriteExistingProperties)
|
|
{
|
|
return addAspectsAndProperties(nodePair, null, assocTypeQName, existingAspects, existingProperties, extraAspects, extraProperties, overwriteExistingProperties, true);
|
|
}
|
|
|
|
private boolean addAspectsAndProperties(
|
|
Pair<Long, NodeRef> nodePair,
|
|
QName classQName,
|
|
QName assocTypeQName,
|
|
Set<QName> existingAspects,
|
|
Map<QName, Serializable> existingProperties,
|
|
Set<QName> extraAspects,
|
|
Map<QName, Serializable> extraProperties,
|
|
boolean overwriteExistingProperties,
|
|
boolean invokeOnUpdateProperties)
|
|
{
|
|
ParameterCheck.mandatory("nodePair", nodePair);
|
|
|
|
Long nodeId = nodePair.getFirst();
|
|
NodeRef nodeRef = nodePair.getSecond();
|
|
|
|
// Ensure that have a type that has no mandatory aspects or properties
|
|
if (classQName == null)
|
|
{
|
|
classQName = ContentModel.TYPE_BASE;
|
|
}
|
|
|
|
// Ensure we have 'extra' aspects and properties to play with
|
|
if (extraAspects == null)
|
|
{
|
|
extraAspects = Collections.emptySet();
|
|
}
|
|
if (extraProperties == null)
|
|
{
|
|
extraProperties = Collections.emptyMap();
|
|
}
|
|
|
|
// Get the existing aspects and properties, if necessary
|
|
if (existingAspects == null)
|
|
{
|
|
existingAspects = nodeDAO.getNodeAspects(nodeId);
|
|
}
|
|
if (existingProperties == null)
|
|
{
|
|
existingProperties = nodeDAO.getNodeProperties(nodeId);
|
|
}
|
|
|
|
// To determine the 'missing' aspects, we need to determine the full set of properties
|
|
Map<QName, Serializable> allProperties = new HashMap<QName, Serializable>(37);
|
|
allProperties.putAll(existingProperties);
|
|
allProperties.putAll(extraProperties);
|
|
|
|
// Copy incoming existing values so that we can modify appropriately
|
|
existingAspects = new HashSet<QName>(existingAspects);
|
|
|
|
// Get the 'missing' aspects and append the 'extra' aspects
|
|
Set<QName> missingAspects = getMissingAspects(existingAspects, allProperties, classQName);
|
|
missingAspects.addAll(extraAspects);
|
|
|
|
if (assocTypeQName != null)
|
|
{
|
|
missingAspects.addAll(getMissingAspectsAssoc(existingAspects, allProperties, assocTypeQName));
|
|
}
|
|
|
|
// Notify 'before' adding aspect
|
|
for (QName missingAspect : missingAspects)
|
|
{
|
|
invokeBeforeAddAspect(nodeRef, missingAspect);
|
|
}
|
|
|
|
// Get all missing properties for aspects that are missing.
|
|
// This will include the type if the type was passed in.
|
|
Set<QName> allClassQNames = new HashSet<QName>(13);
|
|
allClassQNames.add(classQName);
|
|
allClassQNames.addAll(missingAspects);
|
|
Map<QName, Serializable> missingProperties = getMissingProperties(existingProperties, allClassQNames);
|
|
missingProperties.putAll(extraProperties);
|
|
|
|
// Bulk-add the properties
|
|
boolean changedProperties = false;
|
|
if (overwriteExistingProperties)
|
|
{
|
|
// Overwrite properties
|
|
changedProperties = nodeDAO.setNodeProperties(nodeId, missingProperties);
|
|
}
|
|
else
|
|
{
|
|
// Append properties
|
|
changedProperties = nodeDAO.addNodeProperties(nodeId, missingProperties);
|
|
}
|
|
if (changedProperties && invokeOnUpdateProperties)
|
|
{
|
|
Map<QName, Serializable> propertiesAfter = nodeDAO.getNodeProperties(nodeId);
|
|
invokeOnUpdateProperties(nodeRef, existingProperties, propertiesAfter);
|
|
}
|
|
// Bulk-add the aspects
|
|
boolean changedAspects = nodeDAO.addNodeAspects(nodeId, missingAspects);
|
|
if (changedAspects)
|
|
{
|
|
for (QName missingAspect : missingAspects)
|
|
{
|
|
invokeOnAddAspect(nodeRef, missingAspect);
|
|
}
|
|
}
|
|
// Done
|
|
return changedAspects || changedProperties;
|
|
}
|
|
|
|
private Set<QName> getMissingAspectsAssoc(
|
|
Set<QName> existingAspects,
|
|
Map<QName, Serializable> existingProperties,
|
|
QName assocTypeQName)
|
|
{
|
|
AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName);
|
|
if (assocDef == null)
|
|
{
|
|
return Collections.emptySet();
|
|
}
|
|
ClassDefinition classDefinition = assocDef.getSourceClass();
|
|
return getMissingAspects(existingAspects, existingProperties, classDefinition.getName());
|
|
}
|
|
|
|
/**
|
|
* Get any aspects that should be added given the type, properties and existing aspects.
|
|
* Note that this <b>does not</b> included a search for properties required for the missing
|
|
* aspects.
|
|
*
|
|
* @param classQName the type, aspect or association
|
|
* @return Returns any aspects that should be added
|
|
*/
|
|
private Set<QName> getMissingAspects(
|
|
Set<QName> existingAspects,
|
|
Map<QName, Serializable> existingProperties,
|
|
QName classQName)
|
|
{
|
|
// Copy incoming existing values so that we can modify appropriately
|
|
existingAspects = new HashSet<QName>(existingAspects);
|
|
|
|
ClassDefinition classDefinition = dictionaryService.getClass(classQName);
|
|
if (classDefinition == null)
|
|
{
|
|
return Collections.emptySet();
|
|
}
|
|
|
|
Set<QName> missingAspects = new HashSet<QName>(7);
|
|
// Check that the aspect itself is present (only applicable for aspects)
|
|
if (classDefinition.isAspect() && !existingAspects.contains(classQName))
|
|
{
|
|
missingAspects.add(classQName);
|
|
}
|
|
|
|
// Find all aspects that should be present on the class
|
|
List<AspectDefinition> defaultAspectDefs = classDefinition.getDefaultAspects();
|
|
for (AspectDefinition defaultAspectDef : defaultAspectDefs)
|
|
{
|
|
QName defaultAspect = defaultAspectDef.getName();
|
|
if (!existingAspects.contains(defaultAspect))
|
|
{
|
|
missingAspects.add(defaultAspect);
|
|
}
|
|
}
|
|
// Find all aspects that should be present given the existing properties
|
|
for (QName existingPropQName : existingProperties.keySet())
|
|
{
|
|
PropertyDefinition existingPropDef = dictionaryService.getProperty(existingPropQName);
|
|
if (existingPropDef == null || !existingPropDef.getContainerClass().isAspect())
|
|
{
|
|
continue; // Property is undefined or belongs to a class
|
|
}
|
|
QName existingPropDefiningType = existingPropDef.getContainerClass().getName();
|
|
if (!existingAspects.contains(existingPropDefiningType))
|
|
{
|
|
missingAspects.add(existingPropDefiningType);
|
|
}
|
|
}
|
|
// If there were missing aspects, recurse to find further missing aspects
|
|
// Don't re-add ones we know about or we can end in infinite recursion.
|
|
// Don't send any properties because we don't want to reprocess them each time
|
|
Set<QName> allTypesAndAspects = new HashSet<QName>(13);
|
|
allTypesAndAspects.add(classQName);
|
|
allTypesAndAspects.addAll(existingAspects);
|
|
allTypesAndAspects.addAll(missingAspects);
|
|
Set<QName> missingAspectsCopy = new HashSet<QName>(missingAspects);
|
|
for (QName missingAspect : missingAspectsCopy)
|
|
{
|
|
Set<QName> furtherMissingAspects = getMissingAspects(
|
|
allTypesAndAspects,
|
|
Collections.<QName, Serializable>emptyMap(),
|
|
missingAspect);
|
|
missingAspects.addAll(furtherMissingAspects);
|
|
allTypesAndAspects.addAll(furtherMissingAspects);
|
|
}
|
|
// Done
|
|
return missingAspects;
|
|
}
|
|
|
|
/**
|
|
* @param existingProperties existing node properties
|
|
* @param classQNames the types or aspects to introspect
|
|
* @return Returns any properties that should be added
|
|
*/
|
|
private Map<QName, Serializable> getMissingProperties(Map<QName, Serializable> existingProperties, Set<QName> classQNames)
|
|
{
|
|
Map<QName, Serializable> allDefaultProperties = new HashMap<QName, Serializable>(17);
|
|
for (QName classQName : classQNames)
|
|
{
|
|
ClassDefinition classDefinition = dictionaryService.getClass(classQName);
|
|
if (classDefinition == null)
|
|
{
|
|
continue;
|
|
}
|
|
// Get the default properties for this type/aspect
|
|
Map<QName, Serializable> defaultProperties = getDefaultProperties(classQName);
|
|
if (defaultProperties.size() > 0)
|
|
{
|
|
allDefaultProperties.putAll(defaultProperties);
|
|
}
|
|
}
|
|
// Work out what is missing
|
|
Map<QName, Serializable> missingProperties = new HashMap<QName, Serializable>(allDefaultProperties);
|
|
missingProperties.keySet().removeAll(existingProperties.keySet());
|
|
// Done
|
|
return missingProperties;
|
|
}
|
|
|
|
public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index)
|
|
{
|
|
// get nodes
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(childAssocRef.getParentRef());
|
|
Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childAssocRef.getChildRef());
|
|
|
|
Long parentNodeId = parentNodePair.getFirst();
|
|
Long childNodeId = childNodePair.getFirst();
|
|
QName assocTypeQName = childAssocRef.getTypeQName();
|
|
QName assocQName = childAssocRef.getQName();
|
|
|
|
// set the index
|
|
int updated = nodeDAO.setChildAssocIndex(
|
|
parentNodeId, childNodeId, assocTypeQName, assocQName, index);
|
|
if (updated < 1)
|
|
{
|
|
throw new InvalidChildAssociationRefException(
|
|
"Unable to set child association index: \n" +
|
|
" assoc: " + childAssocRef + "\n" +
|
|
" index: " + index,
|
|
childAssocRef);
|
|
}
|
|
}
|
|
|
|
public QName getType(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return nodeDAO.getNodeType(nodePair.getFirst());
|
|
}
|
|
|
|
/**
|
|
* @see org.alfresco.service.cmr.repository.NodeService#setType(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
|
|
*/
|
|
public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException
|
|
{
|
|
// check the node type
|
|
TypeDefinition nodeTypeDef = dictionaryService.getType(typeQName);
|
|
if (nodeTypeDef == null)
|
|
{
|
|
throw new InvalidTypeException(typeQName);
|
|
}
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
|
|
// Invoke policies
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
QName oldType = nodeDAO.getNodeType(nodePair.getFirst());
|
|
invokeBeforeSetType(nodeRef, oldType, typeQName);
|
|
|
|
// Set the type
|
|
boolean updatedNode = nodeDAO.updateNode(nodePair.getFirst(), typeQName, null);
|
|
|
|
// Add the default aspects and properties required for the given type. Existing values will not be overridden.
|
|
boolean updatedProps = addAspectsAndProperties(nodePair, typeQName, null, null, null, null, false);
|
|
|
|
// Invoke policies
|
|
if (updatedNode || updatedProps)
|
|
{
|
|
// Invoke policies
|
|
invokeOnUpdateNode(nodeRef);
|
|
invokeOnSetType(nodeRef, oldType, typeQName);
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see Node#getAspects()
|
|
*/
|
|
public void addAspect(
|
|
NodeRef nodeRef,
|
|
QName aspectTypeQName,
|
|
Map<QName, Serializable> aspectProperties)
|
|
throws InvalidNodeRefException, InvalidAspectException
|
|
{
|
|
// check that the aspect is legal
|
|
AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName);
|
|
if (aspectDef == null)
|
|
{
|
|
throw new InvalidAspectException("The aspect is invalid: " + aspectTypeQName, aspectTypeQName);
|
|
}
|
|
|
|
// Check the properties
|
|
if (aspectProperties == null)
|
|
{
|
|
// Make a map
|
|
aspectProperties = Collections.emptyMap();
|
|
}
|
|
// Make the properties immutable to be sure that they are not used incorrectly
|
|
aspectProperties = Collections.unmodifiableMap(aspectProperties);
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// Add aspect and defaults
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
boolean modified = addAspectsAndProperties(
|
|
nodePair,
|
|
aspectTypeQName,
|
|
null,
|
|
null,
|
|
Collections.singleton(aspectTypeQName),
|
|
aspectProperties,
|
|
false);
|
|
|
|
if (modified)
|
|
{
|
|
// Invoke policy behaviours
|
|
invokeOnUpdateNode(nodeRef);
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see Node#countChildAssocs()
|
|
*/
|
|
public int countChildAssocs(NodeRef nodeRef, boolean isPrimary) throws InvalidNodeRefException
|
|
{
|
|
final Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
final Long nodeId = nodePair.getFirst();
|
|
return nodeDAO.countChildAssocsByParent(nodeId, isPrimary);
|
|
}
|
|
|
|
public void removeAspect(NodeRef nodeRef, QName aspectTypeQName)
|
|
throws InvalidNodeRefException, InvalidAspectException
|
|
{
|
|
/*
|
|
* Note: Aspect and property removal is resilient to missing dictionary definitions
|
|
*/
|
|
// get the node
|
|
final Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
final Long nodeId = nodePair.getFirst();
|
|
|
|
boolean hadAspect = nodeDAO.hasNodeAspect(nodeId, aspectTypeQName);
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
if (hadAspect)
|
|
{
|
|
invokeBeforeRemoveAspect(nodeRef, aspectTypeQName);
|
|
nodeDAO.removeNodeAspects(nodeId, Collections.singleton(aspectTypeQName));
|
|
}
|
|
|
|
AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName);
|
|
boolean updated = false;
|
|
if (aspectDef != null)
|
|
{
|
|
// Remove default properties
|
|
Map<QName,PropertyDefinition> propertyDefs = aspectDef.getProperties();
|
|
Set<QName> propertyToRemoveQNames = propertyDefs.keySet();
|
|
nodeDAO.removeNodeProperties(nodeId, propertyToRemoveQNames);
|
|
|
|
// Remove child associations
|
|
// We have to iterate over the associations and remove all those between the parent and child
|
|
final List<Pair<Long, ChildAssociationRef>> assocsToDelete = new ArrayList<Pair<Long, ChildAssociationRef>>(5);
|
|
final List<Pair<Long, NodeRef>> nodesToDelete = new ArrayList<Pair<Long, NodeRef>>(5);
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
// Double check that it's not a primary association. If so, we can't delete it and
|
|
// have to delete the child node directly and with full archival.
|
|
if (childAssocPair.getSecond().isPrimary())
|
|
{
|
|
nodesToDelete.add(childNodePair);
|
|
}
|
|
else
|
|
{
|
|
assocsToDelete.add(childAssocPair);
|
|
}
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
// Get all the QNames to remove
|
|
Set<QName> assocTypeQNamesToRemove = new HashSet<QName>(aspectDef.getChildAssociations().keySet());
|
|
nodeDAO.getChildAssocs(nodeId, assocTypeQNamesToRemove, callback);
|
|
// Delete all the collected associations
|
|
for (Pair<Long, ChildAssociationRef> assocPair : assocsToDelete)
|
|
{
|
|
updated = true;
|
|
Long assocId = assocPair.getFirst();
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
// delete the association instance - it is not primary
|
|
invokeBeforeDeleteChildAssociation(assocRef);
|
|
nodeDAO.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(assocRef);
|
|
}
|
|
|
|
// Cascade-delete any nodes that were attached to primary associations
|
|
for (Pair<Long, NodeRef> childNodePair : nodesToDelete)
|
|
{
|
|
NodeRef childNodeRef = childNodePair.getSecond();
|
|
this.deleteNode(childNodeRef);
|
|
}
|
|
|
|
// Remove regular associations
|
|
Map<QName, AssociationDefinition> nodeAssocDefs = aspectDef.getAssociations();
|
|
Set<QName> nodeAssocTypeQNamesToRemove = new HashSet<QName>(13);
|
|
for (Map.Entry<QName, AssociationDefinition> entry : nodeAssocDefs.entrySet())
|
|
{
|
|
if (entry.getValue().isChild())
|
|
{
|
|
// Not interested in child assocs
|
|
continue;
|
|
}
|
|
nodeAssocTypeQNamesToRemove.add(entry.getKey());
|
|
}
|
|
int assocsDeleted = nodeDAO.removeNodeAssocsToAndFrom(nodeId, nodeAssocTypeQNamesToRemove);
|
|
updated = updated || assocsDeleted > 0;
|
|
}
|
|
|
|
// Invoke policy behaviours
|
|
if (updated)
|
|
{
|
|
invokeOnUpdateNode(nodeRef);
|
|
}
|
|
if (hadAspect)
|
|
{
|
|
invokeOnRemoveAspect(nodeRef, aspectTypeQName);
|
|
}
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
/**
|
|
* Performs a check on the set of node aspects
|
|
*/
|
|
public boolean hasAspect(NodeRef nodeRef, QName aspectQName) throws InvalidNodeRefException, InvalidAspectException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return nodeDAO.hasNodeAspect(nodePair.getFirst(), aspectQName);
|
|
}
|
|
|
|
public Set<QName> getAspects(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return nodeDAO.getNodeAspects(nodePair.getFirst());
|
|
}
|
|
|
|
/**
|
|
* Delete Node
|
|
*/
|
|
public void deleteNode(NodeRef nodeRef)
|
|
{
|
|
// Pair contains NodeId, NodeRef
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
Boolean requiresDelete = null;
|
|
|
|
// get the primary parent-child relationship before it is gone
|
|
Pair<Long, ChildAssociationRef> childAssocPair = nodeDAO.getPrimaryParentAssoc(nodeId);
|
|
ChildAssociationRef childAssocRef = childAssocPair.getSecond();
|
|
// get type and aspect QNames as they will be unavailable after the delete
|
|
QName nodeTypeQName = nodeDAO.getNodeType(nodeId);
|
|
Set<QName> nodeAspectQNames = nodeDAO.getNodeAspects(nodeId);
|
|
|
|
StoreRef storeRef = nodeRef.getStoreRef();
|
|
StoreRef archiveStoreRef = storeArchiveMap.get(storeRef);
|
|
|
|
/*
|
|
* Work out whether we need to archive or delete the node.
|
|
*/
|
|
|
|
if (archiveStoreRef == null)
|
|
{
|
|
// The store does not specify archiving
|
|
requiresDelete = true;
|
|
}
|
|
else
|
|
{
|
|
// get the type and check if we need archiving.
|
|
TypeDefinition typeDef = dictionaryService.getType(nodeTypeQName);
|
|
if (typeDef != null)
|
|
{
|
|
Boolean requiresArchive = typeDef.getArchive();
|
|
if (requiresArchive != null)
|
|
{
|
|
requiresDelete = !requiresArchive;
|
|
}
|
|
}
|
|
|
|
// If the type hasn't asked for deletion, check whether any applied aspects have
|
|
Iterator<QName> i = nodeAspectQNames.iterator();
|
|
while ((requiresDelete == null || !requiresDelete) && i.hasNext())
|
|
{
|
|
QName nodeAspectQName = i.next();
|
|
AspectDefinition aspectDef = dictionaryService.getAspect(nodeAspectQName);
|
|
if (aspectDef != null)
|
|
{
|
|
Boolean requiresArchive = aspectDef.getArchive();
|
|
if (requiresArchive != null)
|
|
{
|
|
requiresDelete = !requiresArchive;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Now we have worked out whether to archive or delete, go ahead and do it
|
|
*/
|
|
if (requiresDelete == null || requiresDelete)
|
|
{
|
|
// remove the deleted node from the list of new nodes
|
|
untrackNewNodeRef(nodeRef);
|
|
|
|
// track the deletion of this node - so we can prevent new associations to it.
|
|
trackDeletedNodeRef(nodeRef);
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeDeleteNode(nodeRef);
|
|
|
|
// Cascade delecte as required
|
|
deletePrimaryChildrenNotArchived(nodePair);
|
|
// perform a normal deletion
|
|
nodeDAO.deleteNode(nodeId);
|
|
|
|
// Propagate timestamps
|
|
propagateTimeStamps(childAssocRef);
|
|
// Invoke policy behaviours
|
|
invokeOnDeleteNode(childAssocRef, nodeTypeQName, nodeAspectQNames, false);
|
|
// Index
|
|
nodeIndexer.indexDeleteNode(childAssocRef);
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Go ahead and archive the node
|
|
*
|
|
* Archiving will take responsibility for firing the policy behaviours on
|
|
* the nodes it modifies.
|
|
*/
|
|
archiveNode(nodeRef, archiveStoreRef);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* delete primary children - private method for deleteNode.
|
|
*
|
|
* recurses through children when deleting a node. Does not archive.
|
|
*/
|
|
private void deletePrimaryChildrenNotArchived(Pair<Long, NodeRef> nodePair)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
// Get the node's primary children
|
|
final List<Pair<Long, NodeRef>> childNodePairs = new ArrayList<Pair<Long, NodeRef>>(5);
|
|
|
|
final Map<Long, ChildAssociationRef> childAssocRefsByChildId = new HashMap<Long, ChildAssociationRef>(5);
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
// Add it
|
|
childNodePairs.add(childNodePair);
|
|
childAssocRefsByChildId.put(childNodePair.getFirst(), childAssocPair.getSecond());
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
|
|
// Get all the QNames to remove
|
|
nodeDAO.getChildAssocs(nodeId, null, null, null, Boolean.TRUE, null, callback);
|
|
// Each child must be deleted
|
|
for (Pair<Long, NodeRef> childNodePair : childNodePairs)
|
|
{
|
|
// Fire node policies. This ensures that each node in the hierarchy gets a notification fired.
|
|
Long childNodeId = childNodePair.getFirst();
|
|
NodeRef childNodeRef = childNodePair.getSecond();
|
|
QName childNodeType = nodeDAO.getNodeType(childNodeId);
|
|
Set<QName> childNodeQNames = nodeDAO.getNodeAspects(childNodeId);
|
|
ChildAssociationRef childParentAssocRef = childAssocRefsByChildId.get(childNodeId);
|
|
|
|
// remove the deleted node from the list of new nodes
|
|
untrackNewNodeRef(childNodeRef);
|
|
|
|
// track the deletion of this node - so we can prevent new associations to it.
|
|
trackDeletedNodeRef(childNodeRef);
|
|
|
|
invokeBeforeDeleteNode(childNodeRef);
|
|
|
|
// Cascade first
|
|
// This ensures that the beforeDelete policy is fired for all nodes in the hierarchy before
|
|
// the actual delete starts.
|
|
deletePrimaryChildrenNotArchived(childNodePair);
|
|
// Delete the child
|
|
nodeDAO.deleteNode(childNodeId);
|
|
|
|
// Propagate timestamps
|
|
propagateTimeStamps(childParentAssocRef);
|
|
invokeOnDeleteNode(childParentAssocRef, childNodeType, childNodeQNames, false);
|
|
|
|
// Index
|
|
nodeIndexer.indexDeleteNode(childParentAssocRef);
|
|
|
|
// lose interest in tracking this node ref
|
|
untrackNewNodeRef(childNodeRef);
|
|
}
|
|
}
|
|
|
|
public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName)
|
|
{
|
|
return addChild(Collections.singletonList(parentRef), childRef, assocTypeQName, assocQName).get(0);
|
|
}
|
|
|
|
public List<ChildAssociationRef> addChild(Collection<NodeRef> parentRefs, NodeRef childRef, QName assocTypeQName, QName assocQName)
|
|
{
|
|
// Get the node's name, if present
|
|
Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childRef);
|
|
Long childNodeId = childNodePair.getFirst();
|
|
Map<QName, Serializable> childNodeProperties = nodeDAO.getNodeProperties(childNodePair.getFirst());
|
|
String childNodeName = extractNameProperty(childNodeProperties);
|
|
if (childNodeName == null)
|
|
{
|
|
childNodeName = childRef.getId();
|
|
}
|
|
|
|
List <ChildAssociationRef> childAssociationRefs = new ArrayList<ChildAssociationRef>(parentRefs.size());
|
|
List<Pair<Long, NodeRef>> parentNodePairs = new ArrayList<Pair<Long, NodeRef>>(parentRefs.size());
|
|
for (NodeRef parentRef : parentRefs)
|
|
{
|
|
if (isDeletedNodeRef(parentRef))
|
|
{
|
|
throw new InvalidNodeRefException("The parent node has been deleted", parentRef);
|
|
}
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
|
|
Long parentNodeId = parentNodePair.getFirst();
|
|
parentNodePairs.add(parentNodePair);
|
|
|
|
// make the association
|
|
Pair<Long, ChildAssociationRef> childAssocPair = nodeDAO.newChildAssoc(
|
|
parentNodeId, childNodeId,
|
|
assocTypeQName, assocQName,
|
|
childNodeName);
|
|
|
|
childAssociationRefs.add(childAssocPair.getSecond());
|
|
}
|
|
|
|
// check that the child addition of the child has not created a cyclic relationship
|
|
nodeDAO.cycleCheck(childNodeId);
|
|
|
|
// Invoke policy behaviours
|
|
for (ChildAssociationRef childAssocRef : childAssociationRefs)
|
|
{
|
|
invokeOnCreateChildAssociation(childAssocRef, false);
|
|
}
|
|
|
|
// Get the type associated with the association
|
|
// The association may be sourced on an aspect, which may itself mandate further aspects
|
|
for (Pair<Long, NodeRef> parentNodePair : parentNodePairs)
|
|
{
|
|
addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false);
|
|
}
|
|
|
|
// Index
|
|
for (ChildAssociationRef childAssocRef : childAssociationRefs)
|
|
{
|
|
nodeIndexer.indexCreateChildAssociation(childAssocRef);
|
|
}
|
|
|
|
return childAssociationRefs;
|
|
}
|
|
|
|
public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException
|
|
{
|
|
final Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
|
|
final Long parentNodeId = parentNodePair.getFirst();
|
|
final Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childRef);
|
|
final Long childNodeId = childNodePair.getFirst();
|
|
|
|
// Get the primary parent association for the child
|
|
Pair<Long, ChildAssociationRef> primaryChildAssocPair = nodeDAO.getPrimaryParentAssoc(childNodeId);
|
|
// We can shortcut if our parent is also the primary parent
|
|
if (primaryChildAssocPair != null)
|
|
{
|
|
NodeRef primaryParentNodeRef = primaryChildAssocPair.getSecond().getParentRef();
|
|
if (primaryParentNodeRef.equals(parentRef))
|
|
{
|
|
// Shortcut - just delete the child node
|
|
deleteNode(childRef);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// We have to iterate over the associations and remove all those between the parent and child
|
|
final List<Pair<Long, ChildAssociationRef>> assocsToDelete = new ArrayList<Pair<Long, ChildAssociationRef>>(5);
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
// Ignore if the child is not ours (redundant check)
|
|
if (!childNodePair.getFirst().equals(childNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
// Add it
|
|
assocsToDelete.add(childAssocPair);
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
nodeDAO.getChildAssocs(parentNodeId, childNodeId, null, null, null, null, callback);
|
|
|
|
// Delete all the collected associations
|
|
for (Pair<Long, ChildAssociationRef> assocPair : assocsToDelete)
|
|
{
|
|
Long assocId = assocPair.getFirst();
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
// delete the association instance - it is not primary
|
|
invokeBeforeDeleteChildAssociation(assocRef);
|
|
nodeDAO.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(assocRef);
|
|
|
|
// Index
|
|
nodeIndexer.indexDeleteChildAssociation(assocRef);
|
|
}
|
|
|
|
// Done
|
|
}
|
|
|
|
public boolean removeChildAssociation(ChildAssociationRef childAssocRef)
|
|
{
|
|
Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst();
|
|
Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst();
|
|
QName assocTypeQName = childAssocRef.getTypeQName();
|
|
QName assocQName = childAssocRef.getQName();
|
|
Pair<Long, ChildAssociationRef> assocPair = nodeDAO.getChildAssoc(
|
|
parentNodeId, childNodeId, assocTypeQName, assocQName);
|
|
if (assocPair == null)
|
|
{
|
|
// No association exists
|
|
return false;
|
|
}
|
|
Long assocId = assocPair.getFirst();
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
if (assocRef.isPrimary())
|
|
{
|
|
NodeRef childNodeRef = assocRef.getChildRef();
|
|
// Delete the child node
|
|
this.deleteNode(childNodeRef);
|
|
// Done
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Delete the association
|
|
invokeBeforeDeleteChildAssociation(childAssocRef);
|
|
nodeDAO.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(childAssocRef);
|
|
// Index
|
|
nodeIndexer.indexDeleteChildAssociation(childAssocRef);
|
|
// Done
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean removeSecondaryChildAssociation(ChildAssociationRef childAssocRef)
|
|
{
|
|
Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst();
|
|
Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst();
|
|
QName assocTypeQName = childAssocRef.getTypeQName();
|
|
QName assocQName = childAssocRef.getQName();
|
|
Pair<Long, ChildAssociationRef> assocPair = nodeDAO.getChildAssoc(
|
|
parentNodeId, childNodeId, assocTypeQName, assocQName);
|
|
if (assocPair == null)
|
|
{
|
|
// No association exists
|
|
return false;
|
|
}
|
|
Long assocId = assocPair.getFirst();
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
if (assocRef.isPrimary())
|
|
{
|
|
throw new IllegalArgumentException(
|
|
"removeSeconaryChildAssociation can not be applied to a primary association: \n" +
|
|
" Child Assoc: " + assocRef);
|
|
}
|
|
// Delete the secondary association
|
|
nodeDAO.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(childAssocRef);
|
|
// Index
|
|
nodeIndexer.indexDeleteChildAssociation(childAssocRef);
|
|
// Done
|
|
return true;
|
|
}
|
|
|
|
public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
|
|
{
|
|
Long nodeId = getNodePairNotNull(nodeRef).getFirst();
|
|
// Spoof referencable properties
|
|
if (qname.equals(ContentModel.PROP_STORE_PROTOCOL))
|
|
{
|
|
return nodeRef.getStoreRef().getProtocol();
|
|
}
|
|
else if (qname.equals(ContentModel.PROP_STORE_IDENTIFIER))
|
|
{
|
|
return nodeRef.getStoreRef().getIdentifier();
|
|
}
|
|
else if (qname.equals(ContentModel.PROP_NODE_UUID))
|
|
{
|
|
return nodeRef.getId();
|
|
}
|
|
else if (qname.equals(ContentModel.PROP_NODE_DBID))
|
|
{
|
|
return nodeId;
|
|
}
|
|
|
|
Serializable property = nodeDAO.getNodeProperty(nodeId, qname);
|
|
|
|
// check if we need to provide a spoofed name
|
|
if (property == null && qname.equals(ContentModel.PROP_NAME))
|
|
{
|
|
return nodeRef.getId();
|
|
}
|
|
|
|
// done
|
|
return property;
|
|
}
|
|
|
|
public Map<QName, Serializable> getProperties(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return getPropertiesImpl(nodePair);
|
|
}
|
|
|
|
/**
|
|
* Gets, converts and adds the intrinsic properties to the current node's properties
|
|
*/
|
|
private Map<QName, Serializable> getPropertiesImpl(Pair<Long, NodeRef> nodePair) throws InvalidNodeRefException
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
Map<QName, Serializable> nodeProperties = nodeDAO.getNodeProperties(nodeId);
|
|
// done
|
|
return nodeProperties;
|
|
}
|
|
|
|
public Long getNodeAclId(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return getAclIDImpl(nodePair);
|
|
}
|
|
|
|
/**
|
|
* Gets, converts and adds the intrinsic properties to the current node's properties
|
|
*/
|
|
private Long getAclIDImpl(Pair<Long, NodeRef> nodePair) throws InvalidNodeRefException
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
Long aclID = nodeDAO.getNodeAclId(nodeId);
|
|
// done
|
|
return aclID;
|
|
}
|
|
|
|
/**
|
|
* Performs additional tasks associated with setting a property.
|
|
*
|
|
* @return Returns <tt>true</tt> if any work was done by this method
|
|
*/
|
|
private boolean setPropertiesCommonWork(Pair<Long, NodeRef> nodePair, Map<QName, Serializable> properties)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
boolean changed = false;
|
|
// cm:name special handling
|
|
if (properties.containsKey(ContentModel.PROP_NAME))
|
|
{
|
|
String name = extractNameProperty(properties);
|
|
Pair<Long, ChildAssociationRef> primaryParentAssocPair = nodeDAO.getPrimaryParentAssoc(nodeId);
|
|
if (primaryParentAssocPair != null)
|
|
{
|
|
String oldName = extractNameProperty(nodeDAO.getNodeProperties(nodeId));
|
|
String newName = DefaultTypeConverter.INSTANCE.convert(String.class, name);
|
|
changed = setChildNameUnique(nodePair, newName, oldName);
|
|
}
|
|
}
|
|
// Done
|
|
return changed;
|
|
}
|
|
|
|
/**
|
|
* Gets the properties map, sets the value (null is allowed) and checks that the new set
|
|
* of properties is valid.
|
|
*
|
|
* @see DbNodeServiceImpl.NullPropertyValue
|
|
*/
|
|
public void setProperty(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException
|
|
{
|
|
ParameterCheck.mandatory("nodeRef", nodeRef);
|
|
ParameterCheck.mandatory("qname", qname);
|
|
|
|
// The UUID cannot be explicitly changed
|
|
if (qname.equals(ContentModel.PROP_NODE_UUID))
|
|
{
|
|
throw new IllegalArgumentException("The node UUID cannot be changed.");
|
|
}
|
|
|
|
// get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
|
|
// Invoke policy behaviour
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// cm:name special handling
|
|
setPropertiesCommonWork(
|
|
nodePair,
|
|
Collections.singletonMap(qname, value));
|
|
|
|
// Add the property and all required defaults
|
|
boolean changed = addAspectsAndProperties(
|
|
nodePair, null,
|
|
null, null,
|
|
null, Collections.singletonMap(qname, value), false);
|
|
|
|
if (changed)
|
|
{
|
|
// Invoke policy behaviour
|
|
invokeOnUpdateNode(nodeRef);
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures that all required properties are present on the node and copies the
|
|
* property values to the <code>Node</code>.
|
|
* <p>
|
|
* To remove a property, <b>remove it from the map</b> before calling this method.
|
|
* Null-valued properties are allowed.
|
|
* <p>
|
|
* If any of the values are null, a marker object is put in to mimic nulls. They will be turned back into
|
|
* a real nulls when the properties are requested again.
|
|
*
|
|
* @see Node#getProperties()
|
|
*/
|
|
public void setProperties(NodeRef nodeRef, Map<QName, Serializable> properties) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// SetProperties common tasks
|
|
setPropertiesCommonWork(nodePair, properties);
|
|
|
|
// Set properties and defaults, overwriting the existing properties
|
|
boolean changed = addAspectsAndProperties(nodePair, null, null, null, null, properties, true);
|
|
|
|
if (changed)
|
|
{
|
|
// Invoke policy behaviours
|
|
invokeOnUpdateNode(nodeRef);
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
}
|
|
|
|
public void addProperties(NodeRef nodeRef, Map<QName, Serializable> properties) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// cm:name special handling
|
|
setPropertiesCommonWork(nodePair, properties);
|
|
|
|
// Add properties and defaults
|
|
boolean changed = addAspectsAndProperties(nodePair, null, null, null, null, properties, false);
|
|
|
|
if (changed)
|
|
{
|
|
// Invoke policy behaviours
|
|
invokeOnUpdateNode(nodeRef);
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
}
|
|
|
|
public void removeProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// Get the values before
|
|
Map<QName, Serializable> propertiesBefore = getPropertiesImpl(nodePair);
|
|
|
|
// cm:name special handling
|
|
if (qname.equals(ContentModel.PROP_NAME))
|
|
{
|
|
String oldName = extractNameProperty(nodeDAO.getNodeProperties(nodeId));
|
|
String newName = null;
|
|
setChildNameUnique(nodePair, newName, oldName);
|
|
}
|
|
|
|
// Remove
|
|
nodeDAO.removeNodeProperties(nodeId, Collections.singleton(qname));
|
|
|
|
// Invoke policy behaviours
|
|
Map<QName, Serializable> propertiesAfter = getPropertiesImpl(nodePair);
|
|
invokeOnUpdateNode(nodeRef);
|
|
invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter);
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
public Collection<NodeRef> getParents(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
List<ChildAssociationRef> parentAssocs = getParentAssocs(
|
|
nodeRef,
|
|
RegexQNamePattern.MATCH_ALL,
|
|
RegexQNamePattern.MATCH_ALL);
|
|
|
|
// Copy into the set to avoid duplicates
|
|
Set<NodeRef> parentNodeRefs = new HashSet<NodeRef>(parentAssocs.size());
|
|
for (ChildAssociationRef parentAssoc : parentAssocs)
|
|
{
|
|
NodeRef parentNodeRef = parentAssoc.getParentRef();
|
|
parentNodeRefs.add(parentNodeRef);
|
|
}
|
|
// Done
|
|
return new ArrayList<NodeRef>(parentNodeRefs);
|
|
}
|
|
|
|
/**
|
|
* Filters out any associations if their qname is not a match to the given pattern.
|
|
*/
|
|
public List<ChildAssociationRef> getParentAssocs(
|
|
final NodeRef nodeRef,
|
|
final QNamePattern typeQNamePattern,
|
|
final QNamePattern qnamePattern)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(10);
|
|
// We have a callback handler to filter results
|
|
ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
if (!typeQNamePattern.isMatch(childAssocPair.getSecond().getTypeQName()))
|
|
{
|
|
return true;
|
|
}
|
|
if (!qnamePattern.isMatch(childAssocPair.getSecond().getQName()))
|
|
{
|
|
return true;
|
|
}
|
|
results.add(childAssocPair.getSecond());
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
|
|
// Get the assocs pointing to it
|
|
QName typeQName = (typeQNamePattern instanceof QName) ? (QName) typeQNamePattern : null;
|
|
QName qname = (qnamePattern instanceof QName) ? (QName) qnamePattern : null;
|
|
|
|
nodeDAO.getParentAssocs(nodeId, typeQName, qname, null, callback);
|
|
// done
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Filters out any associations if their qname is not a match to the given pattern.
|
|
*/
|
|
public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, final QNamePattern typeQNamePattern, final QNamePattern qnamePattern)
|
|
{
|
|
return getChildAssocs(nodeRef, typeQNamePattern, qnamePattern, true) ;
|
|
}
|
|
|
|
/**
|
|
* Filters out any associations if their qname is not a match to the given pattern.
|
|
*/
|
|
public List<ChildAssociationRef> getChildAssocs(
|
|
NodeRef nodeRef,
|
|
final QNamePattern typeQNamePattern,
|
|
final QNamePattern qnamePattern,
|
|
final boolean preload)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(10);
|
|
// We have a callback handler to filter results
|
|
ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return preload;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
if (!typeQNamePattern.isMatch(childAssocPair.getSecond().getTypeQName()))
|
|
{
|
|
return true;
|
|
}
|
|
if (!qnamePattern.isMatch(childAssocPair.getSecond().getQName()))
|
|
{
|
|
return true;
|
|
}
|
|
results.add(childAssocPair.getSecond());
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
|
|
// Get the assocs pointing to it
|
|
QName typeQName = (typeQNamePattern instanceof QName) ? (QName) typeQNamePattern : null;
|
|
QName qname = (qnamePattern instanceof QName) ? (QName) qnamePattern : null;
|
|
|
|
nodeDAO.getChildAssocs(nodeId, null, typeQName, qname, null, null, callback);
|
|
// Done
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Fetches the first n child associations in an efficient manner
|
|
*/
|
|
public List<ChildAssociationRef> getChildAssocs(
|
|
NodeRef nodeRef,
|
|
final QName typeQName,
|
|
final QName qname,
|
|
final int maxResults,
|
|
final boolean preload)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
|
|
// We have a callback handler to filter results
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(10);
|
|
ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return preload;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
// Get the assocs pointing to it
|
|
nodeDAO.getChildAssocs(nodePair.getFirst(), typeQName, qname, maxResults, callback);
|
|
// Done
|
|
return results;
|
|
}
|
|
|
|
public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, Set<QName> childNodeTypeQNames)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
|
|
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
// Get all child associations with the specific qualified name
|
|
nodeDAO.getChildAssocsByChildTypes(nodeId, childNodeTypeQNames, callback);
|
|
// Done
|
|
return results;
|
|
}
|
|
|
|
public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
Pair<Long, ChildAssociationRef> childAssocPair = nodeDAO.getChildAssoc(nodeId, assocTypeQName, childName);
|
|
if (childAssocPair != null)
|
|
{
|
|
return childAssocPair.getSecond().getChildRef();
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public List<ChildAssociationRef> getChildrenByName(NodeRef nodeRef, QName assocTypeQName, Collection<String> childNames)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
|
|
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
// Get all child associations with the specific qualified name
|
|
nodeDAO.getChildAssocs(nodeId, assocTypeQName, childNames, callback);
|
|
// Done
|
|
return results;
|
|
}
|
|
|
|
public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// get the primary parent assoc
|
|
Pair<Long, ChildAssociationRef> assocPair = nodeDAO.getPrimaryParentAssoc(nodeId);
|
|
|
|
// done - the assoc may be null for a root node
|
|
ChildAssociationRef assocRef = null;
|
|
if (assocPair == null)
|
|
{
|
|
assocRef = new ChildAssociationRef(null, null, null, nodeRef);
|
|
}
|
|
else
|
|
{
|
|
assocRef = assocPair.getSecond();
|
|
}
|
|
return assocRef;
|
|
}
|
|
|
|
@Override
|
|
public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
|
|
throws InvalidNodeRefException, AssociationExistsException
|
|
{
|
|
Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
|
|
long sourceNodeId = sourceNodePair.getFirst();
|
|
Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
|
|
long targetNodeId = targetNodePair.getFirst();
|
|
|
|
// we are sure that the association doesn't exist - make it
|
|
Long assocId = nodeDAO.newNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName, -1);
|
|
AssociationRef assocRef = new AssociationRef(assocId, sourceRef, assocTypeQName, targetRef);
|
|
|
|
// Invoke policy behaviours
|
|
invokeOnCreateAssociation(assocRef);
|
|
|
|
// Add missing aspects
|
|
addAspectsAndPropertiesAssoc(sourceNodePair, assocTypeQName, null, null, null, null, false);
|
|
|
|
return assocRef;
|
|
}
|
|
|
|
@Override
|
|
public void setAssociations(NodeRef sourceRef, QName assocTypeQName, List<NodeRef> targetRefs)
|
|
{
|
|
Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
|
|
Long sourceNodeId = sourceNodePair.getFirst();
|
|
// First get the existing associations
|
|
Collection<Pair<Long, AssociationRef>> assocsBefore = nodeDAO.getTargetNodeAssocs(sourceNodeId, assocTypeQName);
|
|
Map<NodeRef, Long> targetRefsBefore = new HashMap<NodeRef, Long>(assocsBefore.size());
|
|
Map<NodeRef, Long> toRemoveMap = new HashMap<NodeRef, Long>(assocsBefore.size());
|
|
for (Pair<Long, AssociationRef> assocBeforePair : assocsBefore)
|
|
{
|
|
Long id = assocBeforePair.getFirst();
|
|
NodeRef nodeRef = assocBeforePair.getSecond().getTargetRef();
|
|
targetRefsBefore.put(nodeRef, id);
|
|
toRemoveMap.put(nodeRef, id);
|
|
}
|
|
// Work out which associations need to be removed
|
|
toRemoveMap.keySet().removeAll(targetRefs);
|
|
List<Long> toRemoveIds = new ArrayList<Long>(toRemoveMap.values());
|
|
nodeDAO.removeNodeAssocs(toRemoveIds);
|
|
|
|
// Work out which associations need to be added
|
|
Set<NodeRef> toAdd = new HashSet<NodeRef>(targetRefs);
|
|
toAdd.removeAll(targetRefsBefore.keySet());
|
|
|
|
// Iterate over the desired result and create new or reset indexes
|
|
int assocIndex = 1;
|
|
for (NodeRef targetNodeRef : targetRefs)
|
|
{
|
|
Long id = targetRefsBefore.get(targetNodeRef);
|
|
// Is this an existing assoc?
|
|
if (id != null)
|
|
{
|
|
// Update it
|
|
nodeDAO.setNodeAssocIndex(id, assocIndex);
|
|
}
|
|
else
|
|
{
|
|
Long targetNodeId = getNodePairNotNull(targetNodeRef).getFirst();
|
|
nodeDAO.newNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName, assocIndex);
|
|
}
|
|
assocIndex++;
|
|
}
|
|
|
|
// Invoke policy behaviours
|
|
for (NodeRef targetNodeRef : toAdd)
|
|
{
|
|
AssociationRef assocRef = new AssociationRef(sourceRef, assocTypeQName, targetNodeRef);
|
|
invokeOnCreateAssociation(assocRef);
|
|
}
|
|
}
|
|
|
|
public Collection<ChildAssociationRef> getChildAssocsWithoutParentAssocsOfType(NodeRef parent, QName assocTypeQName)
|
|
{
|
|
// Get the parent node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(parent);
|
|
Long parentNodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
|
|
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair, Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
// Get the child associations that meet the criteria
|
|
nodeDAO.getChildAssocsWithoutParentAssocsOfType(parentNodeId, assocTypeQName, callback);
|
|
// done
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Specific properties <b>not</b> supported by {@link #getChildAssocsByPropertyValue(NodeRef, QName, Serializable)}
|
|
*/
|
|
private static List<QName> getChildAssocsByPropertyValueBannedProps = new ArrayList<QName>();
|
|
static
|
|
{
|
|
getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_NODE_DBID);
|
|
getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_NODE_UUID);
|
|
getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_NAME);
|
|
getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_MODIFIED);
|
|
getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_MODIFIER);
|
|
getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_CREATED);
|
|
getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_CREATOR);
|
|
}
|
|
|
|
@Override
|
|
public List<ChildAssociationRef> getChildAssocsByPropertyValue(
|
|
NodeRef nodeRef,
|
|
QName propertyQName,
|
|
Serializable value)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// Check the QName is not one of the "special" system maintained ones.
|
|
|
|
if (getChildAssocsByPropertyValueBannedProps.contains(propertyQName))
|
|
{
|
|
throw new IllegalArgumentException(
|
|
"getChildAssocsByPropertyValue does not allow search of system maintained properties: " + propertyQName);
|
|
}
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(10);
|
|
// We have a callback handler to filter results
|
|
ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
// Get the assocs pointing to it
|
|
nodeDAO.getChildAssocsByPropertyValue(nodeId, propertyQName, value, callback);
|
|
// Done
|
|
return results;
|
|
}
|
|
|
|
public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
|
|
throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
|
|
Long sourceNodeId = sourceNodePair.getFirst();
|
|
Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
|
|
Long targetNodeId = targetNodePair.getFirst();
|
|
|
|
// delete it
|
|
int assocsDeleted = nodeDAO.removeNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName);
|
|
|
|
if (assocsDeleted > 0)
|
|
{
|
|
AssociationRef assocRef = new AssociationRef(sourceRef, assocTypeQName, targetRef);
|
|
// Invoke policy behaviours
|
|
invokeOnDeleteAssociation(assocRef);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public AssociationRef getAssoc(Long id)
|
|
{
|
|
Pair<Long, AssociationRef> nodeAssocPair = nodeDAO.getNodeAssocOrNull(id);
|
|
return nodeAssocPair == null ? null : nodeAssocPair.getSecond();
|
|
}
|
|
|
|
public List<AssociationRef> getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern)
|
|
{
|
|
Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
|
|
Long sourceNodeId = sourceNodePair.getFirst();
|
|
|
|
QName qnameFilter = null;
|
|
if (qnamePattern instanceof QName)
|
|
{
|
|
qnameFilter = (QName) qnamePattern;
|
|
}
|
|
Collection<Pair<Long, AssociationRef>> assocPairs = nodeDAO.getTargetNodeAssocs(sourceNodeId, qnameFilter);
|
|
List<AssociationRef> nodeAssocRefs = new ArrayList<AssociationRef>(assocPairs.size());
|
|
for (Pair<Long, AssociationRef> assocPair : assocPairs)
|
|
{
|
|
AssociationRef assocRef = assocPair.getSecond();
|
|
// check qname pattern, if not already filtered
|
|
if (qnameFilter == null && !qnamePattern.isMatch(assocRef.getTypeQName()))
|
|
{
|
|
continue; // the assoc name doesn't match the pattern given
|
|
}
|
|
nodeAssocRefs.add(assocRef);
|
|
}
|
|
// done
|
|
return nodeAssocRefs;
|
|
}
|
|
|
|
public List<AssociationRef> getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern)
|
|
{
|
|
Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
|
|
Long targetNodeId = targetNodePair.getFirst();
|
|
|
|
QName qnameFilter = null;
|
|
if (qnamePattern instanceof QName)
|
|
{
|
|
qnameFilter = (QName) qnamePattern;
|
|
}
|
|
Collection<Pair<Long, AssociationRef>> assocPairs = nodeDAO.getSourceNodeAssocs(targetNodeId, qnameFilter);
|
|
List<AssociationRef> nodeAssocRefs = new ArrayList<AssociationRef>(assocPairs.size());
|
|
for (Pair<Long, AssociationRef> assocPair : assocPairs)
|
|
{
|
|
AssociationRef assocRef = assocPair.getSecond();
|
|
// check qname pattern, if not already filtered
|
|
if (qnameFilter == null && !qnamePattern.isMatch(assocRef.getTypeQName()))
|
|
{
|
|
continue; // the assoc name doesn't match the pattern given
|
|
}
|
|
nodeAssocRefs.add(assocRef);
|
|
}
|
|
// done
|
|
return nodeAssocRefs;
|
|
}
|
|
|
|
/**
|
|
* @see #getPaths(NodeRef, boolean)
|
|
* @see #prependPaths(Node, Path, Collection, Stack, boolean)
|
|
*/
|
|
public Path getPath(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
List<Path> paths = getPaths(nodeRef, true); // checks primary path count
|
|
if (paths.size() == 1)
|
|
{
|
|
return paths.get(0); // we know there is only one
|
|
}
|
|
throw new RuntimeException("Primary path count not checked"); // checked by getPaths()
|
|
}
|
|
|
|
/**
|
|
* When searching for <code>primaryOnly == true</code>, checks that there is exactly
|
|
* one path.
|
|
* @see #prependPaths(Node, Path, Collection, Stack, boolean)
|
|
*/
|
|
public List<Path> getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException
|
|
{
|
|
// get the starting node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
|
|
return nodeDAO.getPaths(nodePair, primaryOnly);
|
|
}
|
|
|
|
/**
|
|
* Archives the node without the <b>cm:auditable</b> aspect behaviour
|
|
*/
|
|
private void archiveNode(NodeRef nodeRef, StoreRef archiveStoreRef)
|
|
{
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
try
|
|
{
|
|
archiveNodeImpl(nodeRef, archiveStoreRef);
|
|
}
|
|
finally
|
|
{
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
}
|
|
}
|
|
|
|
private void archiveNodeImpl(NodeRef nodeRef, StoreRef archiveStoreRef)
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
Pair<Long, ChildAssociationRef> primaryParentAssocPair = nodeDAO.getPrimaryParentAssoc(nodeId);
|
|
Set<QName> newAspects = new HashSet<QName>(5);
|
|
Map<QName, Serializable> existingProperties = nodeDAO.getNodeProperties(nodeId);
|
|
Map<QName, Serializable> newProperties = new HashMap<QName, Serializable>(11);
|
|
|
|
// move the node
|
|
Pair<Long, NodeRef> archiveStoreRootNodePair = nodeDAO.getRootNode(archiveStoreRef);
|
|
Pair<Long, NodeRef> newNodePair = null;
|
|
try
|
|
{
|
|
ChildAssociationRef newPrimaryParentAssocPair = moveNode(
|
|
nodeRef,
|
|
archiveStoreRootNodePair.getSecond(),
|
|
ContentModel.ASSOC_CHILDREN,
|
|
NodeArchiveService.QNAME_ARCHIVED_ITEM);
|
|
newNodePair = getNodePairNotNull(newPrimaryParentAssocPair.getChildRef());
|
|
}
|
|
catch (NodeExistsException e)
|
|
{
|
|
// Clear out the offending node and try again
|
|
deleteNode(e.getNodePair().getSecond());
|
|
ChildAssociationRef newPrimaryParentAssocPair = moveNode(
|
|
nodeRef,
|
|
archiveStoreRootNodePair.getSecond(),
|
|
ContentModel.ASSOC_CHILDREN,
|
|
NodeArchiveService.QNAME_ARCHIVED_ITEM);
|
|
newNodePair = getNodePairNotNull(newPrimaryParentAssocPair.getChildRef());
|
|
}
|
|
|
|
// add the aspect
|
|
newAspects.add(ContentModel.ASPECT_ARCHIVED);
|
|
newProperties.put(ContentModel.PROP_ARCHIVED_BY, AuthenticationUtil.getFullyAuthenticatedUser());
|
|
newProperties.put(ContentModel.PROP_ARCHIVED_DATE, new Date());
|
|
newProperties.put(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC, primaryParentAssocPair.getSecond());
|
|
Serializable originalOwner = existingProperties.get(ContentModel.PROP_OWNER);
|
|
Serializable originalCreator = existingProperties.get(ContentModel.PROP_CREATOR);
|
|
if (originalOwner != null || originalCreator != null)
|
|
{
|
|
newProperties.put(
|
|
ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER,
|
|
originalOwner != null ? originalOwner : originalCreator);
|
|
}
|
|
// change the node ownership
|
|
newAspects.add(ContentModel.ASPECT_OWNABLE);
|
|
newProperties.put(ContentModel.PROP_OWNER, AuthenticationUtil.getFullyAuthenticatedUser());
|
|
|
|
// Set the aspects and properties
|
|
addAspectsAndProperties(newNodePair, null, null, null, newAspects, newProperties, false);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* Archives the node without the <b>cm:auditable</b> aspect behaviour
|
|
*/
|
|
public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName, QName assocQName)
|
|
{
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
try
|
|
{
|
|
return restoreNodeImpl(archivedNodeRef, destinationParentNodeRef, assocTypeQName, assocQName);
|
|
}
|
|
finally
|
|
{
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
}
|
|
}
|
|
|
|
private NodeRef restoreNodeImpl(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName, QName assocQName)
|
|
{
|
|
Pair<Long, NodeRef> archivedNodePair = getNodePairNotNull(archivedNodeRef);
|
|
Long archivedNodeId = archivedNodePair.getFirst();
|
|
Set<QName> existingAspects = nodeDAO.getNodeAspects(archivedNodeId);
|
|
Set<QName> newAspects = new HashSet<QName>(5);
|
|
Map<QName, Serializable> existingProperties = nodeDAO.getNodeProperties(archivedNodeId);
|
|
Map<QName, Serializable> newProperties = new HashMap<QName, Serializable>(11);
|
|
|
|
// the node must be a top-level archive node
|
|
if (!existingAspects.contains(ContentModel.ASPECT_ARCHIVED))
|
|
{
|
|
throw new AlfrescoRuntimeException("The node to restore is not an archive node");
|
|
}
|
|
ChildAssociationRef originalPrimaryParentAssocRef = (ChildAssociationRef) existingProperties.get(
|
|
ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC);
|
|
Serializable originalOwner = existingProperties.get(ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER);
|
|
// remove the archived aspect
|
|
Set<QName> removePropertyQNames = new HashSet<QName>(11);
|
|
removePropertyQNames.add(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC);
|
|
removePropertyQNames.add(ContentModel.PROP_ARCHIVED_BY);
|
|
removePropertyQNames.add(ContentModel.PROP_ARCHIVED_DATE);
|
|
removePropertyQNames.add(ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER);
|
|
nodeDAO.removeNodeProperties(archivedNodeId, removePropertyQNames);
|
|
nodeDAO.removeNodeAspects(archivedNodeId, Collections.singleton(ContentModel.ASPECT_ARCHIVED));
|
|
|
|
// restore the original ownership
|
|
if (originalOwner != null)
|
|
{
|
|
newAspects.add(ContentModel.ASPECT_OWNABLE);
|
|
newProperties.put(ContentModel.PROP_OWNER, originalOwner);
|
|
}
|
|
|
|
if (destinationParentNodeRef == null)
|
|
{
|
|
// we must restore to the original location
|
|
destinationParentNodeRef = originalPrimaryParentAssocRef.getParentRef();
|
|
}
|
|
// check the associations
|
|
if (assocTypeQName == null)
|
|
{
|
|
assocTypeQName = originalPrimaryParentAssocRef.getTypeQName();
|
|
}
|
|
if (assocQName == null)
|
|
{
|
|
assocQName = originalPrimaryParentAssocRef.getQName();
|
|
}
|
|
|
|
// move the node to the target parent, which may or may not be the original parent
|
|
ChildAssociationRef newChildAssocRef = moveNode(
|
|
archivedNodeRef,
|
|
destinationParentNodeRef,
|
|
assocTypeQName,
|
|
assocQName);
|
|
|
|
// the node reference has changed due to the store move
|
|
NodeRef restoredNodeRef = newChildAssocRef.getChildRef();
|
|
invokeOnRestoreNode(newChildAssocRef);
|
|
// done
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Restored node: \n" +
|
|
" original noderef: " + archivedNodeRef + "\n" +
|
|
" restored noderef: " + restoredNodeRef + "\n" +
|
|
" new parent: " + destinationParentNodeRef);
|
|
}
|
|
return restoredNodeRef;
|
|
}
|
|
|
|
/**
|
|
* Move Node
|
|
*
|
|
* Drops the old primary association and creates a new one
|
|
*/
|
|
public ChildAssociationRef moveNode(
|
|
NodeRef nodeToMoveRef,
|
|
NodeRef newParentRef,
|
|
QName assocTypeQName,
|
|
QName assocQName)
|
|
{
|
|
if (isDeletedNodeRef(newParentRef))
|
|
{
|
|
throw new InvalidNodeRefException("The parent node has been deleted", newParentRef);
|
|
}
|
|
|
|
Pair<Long, NodeRef> nodeToMovePair = getNodePairNotNull(nodeToMoveRef);
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(newParentRef);
|
|
|
|
Long nodeToMoveId = nodeToMovePair.getFirst();
|
|
QName nodeToMoveTypeQName = nodeDAO.getNodeType(nodeToMoveId);
|
|
NodeRef oldNodeToMoveRef = nodeToMovePair.getSecond();
|
|
Long parentNodeId = parentNodePair.getFirst();
|
|
NodeRef parentNodeRef = parentNodePair.getSecond();
|
|
StoreRef oldStoreRef = oldNodeToMoveRef.getStoreRef();
|
|
StoreRef newStoreRef = parentNodeRef.getStoreRef();
|
|
|
|
// Get the primary parent association
|
|
Pair<Long, ChildAssociationRef> oldParentAssocPair = nodeDAO.getPrimaryParentAssoc(nodeToMoveId);
|
|
if (oldParentAssocPair == null)
|
|
{
|
|
// The node doesn't have parent. Moving it is not possible.
|
|
throw new IllegalArgumentException("Node " + nodeToMoveId + " doesn't have a parent. Use 'addChild' instead of move.");
|
|
}
|
|
ChildAssociationRef oldParentAssocRef = oldParentAssocPair.getSecond();
|
|
|
|
// Get the aspects for later use
|
|
Set<QName> nodeToMoveAspectQNames = nodeDAO.getNodeAspects(nodeToMoveId);
|
|
|
|
boolean movingStore = !oldStoreRef.equals(newStoreRef);
|
|
|
|
// Invoke "Before"policy behaviour
|
|
if (movingStore)
|
|
{
|
|
// remove the deleted node from the list of new nodes
|
|
untrackNewNodeRef(nodeToMoveRef);
|
|
|
|
// track the deletion of this node - so we can prevent new associations to it.
|
|
trackDeletedNodeRef(nodeToMoveRef);
|
|
|
|
invokeBeforeDeleteNode(nodeToMoveRef);
|
|
invokeBeforeCreateNode(newParentRef, assocTypeQName, assocQName, nodeToMoveTypeQName);
|
|
}
|
|
else
|
|
{
|
|
invokeBeforeDeleteChildAssociation(oldParentAssocRef);
|
|
}
|
|
|
|
// Move node under the new parent
|
|
Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>> moveNodeResult = nodeDAO.moveNode(
|
|
nodeToMoveId,
|
|
parentNodeId,
|
|
assocTypeQName,
|
|
assocQName);
|
|
Pair<Long, ChildAssociationRef> newParentAssocPair = moveNodeResult.getFirst();
|
|
Pair<Long, NodeRef> newNodeToMovePair = moveNodeResult.getSecond();
|
|
ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();
|
|
|
|
// Handle indexing differently if it is a store move
|
|
if (movingStore)
|
|
{
|
|
// The association existed before and the node is moving to a new store
|
|
nodeIndexer.indexDeleteNode(oldParentAssocRef);
|
|
nodeIndexer.indexCreateNode(newParentAssocRef);
|
|
}
|
|
else
|
|
{
|
|
// The node is in the same store and is just having it's child association modified
|
|
nodeIndexer.indexUpdateChildAssociation(oldParentAssocRef, newParentAssocRef);
|
|
}
|
|
|
|
// Call behaviours
|
|
if (movingStore)
|
|
{
|
|
// Propagate timestamps
|
|
propagateTimeStamps(oldParentAssocRef);
|
|
propagateTimeStamps(newParentAssocRef);
|
|
|
|
// The Node changes NodeRefs, so this is really the deletion of the old node and creation
|
|
// of a node in a new store as far as the clients are concerned.
|
|
invokeOnDeleteNode(oldParentAssocRef, nodeToMoveTypeQName, nodeToMoveAspectQNames, true);
|
|
invokeOnCreateNode(newParentAssocRef);
|
|
|
|
// Pull children to the new store
|
|
pullNodeChildrenToSameStore(newNodeToMovePair);
|
|
}
|
|
else
|
|
{
|
|
// Propagate timestamps (watch out for moves within the same folder)
|
|
if (!oldParentAssocRef.getParentRef().equals(newParentAssocRef.getParentRef()))
|
|
{
|
|
propagateTimeStamps(oldParentAssocRef);
|
|
propagateTimeStamps(newParentAssocRef);
|
|
}
|
|
else
|
|
{
|
|
// Propagate timestamps for rename case, see ALF-10884
|
|
propagateTimeStamps(newParentAssocRef);
|
|
}
|
|
|
|
invokeOnCreateChildAssociation(newParentAssocRef, false);
|
|
invokeOnDeleteChildAssociation(oldParentAssocRef);
|
|
invokeOnMoveNode(oldParentAssocRef, newParentAssocRef);
|
|
}
|
|
|
|
// Done
|
|
return newParentAssocRef;
|
|
}
|
|
|
|
/**
|
|
* This process is less invasive than the <b>move</b> method as the child associations
|
|
* do not need to be remade.
|
|
*/
|
|
private void pullNodeChildrenToSameStore(Pair<Long, NodeRef> nodePair)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
// Get the node's children, but only one's that aren't in the same store
|
|
final List<Pair<Long, NodeRef>> childNodePairs = new ArrayList<Pair<Long, NodeRef>>(5);
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean preLoadNodes()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
// Add it
|
|
childNodePairs.add(childNodePair);
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
// We only need to move child nodes that are not already in the same store
|
|
nodeDAO.getChildAssocs(nodeId, null, null, null, Boolean.TRUE, Boolean.FALSE, callback);
|
|
// Each child must be moved to the same store as the parent
|
|
for (Pair<Long, NodeRef> oldChildNodePair : childNodePairs)
|
|
{
|
|
Long childNodeId = oldChildNodePair.getFirst();
|
|
NodeRef childNodeRef = oldChildNodePair.getSecond();
|
|
NodeRef.Status childNodeStatus = nodeDAO.getNodeRefStatus(childNodeRef);
|
|
if (childNodeStatus == null || childNodeStatus.isDeleted())
|
|
{
|
|
// Node has already been deleted.
|
|
continue;
|
|
}
|
|
|
|
QName childNodeTypeQName = nodeDAO.getNodeType(childNodeId);
|
|
Set<QName> childNodeAspectQNames = nodeDAO.getNodeAspects(childNodeId);
|
|
Pair<Long, ChildAssociationRef> oldParentAssocPair = nodeDAO.getPrimaryParentAssoc(childNodeId);
|
|
ChildAssociationRef oldParentAssocRef = oldParentAssocPair.getSecond();
|
|
|
|
// remove the deleted node from the list of new nodes
|
|
untrackNewNodeRef(childNodeRef);
|
|
|
|
// track the deletion of this node - so we can prevent new associations to it.
|
|
trackDeletedNodeRef(childNodeRef);
|
|
|
|
// Fire node policies. This ensures that each node in the hierarchy gets a notification fired.
|
|
invokeBeforeDeleteNode(childNodeRef);
|
|
invokeBeforeCreateNode(
|
|
oldParentAssocPair.getSecond().getParentRef(),
|
|
oldParentAssocPair.getSecond().getTypeQName(),
|
|
oldParentAssocPair.getSecond().getQName(),
|
|
childNodeTypeQName);
|
|
// Move the node as this gives back the primary parent association
|
|
Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>> moveResult;
|
|
try
|
|
{
|
|
moveResult = nodeDAO.moveNode(childNodeId, nodeId, null,null);
|
|
}
|
|
catch (NodeExistsException e)
|
|
{
|
|
deleteNode(e.getNodePair().getSecond());
|
|
moveResult = nodeDAO.moveNode(childNodeId, nodeId, null,null);
|
|
}
|
|
// Move the node as this gives back the primary parent association
|
|
Pair<Long, ChildAssociationRef> newParentAssocPair = moveResult.getFirst();
|
|
Pair<Long, NodeRef> newChildNodePair = moveResult.getSecond();
|
|
ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();
|
|
// Index
|
|
nodeIndexer.indexCreateNode(newParentAssocPair.getSecond());
|
|
// Propagate timestamps
|
|
propagateTimeStamps(oldParentAssocRef);
|
|
propagateTimeStamps(newParentAssocRef);
|
|
// Fire node policies. This ensures that each node in the hierarchy gets a notification fired.
|
|
invokeOnDeleteNode(oldParentAssocRef, childNodeTypeQName, childNodeAspectQNames, true);
|
|
invokeOnCreateNode(newParentAssocRef);
|
|
// Cascade
|
|
pullNodeChildrenToSameStore(newChildNodePair);
|
|
}
|
|
}
|
|
|
|
public NodeRef getStoreArchiveNode(StoreRef storeRef)
|
|
{
|
|
StoreRef archiveStoreRef = storeArchiveMap.get(storeRef);
|
|
if (archiveStoreRef == null)
|
|
{
|
|
// no mapping for the given store
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
return getRootNode(archiveStoreRef);
|
|
}
|
|
}
|
|
|
|
private String extractNameProperty(Map<QName, Serializable> properties)
|
|
{
|
|
Serializable nameValue = properties.get(ContentModel.PROP_NAME);
|
|
String name = (String) DefaultTypeConverter.INSTANCE.convert(String.class, nameValue);
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Ensures name uniqueness for the child and the child association. Note that nothing is done if the
|
|
* association type doesn't enforce name uniqueness.
|
|
*
|
|
* @return Returns <tt>true</tt> if the child association <b>cm:name</b> was written
|
|
*/
|
|
private boolean setChildNameUnique(Pair<Long, NodeRef> childNodePair, String newName, String oldName)
|
|
{
|
|
if (newName == null)
|
|
{
|
|
newName = childNodePair.getSecond().getId(); // Use the node's GUID
|
|
}
|
|
Long childNodeId = childNodePair.getFirst();
|
|
|
|
if (EqualsHelper.nullSafeEquals(newName, oldName))
|
|
{
|
|
// The name has not changed
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
nodeDAO.setChildAssocsUniqueName(childNodeId, newName);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Propagate, if necessary, a <b>cm:modified</b> timestamp change to the parent of the
|
|
* given association. The parent node has to be <b>cm:auditable</b> and the association
|
|
* has to be marked for propagation as well.
|
|
*
|
|
* @param assocRef the association to propagate along
|
|
*/
|
|
private void propagateTimeStamps(ChildAssociationRef assocRef)
|
|
{
|
|
if (!enableTimestampPropagation)
|
|
{
|
|
return; // Bypassed on a system-wide basis
|
|
}
|
|
// First check if the association type warrants propagation in the first place
|
|
AssociationDefinition assocDef = dictionaryService.getAssociation(assocRef.getTypeQName());
|
|
if (assocDef == null || !assocDef.isChild())
|
|
{
|
|
return;
|
|
}
|
|
ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef;
|
|
if (!childAssocDef.getPropagateTimestamps())
|
|
{
|
|
return;
|
|
}
|
|
// The dictionary says propagate. Now get the parent node and prompt the touch.
|
|
NodeRef parentNodeRef = assocRef.getParentRef();
|
|
|
|
// Do not propagate if the cm:auditable behaviour is off
|
|
if (!policyBehaviourFilter.isEnabled(parentNodeRef, ContentModel.ASPECT_AUDITABLE))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentNodeRef);
|
|
Long parentNodeId = parentNodePair.getFirst();
|
|
// If we have already modified a particular parent node in the current txn,
|
|
// it is not necessary to start a new transaction to tweak the cm:modified date.
|
|
// But if the parent node was NOT touched, then doing so in this transaction would
|
|
// create excessive concurrency and retries; in latter case we defer to a small,
|
|
// post-commit isolated transaction.
|
|
if (TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE).contains(parentNodeId))
|
|
{
|
|
// It is already registered in the current transaction.
|
|
return;
|
|
}
|
|
if (nodeDAO.isInCurrentTxn(parentNodeId))
|
|
{
|
|
// The parent and child are in the same transaction
|
|
TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE).add(parentNodeId);
|
|
// Make sure that it is not processed after the transaction
|
|
TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_POST).remove(parentNodeId);
|
|
}
|
|
else
|
|
{
|
|
TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_POST).add(parentNodeId);
|
|
}
|
|
|
|
// Bind a listener for post-transaction manipulation
|
|
AlfrescoTransactionSupport.bindListener(auditableTransactionListener);
|
|
}
|
|
|
|
private static final String KEY_AUDITABLE_PROPAGATION_PRE = "node.auditable.propagation.pre";
|
|
private static final String KEY_AUDITABLE_PROPAGATION_POST = "node.auditable.propagation.post";
|
|
private AuditableTransactionListener auditableTransactionListener = new AuditableTransactionListener();
|
|
/**
|
|
* Wrapper to set the <b>cm:modified</b> time on individual nodes.
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.4.6
|
|
*/
|
|
private class AuditableTransactionListener extends TransactionListenerAdapter
|
|
{
|
|
@Override
|
|
public void beforeCommit(boolean readOnly)
|
|
{
|
|
// An error in prior code if it's read only
|
|
if (readOnly)
|
|
{
|
|
throw new IllegalStateException("Attempting to modify parent cm:modified in read-only txn.");
|
|
}
|
|
|
|
Set<Long> parentNodeIds = TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE);
|
|
if (parentNodeIds.size() == 0)
|
|
{
|
|
return;
|
|
}
|
|
// Process parents, but use the current txn
|
|
Date modifiedDate = new Date();
|
|
process(parentNodeIds, modifiedDate, true);
|
|
}
|
|
|
|
@Override
|
|
public void afterCommit()
|
|
{
|
|
Set<Long> parentNodeIds = TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_POST);
|
|
if (parentNodeIds.size() == 0)
|
|
{
|
|
return;
|
|
}
|
|
Date modifiedDate = new Date();
|
|
process(parentNodeIds, modifiedDate, false);
|
|
}
|
|
|
|
/**
|
|
* @param parentNodeIds the parent node IDs that need to be touched for <b>cm:modified</b>
|
|
* @param modifiedDate the date to set
|
|
* @param useCurrentTxn <tt>true</tt> to use the current transaction
|
|
*/
|
|
private void process(final Set<Long> parentNodeIds, Date modifiedDate, boolean useCurrentTxn)
|
|
{
|
|
// Walk through the IDs
|
|
for (Long parentNodeId: parentNodeIds)
|
|
{
|
|
processSingle(parentNodeId, modifiedDate, useCurrentTxn);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Touch a single node in a new, writable txn
|
|
*
|
|
* @param parentNodeId the parent node to touch
|
|
* @param modifiedDate the date to set
|
|
* @param useCurrentTxn <tt>true</tt> to use the current transaction
|
|
*/
|
|
private void processSingle(final Long parentNodeId, final Date modifiedDate, boolean useCurrentTxn)
|
|
{
|
|
RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
|
|
txnHelper.setMaxRetries(1);
|
|
RetryingTransactionCallback<Void> callback = new RetryingTransactionCallback<Void>()
|
|
{
|
|
@Override
|
|
public Void execute() throws Throwable
|
|
{
|
|
Pair<Long, NodeRef> parentNodePair = nodeDAO.getNodePair(parentNodeId);
|
|
if (parentNodePair == null)
|
|
{
|
|
return null; // Parent has gone away
|
|
}
|
|
else if (!nodeDAO.hasNodeAspect(parentNodeId, ContentModel.ASPECT_AUDITABLE))
|
|
{
|
|
return null; // Not auditable
|
|
}
|
|
NodeRef parentNodeRef = parentNodePair.getSecond();
|
|
|
|
// Invoke policy behaviour
|
|
invokeBeforeUpdateNode(parentNodeRef);
|
|
|
|
// Touch the node; it is cm:auditable
|
|
boolean changed = nodeDAO.setModifiedDate(parentNodeId, modifiedDate);
|
|
|
|
if (changed)
|
|
{
|
|
// Invoke policy behaviour
|
|
invokeOnUpdateNode(parentNodeRef);
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(parentNodeRef);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
try
|
|
{
|
|
txnHelper.doInTransaction(callback, false, !useCurrentTxn);
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug(
|
|
"Touched cm:modified date for node " + parentNodeId +
|
|
" (" + modifiedDate + ")" +
|
|
(useCurrentTxn ? " in txn " : " in new txn ") +
|
|
nodeDAO.getCurrentTransactionId(false));
|
|
}
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
logger.info("Failed to update cm:modified date for node: " + parentNodeId);
|
|
}
|
|
}
|
|
}
|
|
}
|