Files
alfresco-community-repo/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
Dave Ward 811519ae48 Merged V4.0-BUG-FIX to HEAD
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
2012-02-08 11:06:09 +00:00

4262 lines
164 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.domain.node;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.Savepoint;
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.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.Stack;
import java.util.TreeSet;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.ibatis.BatchingDAO;
import org.alfresco.ibatis.RetryingCallbackHelper;
import org.alfresco.ibatis.RetryingCallbackHelper.RetryingCallback;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.NullCache;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.cache.TransactionalCache;
import org.alfresco.repo.cache.lookup.EntityLookupCache;
import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
import org.alfresco.repo.domain.contentdata.ContentDataDAO;
import org.alfresco.repo.domain.control.ControlDAO;
import org.alfresco.repo.domain.locale.LocaleDAO;
import org.alfresco.repo.domain.permissions.AccessControlListDAO;
import org.alfresco.repo.domain.permissions.AclDAO;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.domain.usage.UsageDAO;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.permissions.AccessControlListProperties;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionAwareSingleton;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.InvalidTypeException;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
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.ContentData;
import org.alfresco.service.cmr.repository.CyclicChildRelationshipException;
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
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.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.transaction.ReadOnlyServerException;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.ReadWriteLockExecuter;
import org.alfresco.util.SerializationUtils;
import org.alfresco.util.EqualsHelper.MapValueComparison;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.util.Assert;
/**
* Abstract implementation for Node DAO.
* <p>
* This provides basic services such as caching, but defers to the underlying implementation
* for CRUD operations.
*
* @author Derek Hulley
* @since 3.4
*/
public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
{
private static final String CACHE_REGION_ROOT_NODES = "N.RN";
private static final String CACHE_REGION_NODES = "N.N";
private static final String CACHE_REGION_ASPECTS = "N.A";
private static final String CACHE_REGION_PROPERTIES = "N.P";
private static final String CACHE_REGION_PARENT_ASSOCS = "N.PA";
protected Log logger = LogFactory.getLog(getClass());
private Log loggerPaths = LogFactory.getLog(getClass().getName() + ".paths");
protected final boolean isDebugEnabled = logger.isDebugEnabled();
private NodePropertyHelper nodePropertyHelper;
private ServerIdCallback serverIdCallback = new ServerIdCallback();
private UpdateTransactionListener updateTransactionListener = new UpdateTransactionListener();
private RetryingCallbackHelper childAssocRetryingHelper;
private TransactionService transactionService;
private DictionaryService dictionaryService;
private BehaviourFilter policyBehaviourFilter;
private AclDAO aclDAO;
private AccessControlListDAO accessControlListDAO;
private ControlDAO controlDAO;
private QNameDAO qnameDAO;
private ContentDataDAO contentDataDAO;
private LocaleDAO localeDAO;
private UsageDAO usageDAO;
/**
* Cache for the Store root nodes by StoreRef:<br/>
* KEY: StoreRef<br/>
* VALUE: Node representing the root node<br/>
* VALUE KEY: IGNORED<br/>
*/
private EntityLookupCache<StoreRef, Node, Serializable> rootNodesCache;
/**
* Cache for nodes with the root aspect by StoreRef:<br/>
* KEY: StoreRef<br/>
* VALUE: A set of nodes with the root aspect<br/>
*/
private SimpleCache<StoreRef, Set<NodeRef>> allRootNodesCache;
/**
* Bidirectional cache for the Node ID to Node lookups:<br/>
* KEY: Node ID<br/>
* VALUE: Node<br/>
* VALUE KEY: The Node's NodeRef<br/>
*/
private EntityLookupCache<Long, Node, NodeRef> nodesCache;
/**
* Backing transactional cache to allow read-through requests to be honoured
*/
private TransactionalCache<Serializable, Serializable> nodesTransactionalCache;
/**
* Cache for the QName values:<br/>
* KEY: NodeVersionKey<br/>
* VALUE: Set&lt;QName&gt;<br/>
* VALUE KEY: None<br/>
*/
private EntityLookupCache<NodeVersionKey, Set<QName>, Serializable> aspectsCache;
/**
* Cache for the Node properties:<br/>
* KEY: NodeVersionKey<br/>
* VALUE: Map&lt;QName, Serializable&gt;<br/>
* VALUE KEY: None<br/>
*/
private EntityLookupCache<NodeVersionKey, Map<QName, Serializable>, Serializable> propertiesCache;
/**
* Cache for the Node parent assocs:<br/>
* KEY: NodeVersionKey<br/>
* VALUE: ParentAssocs<br/>
* VALUE KEY: None<br/s>
*/
private EntityLookupCache<NodeVersionKey, ParentAssocsInfo, Serializable> parentAssocsCache;
/**
* Cache for fast lookups of child nodes by <b>cm:name</b>.
*/
private SimpleCache<ChildByNameKey, ChildAssocEntity> childByNameCache;
/**
* Constructor. Set up various instance-specific members such as caches and locks.
*/
public AbstractNodeDAOImpl()
{
childAssocRetryingHelper = new RetryingCallbackHelper();
childAssocRetryingHelper.setRetryWaitMs(10);
childAssocRetryingHelper.setMaxRetries(5);
// Caches
rootNodesCache = new EntityLookupCache<StoreRef, Node, Serializable>(new RootNodesCacheCallbackDAO());
nodesCache = new EntityLookupCache<Long, Node, NodeRef>(new NodesCacheCallbackDAO());
aspectsCache = new EntityLookupCache<NodeVersionKey, Set<QName>, Serializable>(new AspectsCallbackDAO());
propertiesCache = new EntityLookupCache<NodeVersionKey, Map<QName, Serializable>, Serializable>(new PropertiesCallbackDAO());
parentAssocsCache = new EntityLookupCache<NodeVersionKey, ParentAssocsInfo, Serializable>(new ParentAssocsCallbackDAO());
childByNameCache = new NullCache<ChildByNameKey, ChildAssocEntity>();
}
/**
* @param transactionService the service to start post-txn processes
*/
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
/**
* @param dictionaryService the service help determine <b>cm:auditable</b> characteristics
*/
public void setDictionaryService(DictionaryService dictionaryService)
{
this.dictionaryService = dictionaryService;
}
/**
* @param policyBehaviourFilter the service to determine the behaviour for <b>cm:auditable</b> and
* other inherent capabilities.
*/
public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter)
{
this.policyBehaviourFilter = policyBehaviourFilter;
}
/**
* @param aclDAO used to update permissions during certain operations
*/
public void setAclDAO(AclDAO aclDAO)
{
this.aclDAO = aclDAO;
}
/**
* @param accessControlListDAO used to update ACL inheritance during node moves
*/
public void setAccessControlListDAO(AccessControlListDAO accessControlListDAO)
{
this.accessControlListDAO = accessControlListDAO;
}
/**
* @param controlDAO create Savepoints
*/
public void setControlDAO(ControlDAO controlDAO)
{
this.controlDAO = controlDAO;
}
/**
* @param qnameDAO translates QName IDs into QName instances and vice-versa
*/
public void setQnameDAO(QNameDAO qnameDAO)
{
this.qnameDAO = qnameDAO;
}
/**
* @param contentDataDAO used to create and delete content references
*/
public void setContentDataDAO(ContentDataDAO contentDataDAO)
{
this.contentDataDAO = contentDataDAO;
}
/**
* @param localeDAO used to handle MLText properties
*/
public void setLocaleDAO(LocaleDAO localeDAO)
{
this.localeDAO = localeDAO;
}
/**
* @param usageDAO used to keep content usage calculations in line
*/
public void setUsageDAO(UsageDAO usageDAO)
{
this.usageDAO = usageDAO;
}
/**
* Set the cache that maintains the Store root node data
*
* @param cache the cache
*/
public void setRootNodesCache(SimpleCache<Serializable, Serializable> cache)
{
this.rootNodesCache = new EntityLookupCache<StoreRef, Node, Serializable>(
cache,
CACHE_REGION_ROOT_NODES,
new RootNodesCacheCallbackDAO());
}
/**
* Set the cache that maintains the extended Store root node data
*
* @param cache the cache
*/
public void setAllRootNodesCache(SimpleCache<StoreRef, Set<NodeRef>> allRootNodesCache)
{
this.allRootNodesCache = allRootNodesCache;
}
/**
* Set the cache that maintains node ID-NodeRef cross referencing data
*
* @param cache the cache
*/
public void setNodesCache(SimpleCache<Serializable, Serializable> cache)
{
this.nodesCache = new EntityLookupCache<Long, Node, NodeRef>(
cache,
CACHE_REGION_NODES,
new NodesCacheCallbackDAO());
if (cache instanceof TransactionalCache)
{
this.nodesTransactionalCache = (TransactionalCache<Serializable, Serializable>) cache;
}
}
/**
* Set the cache that maintains the Node QName IDs
*
* @param aspectsCache the cache
*/
public void setAspectsCache(SimpleCache<NodeVersionKey, Set<QName>> aspectsCache)
{
this.aspectsCache = new EntityLookupCache<NodeVersionKey, Set<QName>, Serializable>(
aspectsCache,
CACHE_REGION_ASPECTS,
new AspectsCallbackDAO());
}
/**
* Set the cache that maintains the Node property values
*
* @param propertiesCache the cache
*/
public void setPropertiesCache(SimpleCache<NodeVersionKey, Map<QName, Serializable>> propertiesCache)
{
this.propertiesCache = new EntityLookupCache<NodeVersionKey, Map<QName, Serializable>, Serializable>(
propertiesCache,
CACHE_REGION_PROPERTIES,
new PropertiesCallbackDAO());
}
/**
* Set the cache that maintains the Node parent associations
*
* @param parentAssocsCache the cache
*/
public void setParentAssocsCache(SimpleCache<NodeVersionKey, Serializable> parentAssocsCache)
{
this.parentAssocsCache = new EntityLookupCache<NodeVersionKey, ParentAssocsInfo, Serializable>(
parentAssocsCache,
CACHE_REGION_PARENT_ASSOCS,
new ParentAssocsCallbackDAO());
}
/**
* Set the cache that maintains lookups by child <b>cm:name</b>
*
* @param childByNameCache the cache
*/
public void setChildByNameCache(SimpleCache<ChildByNameKey, ChildAssocEntity> childByNameCache)
{
this.childByNameCache = childByNameCache;
}
/*
* Initialize
*/
public void init()
{
PropertyCheck.mandatory(this, "transactionService", transactionService);
PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
PropertyCheck.mandatory(this, "aclDAO", aclDAO);
PropertyCheck.mandatory(this, "accessControlListDAO", accessControlListDAO);
PropertyCheck.mandatory(this, "qnameDAO", qnameDAO);
PropertyCheck.mandatory(this, "contentDataDAO", contentDataDAO);
PropertyCheck.mandatory(this, "localeDAO", localeDAO);
PropertyCheck.mandatory(this, "usageDAO", usageDAO);
this.nodePropertyHelper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO);
}
/*
* Server
*/
/**
* Wrapper to get the server ID within the context of a lock
*/
private class ServerIdCallback extends ReadWriteLockExecuter<Long>
{
private TransactionAwareSingleton<Long> serverIdStorage = new TransactionAwareSingleton<Long>();
public Long getWithReadLock() throws Throwable
{
return serverIdStorage.get();
}
public Long getWithWriteLock() throws Throwable
{
if (serverIdStorage.get() != null)
{
return serverIdStorage.get();
}
// Avoid write operations in read-only transactions
// ALF-5456: IP address change can cause read-write errors on startup
if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY)
{
return null;
}
// Server IP address
String ipAddress = null;
try
{
ipAddress = InetAddress.getLocalHost().getHostAddress();
}
catch (UnknownHostException e)
{
throw new AlfrescoRuntimeException("Failed to get server IP address", e);
}
// Get the server instance
ServerEntity serverEntity = selectServer(ipAddress);
if (serverEntity != null)
{
serverIdStorage.put(serverEntity.getId());
return serverEntity.getId();
}
// Doesn't exist, so create it
Long serverId = insertServer(ipAddress);
serverIdStorage.put(serverId);
if (isDebugEnabled)
{
logger.debug("Created server entity: " + serverEntity);
}
return serverId;
}
}
/**
* Get the ID of the current server, or <tt>null</tt> if there is no ID for the current
* server and one can't be created.
*
* @see ServerIdCallback
*/
private Long getServerId()
{
return serverIdCallback.execute();
}
/*
* Cache helpers
*/
private void clearCaches()
{
nodesCache.clear();
aspectsCache.clear();
propertiesCache.clear();
parentAssocsCache.clear();
}
/**
* Invalidate cache entries for all children of a give node. This usually applies
* where the child associations or nodes are modified en-masse.
*
* @param parentNodeId the parent node of all child nodes to be invalidated (may be <tt>null</tt>)
* @param touchNodes <tt>true<tt> to also touch the nodes
* @return the number of child associations found (might be capped)
*/
private int invalidateNodeChildrenCaches(Long parentNodeId, boolean primary, boolean touchNodes)
{
Long txnId = getCurrentTransaction().getId();
int count = 0;
List<Long> childNodeIds = new ArrayList<Long>(256);
Long minAssocIdInclusive = Long.MIN_VALUE;
while (minAssocIdInclusive != null)
{
childNodeIds.clear();
List<ChildAssocEntity> childAssocs = selectChildNodeIds(
parentNodeId,
Boolean.valueOf(primary),
minAssocIdInclusive,
256);
// Remove the cache entries as we go
for (ChildAssocEntity childAssoc : childAssocs)
{
Long childAssocId = childAssoc.getId();
if (childAssocId.compareTo(minAssocIdInclusive) < 0)
{
throw new RuntimeException("Query results did not increase for assoc ID");
}
else
{
minAssocIdInclusive = new Long(childAssocId.longValue() + 1L);
}
// Invalidate the node cache
Long childNodeId = childAssoc.getChildNode().getId();
childNodeIds.add(childNodeId);
invalidateNodeCaches(childNodeId);
count++;
}
// Bring all the nodes into the transaction, if required
if (touchNodes)
{
updateNodes(txnId, childNodeIds);
}
// Now break out if we didn't have the full set of results
if (childAssocs.size() < 256)
{
break;
}
}
// Done
return count;
}
/**
* Invalidates all cached artefacts for a particular node, forcing a refresh.
*
* @param nodeId the node ID
*/
private void invalidateNodeCaches(Long nodeId)
{
// Take the current value from the nodesCache and use that to invalidate the other caches
Node node = nodesCache.getValue(nodeId);
if (node != null)
{
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
invalidateNodeCaches(nodeVersionKey, true, true, true);
}
// Finally remove the node reference
nodesCache.removeByKey(nodeId);
}
/**
* Invalidate specific node caches using an exact key
*
* @param nodeVersionKey the node ID-VERSION key to use
*/
private void invalidateNodeCaches(
NodeVersionKey nodeVersionKey,
boolean invalidateNodeAspectsCache,
boolean invalidateNodePropertiesCache,
boolean invalidateParentAssocsCache)
{
if (invalidateNodeAspectsCache)
{
aspectsCache.removeByKey(nodeVersionKey);
}
if (invalidateNodePropertiesCache)
{
propertiesCache.removeByKey(nodeVersionKey);
}
if (invalidateParentAssocsCache)
{
parentAssocsCache.removeByKey(nodeVersionKey);
}
}
/*
* Transactions
*/
private static final String KEY_TRANSACTION = "node.transaction.id";
/**
* Wrapper to update the current transaction to get the change time correct
*
* @author Derek Hulley
* @since 3.4
*/
private class UpdateTransactionListener extends TransactionListenerAdapter
{
@Override
public void beforeCommit(boolean readOnly)
{
if (readOnly)
{
return;
}
TransactionEntity txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
Long txnId = txn.getId();
// Update it
Long now = System.currentTimeMillis();
updateTransaction(txnId, now);
}
}
/**
* @return Returns a new transaction or an existing one if already active
*/
private TransactionEntity getCurrentTransaction()
{
TransactionEntity txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
if (txn != null)
{
// We have been busy here before
return txn;
}
// Check that this is a writable txn
if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE)
{
throw new ReadOnlyServerException();
}
// Have to create a new transaction entry
Long serverId = getServerId();
Long now = System.currentTimeMillis();
String changeTxnId = AlfrescoTransactionSupport.getTransactionId();
Long txnId = insertTransaction(serverId, changeTxnId, now);
// Store it for later
if (isDebugEnabled)
{
logger.debug("Create txn: " + txnId);
}
txn = new TransactionEntity();
txn.setId(txnId);
txn.setChangeTxnId(changeTxnId);
txn.setCommitTimeMs(now);
ServerEntity server = new ServerEntity();
server.setId(serverId);
txn.setServer(server);
AlfrescoTransactionSupport.bindResource(KEY_TRANSACTION, txn);
// Listen for the end of the transaction
AlfrescoTransactionSupport.bindListener(updateTransactionListener);
// Done
return txn;
}
public Long getCurrentTransactionId(boolean ensureNew)
{
TransactionEntity txn;
if (ensureNew)
{
txn = getCurrentTransaction();
}
else
{
txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
}
return txn == null ? null : txn.getId();
}
/*
* Stores
*/
public List<Pair<Long, StoreRef>> getStores()
{
List<StoreEntity> storeEntities = selectAllStores();
List<Pair<Long, StoreRef>> storeRefs = new ArrayList<Pair<Long,StoreRef>>(storeEntities.size());
for (StoreEntity storeEntity : storeEntities)
{
storeRefs.add(new Pair<Long, StoreRef>(storeEntity.getId(), storeEntity.getStoreRef()));
}
return storeRefs;
}
/**
* @throws InvalidStoreRefException if the store is invalid
*/
private StoreEntity getStoreNotNull(StoreRef storeRef)
{
Pair<StoreRef, Node> rootNodePair = rootNodesCache.getByKey(storeRef);
if (rootNodePair == null)
{
throw new InvalidStoreRefException(storeRef);
}
else
{
return rootNodePair.getSecond().getStore();
}
}
public boolean exists(StoreRef storeRef)
{
Pair<StoreRef, Node> rootNodePair = rootNodesCache.getByKey(storeRef);
return rootNodePair != null;
}
public Pair<Long, NodeRef> getRootNode(StoreRef storeRef)
{
Pair<StoreRef, Node> rootNodePair = rootNodesCache.getByKey(storeRef);
if (rootNodePair == null)
{
throw new InvalidStoreRefException(storeRef);
}
else
{
return rootNodePair.getSecond().getNodePair();
}
}
public Set<NodeRef> getAllRootNodes(StoreRef storeRef)
{
Set<NodeRef> rootNodes = allRootNodesCache.get(storeRef);
if (rootNodes == null)
{
final Map<StoreRef, Set<NodeRef>> allRootNodes = new HashMap<StoreRef, Set<NodeRef>>(97);
getNodesWithAspects(Collections.singleton(ContentModel.ASPECT_ROOT), 0L, Long.MAX_VALUE, new NodeRefQueryCallback()
{
@Override
public boolean handle(Pair<Long, NodeRef> nodePair)
{
NodeRef nodeRef = nodePair.getSecond();
StoreRef storeRef = nodeRef.getStoreRef();
Set<NodeRef> rootNodes = allRootNodes.get(storeRef);
if (rootNodes == null)
{
rootNodes = new HashSet<NodeRef>(97);
allRootNodes.put(storeRef, rootNodes);
}
rootNodes.add(nodeRef);
return true;
}
});
rootNodes = allRootNodes.get(storeRef);
if (rootNodes == null)
{
rootNodes = Collections.emptySet();
allRootNodes.put(storeRef, rootNodes);
}
for (Map.Entry<StoreRef, Set<NodeRef>> entry : allRootNodes.entrySet())
{
StoreRef entryStoreRef = entry.getKey();
// Prevent unnecessary cross-invalidation
if (!allRootNodesCache.contains(entryStoreRef))
{
allRootNodesCache.put(entryStoreRef, entry.getValue());
}
}
}
return rootNodes;
}
public Pair<Long, NodeRef> newStore(StoreRef storeRef)
{
// Create the store
StoreEntity store = new StoreEntity();
store.setProtocol(storeRef.getProtocol());
store.setIdentifier(storeRef.getIdentifier());
Long storeId = insertStore(store);
store.setId(storeId);
// Get an ACL for the root node
Long aclId = aclDAO.createAccessControlList();
// Create a root node
Long nodeTypeQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_STOREROOT).getFirst();
NodeEntity rootNode = newNodeImpl(store, null, nodeTypeQNameId, null, aclId, false, null);
Long rootNodeId = rootNode.getId();
addNodeAspects(rootNodeId, Collections.singleton(ContentModel.ASPECT_ROOT));
// Now update the store with the root node ID
store.setRootNode(rootNode);
updateStoreRoot(store);
// Push the value into the caches
rootNodesCache.setValue(storeRef, rootNode);
if (isDebugEnabled)
{
logger.debug("Created store: \n" + " " + store);
}
return new Pair<Long, NodeRef>(rootNode.getId(), rootNode.getNodeRef());
}
@Override
public void moveStore(StoreRef oldStoreRef, StoreRef newStoreRef)
{
StoreEntity store = getStoreNotNull(oldStoreRef);
store.setProtocol(newStoreRef.getProtocol());
store.setIdentifier(newStoreRef.getIdentifier());
// Update it
int count = updateStore(store);
if (count != 1)
{
throw new ConcurrencyFailureException("Store not updated: " + oldStoreRef);
}
// All the NodeRef-based caches are invalid. ID-based caches are fine.
rootNodesCache.removeByKey(oldStoreRef);
allRootNodesCache.remove(oldStoreRef);
nodesCache.clear();
if (isDebugEnabled)
{
logger.debug("Moved store: " + oldStoreRef + " --> " + newStoreRef);
}
}
/**
* Callback to cache store root nodes by {@link StoreRef}.
*
* @author Derek Hulley
* @since 3.4
*/
private class RootNodesCacheCallbackDAO extends EntityLookupCallbackDAOAdaptor<StoreRef, Node, Serializable>
{
/**
* @throws UnsupportedOperationException Stores must be created externally
*/
public Pair<StoreRef, Node> createValue(Node value)
{
throw new UnsupportedOperationException("Root node creation is done externally: " + value);
}
/**
* @param key the store ID
*/
public Pair<StoreRef, Node> findByKey(StoreRef storeRef)
{
NodeEntity node = selectStoreRootNode(storeRef);
return node == null ? null : new Pair<StoreRef, Node>(storeRef, node);
}
}
/*
* Nodes
*/
/**
* Callback to cache nodes by ID and {@link NodeRef}. When looking up objects based on the
* value key, only the referencing properties need be populated. <b>ALL</b> nodes are cached,
* not just live nodes.
*
* @see NodeEntity
*
* @author Derek Hulley
* @since 3.4
*/
private class NodesCacheCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Node, NodeRef>
{
/**
* @throws UnsupportedOperationException Nodes are created externally
*/
public Pair<Long, Node> createValue(Node value)
{
throw new UnsupportedOperationException("Node creation is done externally: " + value);
}
/**
* @param nodeId the key node ID
*/
public Pair<Long, Node> findByKey(Long nodeId)
{
NodeEntity node = selectNodeById(nodeId, null);
if (node != null)
{
// Lock it to prevent 'accidental' modification
node.lock();
return new Pair<Long, Node>(nodeId, node);
}
else
{
return null;
}
}
/**
* @return Returns the Node's NodeRef
*/
@Override
public NodeRef getValueKey(Node value)
{
return value.getNodeRef();
}
/**
* Looks the node up based on the NodeRef of the given node
*/
@Override
public Pair<Long, Node> findByValue(Node node)
{
NodeRef nodeRef = node.getNodeRef();
node = selectNodeByNodeRef(nodeRef, null);
if (node != null)
{
// Lock it to prevent 'accidental' modification
node.lock();
return new Pair<Long, Node>(node.getId(), node);
}
else
{
return null;
}
}
}
public boolean exists(Long nodeId)
{
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
return pair != null && !pair.getSecond().getDeleted();
}
public boolean exists(NodeRef nodeRef)
{
NodeEntity node = new NodeEntity(nodeRef);
Pair<Long, Node> pair = nodesCache.getByValue(node);
return pair != null && !pair.getSecond().getDeleted();
}
@Override
public boolean isInCurrentTxn(Long nodeId)
{
Long currentTxnId = getCurrentTransactionId(false);
if (currentTxnId == null)
{
// No transactional changes have been made to any nodes, therefore the node cannot
// be part of the current transaction
return false;
}
Node node = getNodeNotNull(nodeId);
Long nodeTxnId = node.getTransaction().getId();
return nodeTxnId.equals(currentTxnId);
}
public Status getNodeRefStatus(NodeRef nodeRef)
{
Node node = new NodeEntity(nodeRef);
Pair<Long, Node> nodePair = nodesCache.getByValue(node);
// The nodesCache gets both live and deleted nodes.
if (nodePair == null)
{
return null;
}
else
{
return nodePair.getSecond().getNodeStatus();
}
}
public Pair<Long, NodeRef> getNodePair(NodeRef nodeRef)
{
NodeEntity node = new NodeEntity(nodeRef);
Pair<Long, Node> pair = nodesCache.getByValue(node);
// The noderef is currently invalid WRT to the cache. Let's just check the database
if (pair == null || pair.getSecond().getDeleted())
{
Node dbNode = selectNodeByNodeRef(nodeRef, null);
if (dbNode == null)
{
// The DB agrees. This is an invalid noderef. Why are you trying to use it?
return null;
}
Long nodeId = dbNode.getId();
if (dbNode.getDeleted())
{
// The node is actually deleted as the cache said. Could still be a race condition, so let's allow the
// transaction to be retried by attaching a cause to our InvalidNodeRefException
InvalidNodeRefException e = new InvalidNodeRefException(nodeRef);
e.initCause(new ConcurrencyFailureException("Attempt to follow reference " + nodeRef
+ " to deleted node " + nodeId));
throw e;
}
else
{
// The cache was wrong, possibly due to it caching negative results earlier. Let's repair it and carry on!
if (logger.isDebugEnabled())
{
logger.debug("Stale cache detected for Node " + nodeRef + ": previously though to be deleted. Repairing cache.");
}
invalidateNodeCaches(nodeId);
nodesCache.setValue(dbNode.getId(), dbNode);
return dbNode.getNodePair();
}
}
return pair.getSecond().getNodePair();
}
public Pair<Long, NodeRef> getNodePair(Long nodeId)
{
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
return (pair == null || pair.getSecond().getDeleted()) ? null : pair.getSecond().getNodePair();
}
/**
* Find an undeleted node
*
* @param nodeId the node
* @return Returns the fully populated node
* @throws ConcurrencyFailureException if the ID doesn't reference a <b>live</b> node
*/
private Node getNodeNotNull(Long nodeId)
{
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
if (pair == null || pair.getSecond().getDeleted())
{
// Force a removal from the cache
nodesCache.removeByKey(nodeId);
// Go back to the database and get what is there
NodeEntity dbNode = selectNodeById(nodeId, null);
if (pair == null)
{
throw new ConcurrencyFailureException(
"No node exists: \n" +
" ID: " + nodeId + "\n" +
" DB row: " + dbNode);
}
else
{
throw new ConcurrencyFailureException(
"No live node exists: \n" +
" ID: " + nodeId + "\n" +
" Cache row: " + pair.getSecond() + "\n" +
" DB row: " + dbNode);
}
}
else
{
return pair.getSecond();
}
}
public QName getNodeType(Long nodeId)
{
Node node = getNodeNotNull(nodeId);
Long nodeTypeQNameId = node.getTypeQNameId();
return qnameDAO.getQName(nodeTypeQNameId).getSecond();
}
public Long getNodeAclId(Long nodeId)
{
Node node = getNodeNotNull(nodeId);
return node.getAclId();
}
@Override
public ChildAssocEntity newNode(
Long parentNodeId,
QName assocTypeQName,
QName assocQName,
StoreRef storeRef,
String uuid,
QName nodeTypeQName,
Locale nodeLocale,
String childNodeName,
Map<QName, Serializable> auditableProperties) throws InvalidTypeException
{
Assert.notNull(parentNodeId, "parentNodeId");
Assert.notNull(assocTypeQName, "assocTypeQName");
Assert.notNull(assocQName, "assocQName");
Assert.notNull(storeRef, "storeRef");
if (auditableProperties == null)
{
auditableProperties = Collections.emptyMap();
}
// Get the parent node
Node parentNode = getNodeNotNull(parentNodeId);
// Find an initial ACL for the node
Long parentAclId = parentNode.getAclId();
Long childAclId = null;
if (parentAclId != null)
{
try
{
Long inheritedACL = aclDAO.getInheritedAccessControlList(parentAclId);
AccessControlListProperties inheritedAcl = aclDAO.getAccessControlListProperties(inheritedACL);
if (inheritedAcl != null)
{
childAclId = inheritedAcl.getId();
}
}
catch (Throwable e)
{
// The get* calls above actually do writes. So pessimistically get rid of the
// parent node from the cache in case it was wrong somehow.
invalidateNodeCaches(parentNodeId);
}
}
// Build the cm:auditable properties
AuditablePropertiesEntity auditableProps = new AuditablePropertiesEntity();
boolean setAuditProps = auditableProps.setAuditValues(null, null, auditableProperties);
if (!setAuditProps)
{
// No cm:auditable properties were supplied
auditableProps = null;
}
// Get the store
StoreEntity store = getStoreNotNull(storeRef);
// Create the node (it is not a root node)
Long nodeTypeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
Long nodeLocaleId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
NodeEntity node = newNodeImpl(store, uuid, nodeTypeQNameId, nodeLocaleId, childAclId, false, auditableProps);
Long nodeId = node.getId();
// Protect the node's cm:auditable if it was explicitly set
if (setAuditProps)
{
NodeRef nodeRef = node.getNodeRef();
policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
}
// Now create a primary association for it
if (childNodeName == null)
{
childNodeName = node.getUuid();
}
ChildAssocEntity assoc = newChildAssocImpl(
parentNodeId, nodeId, true, assocTypeQName, assocQName, childNodeName);
// There will be no other parent assocs
boolean isRoot = false;
boolean isStoreRoot = nodeTypeQName.equals(ContentModel.TYPE_STOREROOT);
ParentAssocsInfo parentAssocsInfo = new ParentAssocsInfo(isRoot, isStoreRoot, assoc);
setParentAssocsCached(nodeId, parentAssocsInfo);
if (isDebugEnabled)
{
logger.debug(
"Created new node: \n" +
" Node: " + node + "\n" +
" Assoc: " + assoc);
}
return assoc;
}
/**
* @param uuid the node UUID, or <tt>null</tt> to auto-generate
* @param nodeTypeQNameId the node's type
* @param nodeLocaleId the node's locale or <tt>null</tt> to use the default locale
* @param aclId an ACL ID if available
* @param auditableProps <tt>null</tt> to auto-generate or provide a value to explicitly set
* @param deleted <tt>true</tt> to create an already-deleted node (used for leaving trails of moved nodes)
* @throws NodeExistsException if the target reference is already taken by a live node
*/
private NodeEntity newNodeImpl(
StoreEntity store,
String uuid,
Long nodeTypeQNameId,
Long nodeLocaleId,
Long aclId,
boolean deleted,
AuditablePropertiesEntity auditableProps) throws InvalidTypeException
{
NodeEntity node = new NodeEntity();
// Store
node.setStore(store);
// UUID
if (uuid == null)
{
node.setUuid(GUID.generate());
}
else
{
node.setUuid(uuid);
}
// QName
node.setTypeQNameId(nodeTypeQNameId);
QName nodeTypeQName = qnameDAO.getQName(nodeTypeQNameId).getSecond();
// Locale
if (nodeLocaleId == null)
{
nodeLocaleId = localeDAO.getOrCreateDefaultLocalePair().getFirst();
}
node.setLocaleId(nodeLocaleId);
// ACL (may be null)
node.setAclId(aclId);
// Deleted
node.setDeleted(deleted);
// Transaction
TransactionEntity txn = getCurrentTransaction();
node.setTransaction(txn);
// Audit
boolean addAuditableAspect = false;
if (auditableProps != null)
{
// Client-supplied cm:auditable values
node.setAuditableProperties(auditableProps);
addAuditableAspect = true;
}
else if (AuditablePropertiesEntity.hasAuditableAspect(nodeTypeQName, dictionaryService))
{
// Automatically-generated cm:auditable values
auditableProps = new AuditablePropertiesEntity();
auditableProps.setAuditValues(null, null, true, 0L);
node.setAuditableProperties(auditableProps);
addAuditableAspect = true;
}
Long id = null;
Savepoint savepoint = controlDAO.createSavepoint("newNodeImpl");
try
{
// First try a straight insert and risk the constraint violation if the node exists
id = insertNode(node);
controlDAO.releaseSavepoint(savepoint);
}
catch (Throwable e)
{
controlDAO.rollbackToSavepoint(savepoint);
// This is probably because there is an existing node. We can handle existing deleted nodes.
NodeRef targetNodeRef = node.getNodeRef();
NodeEntity liveNode = selectNodeByNodeRef(targetNodeRef, false); // Only look for live nodes
if (liveNode != null)
{
throw new NodeExistsException(liveNode.getNodePair(), e);
}
NodeEntity deletedNode = selectNodeByNodeRef(targetNodeRef, true); // Only look for deleted nodes
if (deletedNode != null)
{
Long deletedNodeId = deletedNode.getId();
deleteNodeById(deletedNodeId, true);
// Now repeat, but let any further problems just be thrown out
id = insertNode(node);
}
else
{
throw new AlfrescoRuntimeException("Failed to insert new node: " + node, e);
}
}
node.setId(id);
Set<QName> nodeAspects = null;
if (addAuditableAspect && !deleted)
{
Long auditableAspectQNameId = qnameDAO.getOrCreateQName(ContentModel.ASPECT_AUDITABLE).getFirst();
insertNodeAspect(id, auditableAspectQNameId);
nodeAspects = Collections.<QName>singleton(ContentModel.ASPECT_AUDITABLE);
}
else
{
nodeAspects = Collections.<QName>emptySet();
}
// Lock the node and cache
node.lock();
nodesCache.setValue(id, node);
// Pre-populate some of the other caches so that we don't immediately query
setNodeAspectsCached(id, nodeAspects);
setNodePropertiesCached(id, Collections.<QName, Serializable>emptyMap());
if (isDebugEnabled)
{
logger.debug("Created new node: \n" + " " + node);
}
return node;
}
public Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>> moveNode(
final Long childNodeId,
final Long newParentNodeId,
final QName assocTypeQName,
final QName assocQName)
{
final Node newParentNode = getNodeNotNull(newParentNodeId);
final StoreEntity newParentStore = newParentNode.getStore();
final Node childNode = getNodeNotNull(childNodeId);
final StoreEntity childStore = childNode.getStore();
ChildAssocEntity primaryParentAssoc = getPrimaryParentAssocImpl(childNodeId);
final Long oldParentAclId;
if (primaryParentAssoc == null)
{
oldParentAclId = null;
}
else
{
if (primaryParentAssoc.getParentNode() == null)
{
oldParentAclId = null;
}
else
{
Long oldParentNodeId = primaryParentAssoc.getParentNode().getId();
oldParentAclId = getNodeNotNull(oldParentNodeId).getAclId();
}
}
// Need the child node's name here in case it gets removed
final String childNodeName = (String) getNodeProperty(childNodeId, ContentModel.PROP_NAME);
// First attempt to move the node, which may rollback to a savepoint
Node newChildNode = childNode;
// Store
if (!childStore.getId().equals(newParentStore.getId()))
{
// Remove the cm:auditable aspect from the source node
// Remove the cm:auditable aspect from the old node as the new one will get new values as required
Set<Long> aspectIdsToDelete = qnameDAO.convertQNamesToIds(
Collections.singleton(ContentModel.ASPECT_AUDITABLE),
true);
deleteNodeAspects(childNodeId, aspectIdsToDelete);
// ... but make sure we copy over the cm:auditable data from the originating node
AuditablePropertiesEntity auditableProps = childNode.getAuditableProperties();
// Create a new node and copy all the data over to it
newChildNode = newNodeImpl(
newParentStore,
childNode.getUuid(),
childNode.getTypeQNameId(),
childNode.getLocaleId(),
childNode.getAclId(),
false,
auditableProps);
Long newChildNodeId = newChildNode.getId();
moveNodeData(
childNode.getId(),
newChildNodeId);
// The new node will have new data not present in the cache, yet
invalidateNodeCaches(newChildNodeId);
invalidateNodeChildrenCaches(newChildNodeId, true, true);
invalidateNodeChildrenCaches(newChildNodeId, false, true);
// Now update the original to be 'deleted'
NodeUpdateEntity childNodeUpdate = new NodeUpdateEntity();
childNodeUpdate.setId(childNodeId);
childNodeUpdate.setAclId(null);
childNodeUpdate.setUpdateAclId(true);
childNodeUpdate.setTypeQNameId(qnameDAO.getOrCreateQName(ContentModel.TYPE_CMOBJECT).getFirst());
childNodeUpdate.setUpdateTypeQNameId(true);
childNodeUpdate.setLocaleId(localeDAO.getOrCreateDefaultLocalePair().getFirst());
childNodeUpdate.setUpdateLocaleId(true);
childNodeUpdate.setDeleted(Boolean.TRUE);
childNodeUpdate.setUpdateDeleted(true);
// Update the entity.
// Note: We don't use delete here because that will attempt to clean everything up again.
updateNodeImpl(childNode, childNodeUpdate, null);
// There is no need to invalidate the caches as the touched node's version will have progressed
}
else
{
// Touch the node; make sure parent assocs are invalidated
touchNode(childNodeId, null, null, false, false, true);
}
final Long newChildNodeId = newChildNode.getId();
// Now update the primary parent assoc
RetryingCallback<Integer> callback = new RetryingCallback<Integer>()
{
public Integer execute() throws Throwable
{
// Because we are retrying in-transaction i.e. absorbing exceptions, we need a Savepoint
Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
// We use the child node's UUID if there is no cm:name
String childNodeNameToUse = childNodeName == null ? childNode.getUuid() : childNodeName;
try
{
int updated = updatePrimaryParentAssocs(
newChildNodeId,
newParentNodeId,
assocTypeQName,
assocQName,
childNodeNameToUse);
controlDAO.releaseSavepoint(savepoint);
return updated;
}
catch (Throwable e)
{
controlDAO.rollbackToSavepoint(savepoint);
// We assume that this is from the child cm:name constraint violation
throw new DuplicateChildNodeNameException(
newParentNode.getNodeRef(),
assocTypeQName,
childNodeName,
e);
}
}
};
childAssocRetryingHelper.doWithRetry(callback);
// Check for cyclic relationships
// TODO: This adds a lot of overhead when moving hierarchies.
// While getPaths is faster, it would be better to avoid the parentAssocsCache
// completely.
getPaths(newChildNode.getNodePair(), false);
// cycleCheck(newChildNodeId);
// Update ACLs for moved tree
Long newParentAclId = newParentNode.getAclId();
accessControlListDAO.updateInheritance(newChildNodeId, oldParentAclId, newParentAclId);
// Done
Pair<Long, ChildAssociationRef> assocPair = getPrimaryParentAssoc(newChildNode.getId());
Pair<Long, NodeRef> nodePair = newChildNode.getNodePair();
if (isDebugEnabled)
{
logger.debug("Moved node: " + assocPair + " ... " + nodePair);
}
return new Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>>(assocPair, nodePair);
}
@Override
public boolean updateNode(Long nodeId, QName nodeTypeQName, Locale nodeLocale)
{
// Get the existing node; we need to check for a change in store or UUID
Node oldNode = getNodeNotNull(nodeId);
final Long nodeTypeQNameId;
if (nodeTypeQName == null)
{
nodeTypeQNameId = oldNode.getTypeQNameId();
}
else
{
nodeTypeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
}
final Long nodeLocaleId;
if (nodeLocale == null)
{
nodeLocaleId = oldNode.getLocaleId();
}
else
{
nodeLocaleId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
}
// Wrap all the updates into one
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
nodeUpdate.setId(nodeId);
nodeUpdate.setStore(oldNode.getStore()); // Need node reference
nodeUpdate.setUuid(oldNode.getUuid()); // Need node reference
// TypeQName (if necessary)
if (!nodeTypeQNameId.equals(oldNode.getTypeQNameId()))
{
nodeUpdate.setTypeQNameId(nodeTypeQNameId);
nodeUpdate.setUpdateTypeQNameId(true);
}
// Locale (if necessary)
if (!nodeLocaleId.equals(oldNode.getLocaleId()))
{
nodeUpdate.setLocaleId(nodeLocaleId);
nodeUpdate.setUpdateLocaleId(true);
}
return updateNodeImpl(oldNode, nodeUpdate, null);
}
/**
* Updates the node's transaction and <b>cm:auditable</b> properties while
* providing a convenient method to control cache entry invalidation.
* <p/>
* Not all 'touch' signals actually produce a change: the node may already have been touched
* in the current transaction. In this case, the required caches are explicitly invalidated
* as requested.<br/>
* It is more complicated when the node is modified. If the node is modified against a previous
* transaction then all cache entries are left untrusted and not pulled forward. But if the
* node is modified but in the same transaction, then the cache entries are considered good and
* pull forward against the current version of the node ... <b>unless</b> the cache was specicially
* tagged for invalidation.
* <p/>
* It is sometime necessary to provide the node's current aspects, particularly during
* changes to the aspect list. If not provided, they will be looked up.
*
* @param nodeId the ID of the node (must refer to a live node)
* @param auditableProps optionally override the <b>cm:auditable</b> values
* @param nodeAspects the node's aspects or <tt>null</tt> to look them up
* @param invalidateNodeAspectsCache <tt>true</tt> if the node's cached aspects are unreliable
* @param invalidateNodePropertiesCache <tt>true</tt> if the node's cached properties are unreliable
* @param invalidateParentAssocsCache <tt>true</tt> if the node's cached parent assocs are unreliable
*
* @see #updateNodeImpl(NodeEntity, NodeUpdateEntity)
*/
private boolean touchNode(
Long nodeId, AuditablePropertiesEntity auditableProps, Set<QName> nodeAspects,
boolean invalidateNodeAspectsCache,
boolean invalidateNodePropertiesCache,
boolean invalidateParentAssocsCache)
{
Node node = null;
try
{
node = getNodeNotNull(nodeId);
}
catch (DataIntegrityViolationException e)
{
// The ID doesn't reference a live node.
// We do nothing w.r.t. touching
return false;
}
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
nodeUpdate.setId(nodeId);
nodeUpdate.setAuditableProperties(auditableProps);
// Update it
boolean updatedNode = updateNodeImpl(node, nodeUpdate, nodeAspects);
// Handle the cache invalidation requests
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
if (updatedNode)
{
Node newNode = getNodeNotNull(nodeId);
NodeVersionKey newNodeVersionKey = newNode.getNodeVersionKey();
// The version will have moved on, effectively rendering our caches invalid.
// Copy over caches that DON'T need invalidating
if (!invalidateNodeAspectsCache)
{
copyNodeAspectsCached(nodeVersionKey, newNodeVersionKey);
}
if (!invalidateNodePropertiesCache)
{
copyNodePropertiesCached(nodeVersionKey, newNodeVersionKey);
}
if (!invalidateParentAssocsCache)
{
copyParentAssocsCached(nodeVersionKey, newNodeVersionKey);
}
}
else
{
// The node was not touched. By definition it MUST be in the current transaction.
// We invalidate the caches as specifically requested
invalidateNodeCaches(
nodeVersionKey,
invalidateNodeAspectsCache,
invalidateNodePropertiesCache,
invalidateParentAssocsCache);
}
return updatedNode;
}
/**
* Helper method that updates the node, bringing it into the current transaction with
* the appropriate <b>cm:auditable</b> and transaction behaviour.
* <p>
* If the <tt>NodeRef</tt> of the node is changing (usually a store move) then deleted
* nodes are cleaned out where they might exist.
*
* @param oldNode the existing node, fully populated
* @param nodeUpdate the node update with all update elements populated
* @param nodeAspects the node's aspects or <tt>null</tt> to look them up
* @return <tt>true</tt> if any updates were made
*/
private boolean updateNodeImpl(Node oldNode, NodeUpdateEntity nodeUpdate, Set<QName> nodeAspects)
{
Long nodeId = oldNode.getId();
// Make sure that the ID has been populated
if (!EqualsHelper.nullSafeEquals(nodeId, nodeUpdate.getId()))
{
throw new IllegalArgumentException("NodeUpdateEntity node ID is not correct: " + nodeUpdate);
}
// Copy of the reference data
nodeUpdate.setStore(oldNode.getStore());
nodeUpdate.setUuid(oldNode.getUuid());
// Ensure that other values are set for completeness when caching
if (!nodeUpdate.isUpdateTypeQNameId())
{
nodeUpdate.setTypeQNameId(oldNode.getTypeQNameId());
}
if (!nodeUpdate.isUpdateLocaleId())
{
nodeUpdate.setLocaleId(oldNode.getLocaleId());
}
if (!nodeUpdate.isUpdateAclId())
{
nodeUpdate.setAclId(oldNode.getAclId());
}
if (!nodeUpdate.isUpdateDeleted())
{
nodeUpdate.setDeleted(oldNode.getDeleted());
}
nodeUpdate.setVersion(oldNode.getVersion());
// Update the transaction
TransactionEntity txn = getCurrentTransaction();
nodeUpdate.setTransaction(txn);
if (!txn.getId().equals(oldNode.getTransaction().getId()))
{
// Only update if the txn has changed
nodeUpdate.setUpdateTransaction(true);
}
// Update auditable
if (nodeAspects == null)
{
nodeAspects = getNodeAspects(nodeId);
}
if (nodeAspects.contains(ContentModel.ASPECT_AUDITABLE))
{
NodeRef oldNodeRef = oldNode.getNodeRef();
if (policyBehaviourFilter.isEnabled(oldNodeRef, ContentModel.ASPECT_AUDITABLE))
{
// Make sure that auditable properties are present
AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
if (auditableProps == null)
{
auditableProps = new AuditablePropertiesEntity();
}
else
{
auditableProps = new AuditablePropertiesEntity(auditableProps);
}
long modifiedDateToleranceMs = 1000L;
if (nodeUpdate.isUpdateTransaction())
{
// allow update cm:modified property for new transaction
modifiedDateToleranceMs = 0L;
}
boolean updateAuditableProperties = auditableProps.setAuditValues(null, null, false, modifiedDateToleranceMs);
nodeUpdate.setAuditableProperties(auditableProps);
nodeUpdate.setUpdateAuditableProperties(updateAuditableProperties);
}
else if (nodeUpdate.getAuditableProperties() == null)
{
// cache the explicit setting of auditable properties when creating node (note: auditable aspect is not yet present)
AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
if (auditableProps != null)
{
nodeUpdate.setAuditableProperties(auditableProps); // Can reuse the locked instance
nodeUpdate.setUpdateAuditableProperties(true);
}
}
else
{
// ALF-4117: NodeDAO: Allow cm:auditable to be set
// The nodeUpdate had auditable properties set, so we just use that directly
nodeUpdate.setUpdateAuditableProperties(true);
}
}
else
{
// Make sure that any auditable properties are removed
AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
if (auditableProps != null)
{
nodeUpdate.setAuditableProperties(null);
nodeUpdate.setUpdateAuditableProperties(true);
}
}
// Just bug out if nothing has changed
if (!nodeUpdate.isUpdateAnything())
{
return false;
}
// The node is remaining in the current store
int count = 0;
Throwable concurrencyException = null;
try
{
count = updateNode(nodeUpdate);
}
catch (Throwable e)
{
concurrencyException = e;
}
// Do concurrency check
if (count != 1)
{
// Drop the value from the cache in case the cache is stale
nodesCache.removeByKey(nodeId);
nodesCache.removeByValue(nodeUpdate);
throw new ConcurrencyFailureException("Failed to update node " + nodeId, concurrencyException);
}
else
{
// Check for wrap-around in the version number
if (nodeUpdate.getVersion().equals(LONG_ZERO))
{
// The version was wrapped back to zero
// The caches that are keyed by version are now unreliable
propertiesCache.clear();
aspectsCache.clear();
parentAssocsCache.clear();
}
// Update the caches
nodeUpdate.lock();
nodesCache.setValue(nodeId, nodeUpdate);
// The node's version has moved on so no need to invalidate caches
}
// Done
if (isDebugEnabled)
{
logger.debug(
"Updated Node: \n" +
" OLD: " + oldNode + "\n" +
" NEW: " + nodeUpdate);
}
return true;
}
public void setNodeAclId(Long nodeId, Long aclId)
{
Node oldNode = getNodeNotNull(nodeId);
NodeUpdateEntity nodeUpdateEntity = new NodeUpdateEntity();
nodeUpdateEntity.setId(nodeId);
nodeUpdateEntity.setAclId(aclId);
nodeUpdateEntity.setUpdateAclId(true);
updateNodeImpl(oldNode, nodeUpdateEntity, null);
}
public void setPrimaryChildrenSharedAclId(
Long primaryParentNodeId,
Long optionalOldSharedAlcIdInAdditionToNull,
Long newSharedAclId)
{
Long txnId = getCurrentTransaction().getId();
updatePrimaryChildrenSharedAclId(
txnId,
primaryParentNodeId,
optionalOldSharedAlcIdInAdditionToNull,
newSharedAclId);
invalidateNodeChildrenCaches(primaryParentNodeId, true, false);
}
public void deleteNode(Long nodeId)
{
Node node = getNodeNotNull(nodeId);
// Gather data for later
Long aclId = node.getAclId();
Set<QName> nodeAspects = getNodeAspects(nodeId);
// Finally mark the node as deleted
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
nodeUpdate.setId(nodeId);
// ACL
nodeUpdate.setAclId(null);
nodeUpdate.setUpdateAclId(true);
// Deleted
nodeUpdate.setDeleted(true);
nodeUpdate.setUpdateDeleted(true);
// Use a 'deleted' type QName
Long deletedQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_DELETED).getFirst();
nodeUpdate.setTypeQNameId(deletedQNameId);
nodeUpdate.setUpdateTypeQNameId(true);
boolean updated = updateNodeImpl(node, nodeUpdate, nodeAspects);
if (!updated)
{
invalidateNodeCaches(nodeId);
// Should not be attempting to delete a deleted node
throw new ConcurrencyFailureException(
"Failed to delete an existing live node: \n" +
" Before: " + node + "\n" +
" Update: " + nodeUpdate);
}
// Clean up content data
Set<QName> contentQNames = new HashSet<QName>(dictionaryService.getAllProperties(DataTypeDefinition.CONTENT));
Set<Long> contentQNamesToRemoveIds = qnameDAO.convertQNamesToIds(contentQNames, false);
contentDataDAO.deleteContentDataForNode(nodeId, contentQNamesToRemoveIds);
// Delete content usage deltas
usageDAO.deleteDeltas(nodeId);
// Handle sys:aspect_root
if (nodeAspects.contains(ContentModel.ASPECT_ROOT))
{
StoreRef storeRef = node.getStore().getStoreRef();
allRootNodesCache.remove(storeRef);
}
// Remove peer associations (no associated cache)
deleteNodeAssocsToAndFrom(nodeId);
// Remove child associations (invalidate children)
invalidateNodeChildrenCaches(nodeId, true, true);
invalidateNodeChildrenCaches(nodeId, false, true);
deleteChildAssocsToAndFrom(nodeId);
// Remove aspects
deleteNodeAspects(nodeId, null);
// Remove properties
deleteNodeProperties(nodeId, (Set<Long>) null);
// Remove subscriptions
deleteSubscriptions(nodeId);
// Remove ACLs
if (aclId != null)
{
aclDAO.deleteAclForNode(aclId, false);
}
}
@Override
public int purgeNodes(long maxTxnCommitTimeMs)
{
return deleteNodesByCommitTime(true, maxTxnCommitTimeMs);
}
/*
* Node Properties
*/
public Map<QName, Serializable> getNodeProperties(Long nodeId)
{
Map<QName, Serializable> props = getNodePropertiesCached(nodeId);
Node node = getNodeNotNull(nodeId);
// Handle sys:referenceable
ReferenceablePropertiesEntity.addReferenceableProperties(node, props);
// Handle sys:localized
LocalizedPropertiesEntity.addLocalizedProperties(localeDAO, node, props);
// Handle cm:auditable
if (hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
{
AuditablePropertiesEntity auditableProperties = node.getAuditableProperties();
if (auditableProperties == null)
{
auditableProperties = new AuditablePropertiesEntity();
}
props.putAll(auditableProperties.getAuditableProperties());
}
// Done
if (isDebugEnabled)
{
logger.debug("Fetched properties for Node: \n" +
" Node: " + nodeId + "\n" +
" Props: " + props);
}
return props;
}
public Serializable getNodeProperty(Long nodeId, QName propertyQName)
{
Serializable value = null;
// We have to load the node for cm:auditable
if (AuditablePropertiesEntity.isAuditableProperty(propertyQName))
{
Node node = getNodeNotNull(nodeId);
AuditablePropertiesEntity auditableProperties = node.getAuditableProperties();
if (auditableProperties != null)
{
value = auditableProperties.getAuditableProperty(propertyQName);
}
}
else if (ReferenceablePropertiesEntity.isReferenceableProperty(propertyQName)) // sys:referenceable
{
Node node = getNodeNotNull(nodeId);
value = ReferenceablePropertiesEntity.getReferenceableProperty(node, propertyQName);
}
else if (LocalizedPropertiesEntity.isLocalizedProperty(propertyQName)) // sys:localized
{
Node node = getNodeNotNull(nodeId);
value = LocalizedPropertiesEntity.getLocalizedProperty(localeDAO, node, propertyQName);
}
else
{
Map<QName, Serializable> props = getNodePropertiesCached(nodeId);
value = props.get(propertyQName);
}
// Done
if (isDebugEnabled)
{
logger.debug("Fetched property for Node: \n" +
" Node: " + nodeId + "\n" +
" QName: " + propertyQName + "\n" +
" Value: " + value);
}
return value;
}
/**
* Does differencing to add and/or remove properties. Internally, the existing properties
* will be retrieved and a difference performed to work out which properties need to be
* created, updated or deleted. It is only necessary to pass in old and new values for
* <i>changes</i> i.e. when setting a single property, it is only necessary to pass that
* property's value in the <b>old</b> and </b>new</b> maps; this improves execution speed
* significantly - although it has no effect on the number of resulting DB operations.
* <p/>
* Note: The cached properties are not updated
*
* @param nodeId the node ID
* @param newProps the properties to add or update
* @param isAddOnly <tt>true</tt> if the new properties are just an update or
* <tt>false</tt> if the properties are a complete set
* @return Returns <tt>true</tt> if any properties were changed
*/
private boolean setNodePropertiesImpl(
Long nodeId,
Map<QName, Serializable> newProps,
boolean isAddOnly)
{
if (isAddOnly && newProps.size() == 0)
{
return false; // No point adding nothing
}
// Get the current node
Node node = getNodeNotNull(nodeId);
// Create an update node
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
nodeUpdate.setId(nodeId);
// Copy inbound values
newProps = new HashMap<QName, Serializable>(newProps);
// Copy cm:auditable
if (!policyBehaviourFilter.isEnabled(node.getNodeRef(), ContentModel.ASPECT_AUDITABLE))
{
// Only bother if cm:auditable properties are present
if (AuditablePropertiesEntity.hasAuditableProperty(newProps.keySet()))
{
AuditablePropertiesEntity auditableProps = node.getAuditableProperties();
if (auditableProps == null)
{
auditableProps = new AuditablePropertiesEntity();
}
else
{
auditableProps = new AuditablePropertiesEntity(auditableProps); // Unlocked instance
}
boolean containedAuditProperties = auditableProps.setAuditValues(null, null, newProps);
if (!containedAuditProperties)
{
// Double-check (previous hasAuditableProperty should cover it)
// The behaviour is disabled, but no audit properties were passed in
auditableProps = null;
}
nodeUpdate.setAuditableProperties(auditableProps);
nodeUpdate.setUpdateAuditableProperties(true);
}
}
// Remove cm:auditable
newProps.keySet().removeAll(AuditablePropertiesEntity.getAuditablePropertyQNames());
// Check if the sys:localized property is being changed
Long oldNodeLocaleId = node.getLocaleId();
Locale newLocale = DefaultTypeConverter.INSTANCE.convert(
Locale.class,
newProps.get(ContentModel.PROP_LOCALE));
if (newLocale != null)
{
Long newNodeLocaleId = localeDAO.getOrCreateLocalePair(newLocale).getFirst();
if (!newNodeLocaleId.equals(oldNodeLocaleId))
{
nodeUpdate.setLocaleId(newNodeLocaleId);
nodeUpdate.setUpdateLocaleId(true);
}
}
// else: a 'null' new locale is completely ignored. This is the behaviour we choose.
// Remove sys:localized
LocalizedPropertiesEntity.removeLocalizedProperties(node, newProps);
// Remove sys:referenceable
ReferenceablePropertiesEntity.removeReferenceableProperties(node, newProps);
// Load the current properties.
// This means that we have to go to the DB during cold-write operations,
// but usually a write occurs after a node has been fetched of viewed in
// some way by the client code. Loading the existing properties has the
// advantage that the differencing code can eliminate unnecessary writes
// completely.
Map<QName, Serializable> oldPropsCached = getNodePropertiesCached(nodeId); // Keep pristine for caching
Map<QName, Serializable> oldProps = new HashMap<QName, Serializable>(oldPropsCached);
// If we're adding, remove current properties that are not of interest
if (isAddOnly)
{
oldProps.keySet().retainAll(newProps.keySet());
}
// We need to convert the new properties to our internally-used format,
// which is compatible with model i.e. people may have passed in data
// which needs to be converted to a model-compliant format. We do this
// before comparisons to avoid false negatives.
Map<NodePropertyKey, NodePropertyValue> newPropsRaw = nodePropertyHelper.convertToPersistentProperties(newProps);
newProps = nodePropertyHelper.convertToPublicProperties(newPropsRaw);
// Now find out what's changed
Map<QName, MapValueComparison> diff = EqualsHelper.getMapComparison(
oldProps,
newProps);
// Keep track of properties to delete and add
Set<QName> propsToDelete = new HashSet<QName>(oldProps.size()*2);
Map<QName, Serializable> propsToAdd = new HashMap<QName, Serializable>(newProps.size() * 2);
Set<QName> contentQNamesToDelete = new HashSet<QName>(5);
for (Map.Entry<QName, MapValueComparison> entry : diff.entrySet())
{
QName qname = entry.getKey();
PropertyDefinition removePropDef = dictionaryService.getProperty(qname);
boolean isContent = (removePropDef != null &&
removePropDef.getDataType().getName().equals(DataTypeDefinition.CONTENT));
switch (entry.getValue())
{
case EQUAL:
// Ignore
break;
case LEFT_ONLY:
// Not in the new properties
propsToDelete.add(qname);
if (isContent)
{
contentQNamesToDelete.add(qname);
}
break;
case NOT_EQUAL:
// Must remove from the LHS
propsToDelete.add(qname);
if (isContent)
{
contentQNamesToDelete.add(qname);
}
// Fall through to load up the RHS
case RIGHT_ONLY:
// We're adding this
Serializable value = newProps.get(qname);
if (isContent && value != null)
{
ContentData newContentData = (ContentData) value;
Long newContentDataId = contentDataDAO.createContentData(newContentData).getFirst();
value = new ContentDataWithId(newContentData, newContentDataId);
}
propsToAdd.put(qname, value);
break;
default:
throw new IllegalStateException("Unknown MapValueComparison: " + entry.getValue());
}
}
boolean modifyProps = propsToDelete.size() > 0 || propsToAdd.size() > 0;
boolean updated = modifyProps || nodeUpdate.isUpdateAnything();
// Bring the node into the current transaction
if (nodeUpdate.isUpdateAnything())
{
// We have to explicitly update the node (sys:locale or cm:auditable)
if (updateNodeImpl(node, nodeUpdate, null))
{
// Copy the caches across
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
NodeVersionKey newNodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
copyNodeAspectsCached(nodeVersionKey, newNodeVersionKey);
copyNodePropertiesCached(nodeVersionKey, newNodeVersionKey);
copyParentAssocsCached(nodeVersionKey, newNodeVersionKey);
}
}
else if (modifyProps)
{
// Touch the node; all caches are fine
touchNode(nodeId, null, null, false, false, false);
}
// Touch to bring into current txn
if (modifyProps)
{
// Clean up content properties
try
{
if (contentQNamesToDelete.size() > 0)
{
Set<Long> contentQNameIdsToDelete = qnameDAO.convertQNamesToIds(contentQNamesToDelete, false);
contentDataDAO.deleteContentDataForNode(nodeId, contentQNameIdsToDelete);
}
}
catch (Throwable e)
{
throw new AlfrescoRuntimeException(
"Failed to delete content properties: \n" +
" Node: " + nodeId + "\n" +
" Delete Tried: " + contentQNamesToDelete,
e);
}
try
{
// Apply deletes
Set<Long> propQNameIdsToDelete = qnameDAO.convertQNamesToIds(propsToDelete, true);
deleteNodeProperties(nodeId, propQNameIdsToDelete);
// Now create the raw properties for adding
newPropsRaw = nodePropertyHelper.convertToPersistentProperties(propsToAdd);
insertNodeProperties(nodeId, newPropsRaw);
}
catch (Throwable e)
{
// Don't trust the caches for the node
invalidateNodeCaches(nodeId);
// Focused error
throw new AlfrescoRuntimeException(
"Failed to write property deltas: \n" +
" Node: " + nodeId + "\n" +
" Old: " + oldProps + "\n" +
" New: " + newProps + "\n" +
" Diff: " + diff + "\n" +
" Delete Tried: " + propsToDelete + "\n" +
" Add Tried: " + propsToAdd,
e);
}
// Build the properties to cache based on whether this is an append or replace
Map<QName, Serializable> propsToCache = null;
if (isAddOnly)
{
// Combine the old and new properties
propsToCache = oldPropsCached;
propsToCache.putAll(propsToAdd);
}
else
{
// Replace old properties
propsToCache = newProps;
propsToCache.putAll(propsToAdd); // Ensure correct types
}
// Update cache
setNodePropertiesCached(nodeId, propsToCache);
}
// Done
if (isDebugEnabled && updated)
{
logger.debug(
"Modified node properties: " + nodeId + "\n" +
" Removed: " + propsToDelete + "\n" +
" Added: " + propsToAdd + "\n" +
" Node Update: " + nodeUpdate);
}
return updated;
}
public boolean setNodeProperties(Long nodeId, Map<QName, Serializable> properties)
{
// Merge with current values
boolean modified = setNodePropertiesImpl(nodeId, properties, false);
// Done
return modified;
}
public boolean addNodeProperty(Long nodeId, QName qname, Serializable value)
{
// Copy inbound values
Map<QName, Serializable> newProps = new HashMap<QName, Serializable>(3);
newProps.put(qname, value);
// Merge with current values
boolean modified = setNodePropertiesImpl(nodeId, newProps, true);
// Done
return modified;
}
public boolean addNodeProperties(Long nodeId, Map<QName, Serializable> properties)
{
// Merge with current values
boolean modified = setNodePropertiesImpl(nodeId, properties, true);
// Done
return modified;
}
public boolean removeNodeProperties(Long nodeId, Set<QName> propertyQNames)
{
propertyQNames = new HashSet<QName>(propertyQNames);
ReferenceablePropertiesEntity.removeReferenceableProperties(propertyQNames);
if (propertyQNames.size() == 0)
{
return false; // sys:referenceable properties cannot be removed
}
LocalizedPropertiesEntity.removeLocalizedProperties(propertyQNames);
if (propertyQNames.size() == 0)
{
return false; // sys:localized properties cannot be removed
}
Set<Long> qnameIds = qnameDAO.convertQNamesToIds(propertyQNames, false);
int deleteCount = deleteNodeProperties(nodeId, qnameIds);
if (deleteCount > 0)
{
// Touch the node; all caches are fine
touchNode(nodeId, null, null, false, false, false);
// Update cache
Map<QName, Serializable> cachedProps = getNodePropertiesCached(nodeId);
cachedProps.keySet().removeAll(propertyQNames);
setNodePropertiesCached(nodeId, cachedProps);
}
// Done
return deleteCount > 0;
}
@Override
public boolean setModifiedDate(Long nodeId, Date modifiedDate)
{
// Do nothing if the node is not cm:auditable
if (!hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
{
return false;
}
// Get the node
Node node = getNodeNotNull(nodeId);
NodeRef nodeRef = node.getNodeRef();
// Get the existing auditable values
AuditablePropertiesEntity auditableProps = node.getAuditableProperties();
boolean dateChanged = false;
if (auditableProps == null)
{
// The properties should be present
auditableProps = new AuditablePropertiesEntity();
auditableProps.setAuditValues(null, modifiedDate, true, 1000L);
dateChanged = true;
}
else
{
auditableProps = new AuditablePropertiesEntity(auditableProps);
dateChanged = auditableProps.setAuditModified(modifiedDate, 1000L);
}
if (dateChanged)
{
try
{
policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
// Touch the node; all caches are fine
return touchNode(nodeId, auditableProps, null, false, false, false);
}
finally
{
policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
}
}
else
{
// Date did not advance
return false;
}
}
/**
* @return Returns a writable copy of the cached property map
*/
private Map<QName, Serializable> getNodePropertiesCached(Long nodeId)
{
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
Pair<NodeVersionKey, Map<QName, Serializable>> cacheEntry = propertiesCache.getByKey(nodeVersionKey);
if (cacheEntry == null)
{
invalidateNodeCaches(nodeId);
throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
}
Map<QName, Serializable> cachedProperties = cacheEntry.getSecond();
// Need to return a harmlessly mutable map
Map<QName, Serializable> properties = copyPropertiesAgainstModification(cachedProperties);
// Done
return properties;
}
/**
* Update the node properties cache. The incoming properties will be wrapped to be
* unmodifiable.
* <p>
* <b>NOTE:</b> Incoming properties must exclude the <b>cm:auditable</b> properties
*/
private void setNodePropertiesCached(Long nodeId, Map<QName, Serializable> properties)
{
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
properties = copyPropertiesAgainstModification(properties);
propertiesCache.setValue(nodeVersionKey, Collections.unmodifiableMap(properties));
}
/**
* Helper method to copy cache values from one key to another
*/
private void copyNodePropertiesCached(NodeVersionKey from, NodeVersionKey to)
{
Map<QName, Serializable> cacheEntry = propertiesCache.getValue(from);
if (cacheEntry != null)
{
propertiesCache.setValue(to, cacheEntry);
}
}
/**
* Shallow-copies to a new map except for maps and collections that are binary serialized
*/
private Map<QName, Serializable> copyPropertiesAgainstModification(Map<QName, Serializable> original)
{
// Copy the values, ensuring that any collections are copied as well
Map<QName, Serializable> copy = new HashMap<QName, Serializable>((int)(original.size() * 1.3));
for (Map.Entry<QName, Serializable> element : original.entrySet())
{
QName key = element.getKey();
Serializable value = element.getValue();
if (value instanceof Collection<?> || value instanceof Map<?, ?>)
{
value = (Serializable) SerializationUtils.deserialize(SerializationUtils.serialize(value));
}
copy.put(key, value);
}
return copy;
}
/**
* Callback to cache node properties. The DAO callback only does the simple {@link #findByKey(Long)}.
*
* @author Derek Hulley
* @since 3.4
*/
private class PropertiesCallbackDAO extends EntityLookupCallbackDAOAdaptor<NodeVersionKey, Map<QName, Serializable>, Serializable>
{
public Pair<NodeVersionKey, Map<QName, Serializable>> createValue(Map<QName, Serializable> value)
{
throw new UnsupportedOperationException("A node always has a 'map' of properties.");
}
public Pair<NodeVersionKey, Map<QName, Serializable>> findByKey(NodeVersionKey nodeVersionKey)
{
Long nodeId = nodeVersionKey.getNodeId();
Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> propsRawByNodeVersionKey = selectNodeProperties(nodeId);
Map<NodePropertyKey, NodePropertyValue> propsRaw = propsRawByNodeVersionKey.get(nodeVersionKey);
if (propsRaw == null)
{
// Didn't find a match. Is this because there are none?
if (propsRawByNodeVersionKey.size() == 0)
{
// This is OK. The node has no properties
propsRaw = Collections.emptyMap();
}
else
{
// We found properties associated with a different node ID and version
invalidateNodeCaches(nodeId);
throw new DataIntegrityViolationException(
"Detected stale node entry: " + nodeVersionKey +
" (now " + propsRawByNodeVersionKey.keySet() + ")");
}
}
// Convert to public properties
Map<QName, Serializable> props = nodePropertyHelper.convertToPublicProperties(propsRaw);
// Done
return new Pair<NodeVersionKey, Map<QName, Serializable>>(nodeVersionKey, Collections.unmodifiableMap(props));
}
}
/*
* Aspects
*/
public Set<QName> getNodeAspects(Long nodeId)
{
Set<QName> nodeAspects = getNodeAspectsCached(nodeId);
// Nodes are always referenceable
nodeAspects.add(ContentModel.ASPECT_REFERENCEABLE);
// Nodes are always localized
nodeAspects.add(ContentModel.ASPECT_LOCALIZED);
return nodeAspects;
}
public boolean hasNodeAspect(Long nodeId, QName aspectQName)
{
if (aspectQName.equals(ContentModel.ASPECT_REFERENCEABLE))
{
// Nodes are always referenceable
return true;
}
if (aspectQName.equals(ContentModel.ASPECT_LOCALIZED))
{
// Nodes are always localized
return true;
}
Set<QName> nodeAspects = getNodeAspectsCached(nodeId);
return nodeAspects.contains(aspectQName);
}
public boolean addNodeAspects(Long nodeId, Set<QName> aspectQNames)
{
if (aspectQNames.size() == 0)
{
return false;
}
// Copy the inbound set
Set<QName> aspectQNamesToAdd = new HashSet<QName>(aspectQNames);
// Get existing
Set<QName> existingAspectQNames = getNodeAspectsCached(nodeId);
// Find out what needs adding
aspectQNamesToAdd.removeAll(existingAspectQNames);
aspectQNamesToAdd.remove(ContentModel.ASPECT_REFERENCEABLE); // Implicit
aspectQNamesToAdd.remove(ContentModel.ASPECT_LOCALIZED); // Implicit
if (aspectQNamesToAdd.isEmpty())
{
// Nothing to do
return false;
}
// Add them
Set<Long> aspectQNameIds = qnameDAO.convertQNamesToIds(aspectQNamesToAdd, true);
startBatch();
try
{
for (Long aspectQNameId : aspectQNameIds)
{
insertNodeAspect(nodeId, aspectQNameId);
}
}
catch (RuntimeException e)
{
// This could be because the cache is out of date
invalidateNodeCaches(nodeId);
throw e;
}
finally
{
executeBatch();
}
// Collate the new aspect set, so that touch recognizes the addtion of cm:auditable
Set<QName> newAspectQNames = new HashSet<QName>(existingAspectQNames);
newAspectQNames.addAll(aspectQNamesToAdd);
// Handle sys:aspect_root
if (aspectQNames.contains(ContentModel.ASPECT_ROOT))
{
// invalidate root nodes cache for the store
StoreRef storeRef = getNodeNotNull(nodeId).getStore().getStoreRef();
allRootNodesCache.remove(storeRef);
// Touch the node; parent assocs need invalidation
touchNode(nodeId, null, newAspectQNames, false, false, true);
}
else
{
// Touch the node; all caches are fine
touchNode(nodeId, null, newAspectQNames, false, false, false);
}
// Manually update the cache
setNodeAspectsCached(nodeId, newAspectQNames);
// Done
return true;
}
public boolean removeNodeAspects(Long nodeId)
{
Set<QName> newAspectQNames = Collections.<QName>emptySet();
// Touch the node; all caches are fine
touchNode(nodeId, null, newAspectQNames, false, false, false);
// Just delete all the node's aspects
int deleteCount = deleteNodeAspects(nodeId, null);
// Manually update the cache
setNodeAspectsCached(nodeId, newAspectQNames);
// Done
return deleteCount > 0;
}
public boolean removeNodeAspects(Long nodeId, Set<QName> aspectQNames)
{
if (aspectQNames.size() == 0)
{
return false;
}
// Get the current aspects
Set<QName> existingAspectQNames = getNodeAspects(nodeId);
// Collate the new set of aspects so that touch works correctly against cm:auditable
Set<QName> newAspectQNames = new HashSet<QName>(existingAspectQNames);
newAspectQNames.removeAll(aspectQNames);
// Touch the node; all caches are fine
touchNode(nodeId, null, newAspectQNames, false, false, false);
// Now remove each aspect
Set<Long> aspectQNameIdsToRemove = qnameDAO.convertQNamesToIds(aspectQNames, false);
int deleteCount = deleteNodeAspects(nodeId, aspectQNameIdsToRemove);
if (deleteCount == 0)
{
return false;
}
// Handle sys:aspect_root
if (aspectQNames.contains(ContentModel.ASPECT_ROOT))
{
// invalidate root nodes cache for the store
StoreRef storeRef = getNodeNotNull(nodeId).getStore().getStoreRef();
allRootNodesCache.remove(storeRef);
// Touch the node; parent assocs need invalidation
touchNode(nodeId, null, newAspectQNames, false, false, true);
}
else
{
// Touch the node; all caches are fine
touchNode(nodeId, null, newAspectQNames, false, false, false);
}
// Manually update the cache
setNodeAspectsCached(nodeId, newAspectQNames);
// Done
return deleteCount > 0;
}
public void getNodesWithAspects(
Set<QName> aspectQNames,
Long minNodeId, Long maxNodeId,
NodeRefQueryCallback resultsCallback)
{
Set<Long> qnameIdsSet = qnameDAO.convertQNamesToIds(aspectQNames, false);
if (qnameIdsSet.size() == 0)
{
// No point running a query
return;
}
List<Long> qnameIds = new ArrayList<Long>(qnameIdsSet);
selectNodesWithAspects(qnameIds, minNodeId, maxNodeId, resultsCallback);
}
/**
* @return Returns a writable copy of the cached aspects set
*/
private Set<QName> getNodeAspectsCached(Long nodeId)
{
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
Pair<NodeVersionKey, Set<QName>> cacheEntry = aspectsCache.getByKey(nodeVersionKey);
if (cacheEntry == null)
{
invalidateNodeCaches(nodeId);
throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
}
return new HashSet<QName>(cacheEntry.getSecond());
}
/**
* Update the node aspects cache. The incoming set will be wrapped to be unmodifiable.
*/
private void setNodeAspectsCached(Long nodeId, Set<QName> aspects)
{
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
aspectsCache.setValue(nodeVersionKey, Collections.unmodifiableSet(aspects));
}
/**
* Helper method to copy cache values from one key to another
*/
private void copyNodeAspectsCached(NodeVersionKey from, NodeVersionKey to)
{
Set<QName> cacheEntry = aspectsCache.getValue(from);
if (cacheEntry != null)
{
aspectsCache.setValue(to, cacheEntry);
}
}
/**
* Callback to cache node aspects. The DAO callback only does the simple {@link #findByKey(Long)}.
*
* @author Derek Hulley
* @since 3.4
*/
private class AspectsCallbackDAO extends EntityLookupCallbackDAOAdaptor<NodeVersionKey, Set<QName>, Serializable>
{
public Pair<NodeVersionKey, Set<QName>> createValue(Set<QName> value)
{
throw new UnsupportedOperationException("A node always has a 'set' of aspects.");
}
public Pair<NodeVersionKey, Set<QName>> findByKey(NodeVersionKey nodeVersionKey)
{
Long nodeId = nodeVersionKey.getNodeId();
Set<Long> nodeIds = Collections.singleton(nodeId);
Map<NodeVersionKey, Set<QName>> nodeAspectQNameIdsByVersionKey = selectNodeAspects(nodeIds);
Set<QName> nodeAspectQNames = nodeAspectQNameIdsByVersionKey.get(nodeVersionKey);
if (nodeAspectQNames == null)
{
// Didn't find a match. Is this because there are none?
if (nodeAspectQNameIdsByVersionKey.size() == 0)
{
// This is OK. The node has no properties
nodeAspectQNames = Collections.emptySet();
}
else
{
// We found properties associated with a different node ID and version
invalidateNodeCaches(nodeId);
throw new DataIntegrityViolationException(
"Detected stale node entry: " + nodeVersionKey +
" (now " + nodeAspectQNameIdsByVersionKey.keySet() + ")");
}
}
// Done
return new Pair<NodeVersionKey, Set<QName>>(nodeVersionKey, Collections.unmodifiableSet(nodeAspectQNames));
}
}
/*
* Node assocs
*/
@Override
public Long newNodeAssoc(Long sourceNodeId, Long targetNodeId, QName assocTypeQName, int assocIndex)
{
if (assocIndex == 0)
{
throw new IllegalArgumentException("Index is 1-based, or -1 to indicate 'next value'.");
}
// Touch the node; all caches are fine
touchNode(sourceNodeId, null, null, false, false, false);
// Resolve type QName
Long assocTypeQNameId = qnameDAO.getOrCreateQName(assocTypeQName).getFirst();
// Get the current max; we will need this no matter what
if (assocIndex <= 0)
{
int maxIndex = selectNodeAssocMaxIndex(sourceNodeId, assocTypeQNameId);
assocIndex = maxIndex + 1;
}
Long result = null;
Savepoint savepoint = controlDAO.createSavepoint("NodeService.newNodeAssoc");
try
{
result = insertNodeAssoc(sourceNodeId, targetNodeId, assocTypeQNameId, assocIndex);
controlDAO.releaseSavepoint(savepoint);
return result;
}
catch (Throwable e)
{
controlDAO.rollbackToSavepoint(savepoint);
throw new AssociationExistsException(sourceNodeId, targetNodeId, assocTypeQName);
}
}
@Override
public void setNodeAssocIndex(Long id, int assocIndex)
{
int updated = updateNodeAssoc(id, assocIndex);
if (updated != 1)
{
throw new ConcurrencyFailureException("Expected to update exactly one row: " + id);
}
}
public int removeNodeAssoc(Long sourceNodeId, Long targetNodeId, QName assocTypeQName)
{
Pair<Long, QName> assocTypeQNamePair = qnameDAO.getQName(assocTypeQName);
if (assocTypeQNamePair == null)
{
// Never existed
return 0;
}
Long assocTypeQNameId = assocTypeQNamePair.getFirst();
int deleted = deleteNodeAssoc(sourceNodeId, targetNodeId, assocTypeQNameId);
if (deleted > 0)
{
// Touch the node; all caches are fine
touchNode(sourceNodeId, null, null, false, false, false);
}
return deleted;
}
public int removeNodeAssocsToAndFrom(Long nodeId)
{
int deleted = deleteNodeAssocsToAndFrom(nodeId);
if (deleted > 0)
{
// Touch the node; all caches are fine
touchNode(nodeId, null, null, false, false, false);
}
return deleted;
}
public int removeNodeAssocsToAndFrom(Long nodeId, Set<QName> assocTypeQNames)
{
Set<Long> assocTypeQNameIds = qnameDAO.convertQNamesToIds(assocTypeQNames, false);
if (assocTypeQNameIds.size() == 0)
{
// Never existed
return 0;
}
int deleted = deleteNodeAssocsToAndFrom(nodeId, assocTypeQNameIds);
if (deleted > 0)
{
// Touch the node; all caches are fine
touchNode(nodeId, null, null, false, false, false);
}
return deleted;
}
@Override
public int removeNodeAssocs(List<Long> ids)
{
int toDelete = ids.size();
if (toDelete == 0)
{
return 0;
}
int deleted = deleteNodeAssocs(ids);
if (toDelete != deleted)
{
throw new ConcurrencyFailureException("Deleted " + deleted + " but expected " + toDelete);
}
return deleted;
}
@Override
public Collection<Pair<Long, AssociationRef>> getSourceNodeAssocs(Long targetNodeId, QName typeQName)
{
Long typeQNameId = null;
if (typeQName != null)
{
Pair<Long, QName> typeQNamePair = qnameDAO.getQName(typeQName);
if (typeQNamePair == null)
{
// No such QName
return Collections.emptyList();
}
typeQNameId = typeQNamePair.getFirst();
}
List<NodeAssocEntity> nodeAssocEntities = selectNodeAssocsByTarget(targetNodeId, typeQNameId);
List<Pair<Long, AssociationRef>> results = new ArrayList<Pair<Long,AssociationRef>>(nodeAssocEntities.size());
for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
{
Long assocId = nodeAssocEntity.getId();
AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
results.add(new Pair<Long, AssociationRef>(assocId, assocRef));
}
return results;
}
@Override
public Collection<Pair<Long, AssociationRef>> getTargetNodeAssocs(Long sourceNodeId, QName typeQName)
{
Long typeQNameId = null;
if (typeQName != null)
{
Pair<Long, QName> typeQNamePair = qnameDAO.getQName(typeQName);
if (typeQNamePair == null)
{
// No such QName
return Collections.emptyList();
}
typeQNameId = typeQNamePair.getFirst();
}
List<NodeAssocEntity> nodeAssocEntities = selectNodeAssocsBySource(sourceNodeId, typeQNameId);
List<Pair<Long, AssociationRef>> results = new ArrayList<Pair<Long,AssociationRef>>(nodeAssocEntities.size());
for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
{
Long assocId = nodeAssocEntity.getId();
AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
results.add(new Pair<Long, AssociationRef>(assocId, assocRef));
}
return results;
}
@Override
public Pair<Long, AssociationRef> getNodeAssocOrNull(Long assocId)
{
NodeAssocEntity nodeAssocEntity = selectNodeAssocById(assocId);
if (nodeAssocEntity == null)
{
return null;
}
else
{
AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
return new Pair<Long, AssociationRef>(assocId, assocRef);
}
}
public Pair<Long, AssociationRef> getNodeAssoc(Long assocId)
{
Pair<Long, AssociationRef> ret = getNodeAssocOrNull(assocId);
if (ret == null)
{
throw new ConcurrencyFailureException("Assoc ID does not point to a valid association: " + assocId);
}
else
{
return ret;
}
}
/*
* Child assocs
*/
private ChildAssocEntity newChildAssocImpl(
Long parentNodeId,
Long childNodeId,
boolean isPrimary,
final QName assocTypeQName,
QName assocQName,
final String childNodeName)
{
Assert.notNull(parentNodeId, "parentNodeId");
Assert.notNull(childNodeId, "childNodeId");
Assert.notNull(assocTypeQName, "assocTypeQName");
Assert.notNull(assocQName, "assocQName");
Assert.notNull(childNodeName, "childNodeName");
// Get parent and child nodes. We need them later, so just get them now.
final Node parentNode = getNodeNotNull(parentNodeId);
final Node childNode = getNodeNotNull(childNodeId);
final ChildAssocEntity assoc = new ChildAssocEntity();
// Parent node
assoc.setParentNode(new NodeEntity(parentNode));
// Child node
assoc.setChildNode(new NodeEntity(childNode));
// Type QName
assoc.setTypeQNameAll(qnameDAO, assocTypeQName, true);
// Child node name
assoc.setChildNodeNameAll(dictionaryService, assocTypeQName, childNodeName);
// QName
assoc.setQNameAll(qnameDAO, assocQName, true);
// Primary
assoc.setPrimary(isPrimary);
// Index
assoc.setAssocIndex(-1);
RetryingCallback<Long> callback = new RetryingCallback<Long>()
{
public Long execute() throws Throwable
{
Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
try
{
Long id = insertChildAssoc(assoc);
controlDAO.releaseSavepoint(savepoint);
return id;
}
catch (Throwable e)
{
controlDAO.rollbackToSavepoint(savepoint);
// SQL Server retry
if (e.getMessage().contains("Snapshot isolation transaction aborted"))
{
logger.warn("insertChildAssoc: SQL Server snapshot isolation retry: "+assoc);
throw new ConcurrencyFailureException("SQL Server snapshot isolation retry...", e);
}
// FK conflict retry, eg.
// SQL Server - The INSERT statement conflicted with the FOREIGN KEY constraint
// MySQL - Cannot add or update a child row: a foreign key constraint fails
if (e.getMessage().toUpperCase().contains("FOREIGN KEY"))
{
logger.warn("insertChildAssoc: FK conflict retry: "+assoc);
throw new ConcurrencyFailureException("FK conflict retry...", e);
}
// We assume that this is from the child cm:name constraint violation
throw new DuplicateChildNodeNameException(
parentNode.getNodeRef(),
assocTypeQName,
childNodeName,
e);
}
}
};
Long assocId = childAssocRetryingHelper.doWithRetry(callback);
// Persist it
assoc.setId(assocId);
// Primary associations accompany new nodes, so we only have to bring the
// node into the current transaction for secondary associations
if (!isPrimary)
{
updateNode(childNodeId, null, null);
}
// Done
if (isDebugEnabled)
{
logger.debug("Created child association: " + assoc);
}
return assoc;
}
public Pair<Long, ChildAssociationRef> newChildAssoc(
Long parentNodeId,
Long childNodeId,
QName assocTypeQName,
QName assocQName,
String childNodeName)
{
ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId);
// Create it
ChildAssocEntity assoc = newChildAssocImpl(
parentNodeId, childNodeId, false, assocTypeQName, assocQName, childNodeName);
Long assocId = assoc.getId();
// Touch the node; all caches are fine
touchNode(childNodeId, null, null, false, false, false);
// update cache
parentAssocInfo = parentAssocInfo.addAssoc(assocId, assoc);
setParentAssocsCached(childNodeId, parentAssocInfo);
// Done
return assoc.getPair(qnameDAO);
}
public void deleteChildAssoc(Long assocId)
{
ChildAssocEntity assoc = selectChildAssoc(assocId);
if (assoc == null)
{
throw new ConcurrencyFailureException("Child association not found: " + assocId);
}
// Update cache
Long childNodeId = assoc.getChildNode().getId();
ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId);
// Delete it
int count = deleteChildAssocById(assocId);
if (count != 1)
{
throw new ConcurrencyFailureException("Child association not deleted: " + assocId);
}
// Touch the node; all caches are fine
touchNode(childNodeId, null, null, false, false, false);
// Update cache
parentAssocInfo = parentAssocInfo.removeAssoc(assocId);
setParentAssocsCached(childNodeId, parentAssocInfo);
}
public int setChildAssocIndex(Long parentNodeId, Long childNodeId, QName assocTypeQName, QName assocQName, int index)
{
int count = updateChildAssocIndex(parentNodeId, childNodeId, assocTypeQName, assocQName, index);
if (count > 0)
{
// Touch the node; parent assocs are out of sync
touchNode(childNodeId, null, null, false, false, true);
}
return count;
}
/**
* TODO: See about pulling automatic cm:name update logic into this DAO
*/
public void setChildAssocsUniqueName(final Long childNodeId, final String childName)
{
RetryingCallback<Integer> callback = new RetryingCallback<Integer>()
{
public Integer execute() throws Throwable
{
Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
try
{
Integer count = updateChildAssocsUniqueName(childNodeId, childName);
controlDAO.releaseSavepoint(savepoint);
return count;
}
catch (Throwable e)
{
controlDAO.rollbackToSavepoint(savepoint);
// We assume that this is from the child cm:name constraint violation
throw new DuplicateChildNodeNameException(null, null, childName, e);
}
}
};
Integer count = childAssocRetryingHelper.doWithRetry(callback);
if (count > 0)
{
// Touch the node; parent assocs are out of sync
touchNode(childNodeId, null, null, false, false, true);
}
if (isDebugEnabled)
{
logger.debug(
"Updated cm:name to parent assocs: \n" +
" Node: " + childNodeId + "\n" +
" Name: " + childName + "\n" +
" Updated: " + count);
}
}
public Pair<Long, ChildAssociationRef> getChildAssoc(Long assocId)
{
ChildAssocEntity assoc = selectChildAssoc(assocId);
if (assoc == null)
{
throw new ConcurrencyFailureException("Child association not found: " + assocId);
}
return assoc.getPair(qnameDAO);
}
public List<NodeIdAndAclId> getPrimaryChildrenAcls(Long nodeId)
{
return selectPrimaryChildAcls(nodeId);
}
public Pair<Long, ChildAssociationRef> getChildAssoc(
Long parentNodeId,
Long childNodeId,
QName assocTypeQName,
QName assocQName)
{
List<ChildAssocEntity> assocs = selectChildAssoc(parentNodeId, childNodeId, assocTypeQName, assocQName);
if (assocs.size() == 0)
{
return null;
}
else if (assocs.size() == 1)
{
return assocs.get(0).getPair(qnameDAO);
}
// Keep the primary association or, if there isn't one, the association with the smallest ID
Map<Long, ChildAssocEntity> assocsToDeleteById = new HashMap<Long, ChildAssocEntity>(assocs.size() * 2);
Long minId = null;
Long primaryId = null;
for (ChildAssocEntity assoc : assocs)
{
// First store it
Long assocId = assoc.getId();
assocsToDeleteById.put(assocId, assoc);
if (minId == null || minId.compareTo(assocId) > 0)
{
minId = assocId;
}
if (assoc.isPrimary())
{
primaryId = assocId;
}
}
// Remove either the primary or min assoc
Long assocToKeepId = primaryId == null ? minId : primaryId;
ChildAssocEntity assocToKeep = assocsToDeleteById.remove(assocToKeepId);
// If the current transaction allows, remove the other associations
if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_WRITE)
{
for (Long assocIdToDelete : assocsToDeleteById.keySet())
{
deleteChildAssoc(assocIdToDelete);
}
}
// Done
return assocToKeep.getPair(qnameDAO);
}
/**
* Callback that applies node preloading if required.
* <p/>
* Instances must be used and discarded per query.
*
* @author Derek Hulley
* @since 3.4
*/
private class ChildAssocRefBatchingQueryCallback implements ChildAssocRefQueryCallback
{
private final ChildAssocRefQueryCallback callback;
private final boolean preload;
private final List<NodeRef> nodeRefs;
/**
* @param callback the callback to batch around
*/
private ChildAssocRefBatchingQueryCallback(ChildAssocRefQueryCallback callback)
{
this.callback = callback;
this.preload = callback.preLoadNodes();
if (preload)
{
nodeRefs = new LinkedList<NodeRef>(); // No memory required
}
else
{
nodeRefs = null; // No list needed
}
}
/**
* @throws UnsupportedOperationException always
*/
public boolean preLoadNodes()
{
throw new UnsupportedOperationException("Expected to be used internally only.");
}
/**
* Defers to delegate
*/
@Override
public boolean orderResults()
{
return callback.orderResults();
}
/**
* {@inheritDoc}
*/
public boolean handle(
Pair<Long, ChildAssociationRef> childAssocPair,
Pair<Long, NodeRef> parentNodePair,
Pair<Long, NodeRef> childNodePair)
{
if (preload)
{
nodeRefs.add(childNodePair.getSecond());
}
return callback.handle(childAssocPair, parentNodePair, childNodePair);
}
public void done()
{
// Finish the batch
if (preload && nodeRefs.size() > 0)
{
cacheNodes(nodeRefs);
nodeRefs.clear();
}
// Done
callback.done();
}
}
public void getChildAssocs(
Long parentNodeId,
Long childNodeId,
QName assocTypeQName,
QName assocQName,
Boolean isPrimary,
Boolean sameStore,
ChildAssocRefQueryCallback resultsCallback)
{
selectChildAssocs(
parentNodeId, childNodeId,
assocTypeQName, assocQName, isPrimary, sameStore,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
}
@Override
public void getChildAssocs(
Long parentNodeId,
QName assocTypeQName,
QName assocQName,
int maxResults,
ChildAssocRefQueryCallback resultsCallback)
{
selectChildAssocs(
parentNodeId,
assocTypeQName,
assocQName,
maxResults,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
}
public void getChildAssocs(Long parentNodeId, Set<QName> assocTypeQNames, ChildAssocRefQueryCallback resultsCallback)
{
switch (assocTypeQNames.size())
{
case 0:
return; // No results possible
case 1:
QName assocTypeQName = assocTypeQNames.iterator().next();
selectChildAssocs(
parentNodeId, null, assocTypeQName, (QName) null, null, null,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
break;
default:
selectChildAssocs(
parentNodeId, assocTypeQNames,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
}
}
/**
* Checks a cache and then queries.
* <p/>
* Note: If we were to cach misses, then we would have to ensure that the cache is
* kept up to date whenever any affection association is changed. This is actually
* not possible without forcing the cache to be fully clustered. So to
* avoid clustering the cache, we instead watch the node child version,
* which relies on a cache that is already clustered.
*/
public Pair<Long, ChildAssociationRef> getChildAssoc(Long parentNodeId, QName assocTypeQName, String childName)
{
ChildByNameKey key = new ChildByNameKey(parentNodeId, assocTypeQName, childName);
ChildAssocEntity assoc = childByNameCache.get(key);
boolean query = false;
if (assoc == null)
{
query = true;
}
else
{
// Check that the resultant child node has not moved on
Node childNode = assoc.getChildNode();
Long childNodeId = childNode.getId();
NodeVersionKey childNodeVersionKey = childNode.getNodeVersionKey();
Pair<Long, Node> childNodeFromCache = nodesCache.getByKey(childNodeId);
if (childNodeFromCache == null)
{
// Child node no longer exists (or never did)
query = true;
}
else
{
NodeVersionKey childNodeFromCacheVersionKey = childNodeFromCache.getSecond().getNodeVersionKey();
if (!childNodeFromCacheVersionKey.equals(childNodeVersionKey))
{
// The child node has moved on. We don't know why, but must query again.
query = true;
}
}
}
if (query)
{
assoc = selectChildAssoc(parentNodeId, assocTypeQName, childName);
if (assoc != null)
{
childByNameCache.put(key, assoc);
}
else
{
// We do not cache misses. See javadoc.
}
}
// Now return, checking the assoc's ID for null
return assoc == null ? null : assoc.getPair(qnameDAO);
}
public void getChildAssocs(
Long parentNodeId,
QName assocTypeQName,
Collection<String> childNames,
ChildAssocRefQueryCallback resultsCallback)
{
selectChildAssocs(
parentNodeId, assocTypeQName, childNames,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
}
public void getChildAssocsByPropertyValue(
Long parentNodeId,
QName propertyQName,
Serializable value,
ChildAssocRefQueryCallback resultsCallback)
{
PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName);
NodePropertyValue nodeValue = nodePropertyHelper.makeNodePropertyValue(propertyDef, value);
if(nodeValue != null)
{
switch (nodeValue.getPersistedType())
{
case 1: // Boolean
case 3: // long
case 5: // double
case 6: // string
// no floats due to the range errors testing equality on a float.
break;
default:
throw new IllegalArgumentException("method not supported for persisted value type " + nodeValue.getPersistedType());
}
selectChildAssocsByPropertyValue(parentNodeId,
propertyQName,
nodeValue,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
}
}
public void getChildAssocsByChildTypes(
Long parentNodeId,
Set<QName> childNodeTypeQNames,
ChildAssocRefQueryCallback resultsCallback)
{
selectChildAssocsByChildTypes(
parentNodeId, childNodeTypeQNames,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
}
public void getChildAssocsWithoutParentAssocsOfType(
Long parentNodeId,
QName assocTypeQName,
ChildAssocRefQueryCallback resultsCallback)
{
selectChildAssocsWithoutParentAssocsOfType(
parentNodeId, assocTypeQName,
new ChildAssocRefBatchingQueryCallback(resultsCallback));
}
public Pair<Long, ChildAssociationRef> getPrimaryParentAssoc(Long childNodeId)
{
ChildAssocEntity childAssocEntity = getPrimaryParentAssocImpl(childNodeId);
if(childAssocEntity == null)
{
return null;
}
else
{
return childAssocEntity.getPair(qnameDAO);
}
}
private ChildAssocEntity getPrimaryParentAssocImpl(Long childNodeId)
{
ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
return parentAssocs.getPrimaryParentAssoc();
}
private static final int PARENT_ASSOCS_CACHE_FILTER_THRESHOLD = 2000;
public void getParentAssocs(
Long childNodeId,
QName assocTypeQName,
QName assocQName,
Boolean isPrimary,
ChildAssocRefQueryCallback resultsCallback)
{
if (assocTypeQName == null && assocQName == null && isPrimary == null)
{
// Go for the cache (and return all)
ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
for (ChildAssocEntity assoc : parentAssocs.getParentAssocs().values())
{
resultsCallback.handle(
assoc.getPair(qnameDAO),
assoc.getParentNode().getNodePair(),
assoc.getChildNode().getNodePair());
}
resultsCallback.done();
}
else
{
// Decide whether we query or filter
ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
if (parentAssocs.getParentAssocs().size() > PARENT_ASSOCS_CACHE_FILTER_THRESHOLD)
{
// Query
selectParentAssocs(childNodeId, assocTypeQName, assocQName, isPrimary, resultsCallback);
}
else
{
// Go for the cache (and filter)
for (ChildAssocEntity assoc : parentAssocs.getParentAssocs().values())
{
Pair<Long, ChildAssociationRef> assocPair = assoc.getPair(qnameDAO);
if (((assocTypeQName == null) || (assocPair.getSecond().getTypeQName().equals(assocTypeQName))) &&
((assocQName == null) || (assocPair.getSecond().getQName().equals(assocQName))))
{
resultsCallback.handle(
assocPair,
assoc.getParentNode().getNodePair(),
assoc.getChildNode().getNodePair());
}
}
resultsCallback.done();
}
}
}
/**
* Potentially cheaper than evaluating all of a node's paths to check for child association cycles
* <p/>
* TODO: When is it cheaper to go up and when is it cheaper to go down?
* Look at using direct queries to pass through layers both up and down.
*
* @param nodeId the node to start with
*/
public void cycleCheck(Long nodeId)
{
CycleCallBack callback = new CycleCallBack();
callback.cycleCheck(nodeId);
if (callback.toThrow != null)
{
throw callback.toThrow;
}
}
private class CycleCallBack implements ChildAssocRefQueryCallback
{
final Set<Long> nodeIds = new HashSet<Long>(97);
CyclicChildRelationshipException toThrow;
@Override
public void done()
{
}
@Override
public boolean handle(
Pair<Long, ChildAssociationRef> childAssocPair,
Pair<Long, NodeRef> parentNodePair,
Pair<Long, NodeRef> childNodePair)
{
Long nodeId = childNodePair.getFirst();
if (!nodeIds.add(nodeId))
{
ChildAssociationRef childAssociationRef = childAssocPair.getSecond();
// Remember exception we want to throw and exit. If we throw within here, it will be wrapped by IBatis
toThrow = new CyclicChildRelationshipException(
"Child Association Cycle detected hitting nodes: " + nodeIds,
childAssociationRef);
return false;
}
cycleCheck(nodeId);
nodeIds.remove(nodeId);
return toThrow == null;
}
/**
* No preloading required
*/
@Override
public boolean preLoadNodes()
{
return false;
}
/**
* No ordering required
*/
@Override
public boolean orderResults()
{
return false;
}
public void cycleCheck(Long nodeId)
{
getChildAssocs(nodeId, null, null, null, null, null, this);
}
};
public List<Path> getPaths(Pair<Long, NodeRef> nodePair, boolean primaryOnly) throws InvalidNodeRefException
{
// create storage for the paths - only need 1 bucket if we are looking for the primary path
List<Path> paths = new ArrayList<Path>(primaryOnly ? 1 : 10);
// create an empty current path to start from
Path currentPath = new Path();
// create storage for touched associations
Stack<Long> assocIdStack = new Stack<Long>();
// call recursive method to sort it out
prependPaths(nodePair, null, currentPath, paths, assocIdStack, primaryOnly);
// check that for the primary only case we have exactly one path
if (primaryOnly && paths.size() != 1)
{
throw new RuntimeException("Node has " + paths.size() + " primary paths: " + nodePair);
}
// done
if (loggerPaths.isDebugEnabled())
{
StringBuilder sb = new StringBuilder(256);
if (primaryOnly)
{
sb.append("Primary paths");
}
else
{
sb.append("Paths");
}
sb.append(" for node ").append(nodePair);
for (Path path : paths)
{
sb.append("\n").append(" ").append(path);
}
loggerPaths.debug(sb);
}
return paths;
}
/**
* Build the paths for a node
*
* @param currentNodePair the leave or child node to start with
* @param currentRootNodePair pass in <tt>null</tt> only
* @param currentPath an empty {@link Path}
* @param completedPaths completed paths i.e. the result
* @param assocIdStack a stack to detected cyclic relationships
* @param primaryOnly <tt>true</tt> to follow only primary parent associations
* @throws CyclicChildRelationshipException
*/
private void prependPaths(
Pair<Long, NodeRef> currentNodePair,
Pair<StoreRef, NodeRef> currentRootNodePair,
Path currentPath,
Collection<Path> completedPaths,
Stack<Long> assocIdStack,
boolean primaryOnly) throws CyclicChildRelationshipException
{
if (isDebugEnabled)
{
logger.debug("\n" +
"Prepending paths: \n" +
" Current node: " + currentNodePair + "\n" +
" Current root: " + currentRootNodePair + "\n" +
" Current path: " + currentPath);
}
Long currentNodeId = currentNodePair.getFirst();
NodeRef currentNodeRef = currentNodePair.getSecond();
// Check if we have changed root nodes
StoreRef currentStoreRef = currentNodeRef.getStoreRef();
if (currentRootNodePair == null || !currentStoreRef.equals(currentRootNodePair.getFirst()))
{
// We've changed stores
Pair<Long, NodeRef> rootNodePair = getRootNode(currentStoreRef);
currentRootNodePair = new Pair<StoreRef, NodeRef>(currentStoreRef, rootNodePair.getSecond());
}
// get the parent associations of the given node
ParentAssocsInfo parentAssocInfo = getParentAssocsCached(currentNodeId);
// does the node have parents
boolean hasParents = parentAssocInfo.getParentAssocs().size() > 0;
// does the current node have a root aspect?
// look for a root. If we only want the primary root, then ignore all but the top-level root.
if (!(primaryOnly && hasParents) && parentAssocInfo.isRoot()) // exclude primary search with parents present
{
// create a one-sided assoc ref for the root node and prepend to the stack
// this effectively spoofs the fact that the current node is not below the root
// - we put this assoc in as the first assoc in the path must be a one-sided
// reference pointing to the root node
ChildAssociationRef assocRef = new ChildAssociationRef(null, null, null, currentRootNodePair.getSecond());
// create a path to save and add the 'root' assoc
Path pathToSave = new Path();
Path.ChildAssocElement first = null;
for (Path.Element element : currentPath)
{
if (first == null)
{
first = (Path.ChildAssocElement) element;
}
else
{
pathToSave.append(element);
}
}
if (first != null)
{
// mimic an association that would appear if the current node was below the root node
// or if first beneath the root node it will make the real thing
ChildAssociationRef updateAssocRef = new ChildAssociationRef(
parentAssocInfo.isStoreRoot() ? ContentModel.ASSOC_CHILDREN : first.getRef().getTypeQName(),
currentRootNodePair.getSecond(),
first.getRef().getQName(),
first.getRef().getChildRef());
Path.Element newFirst = new Path.ChildAssocElement(updateAssocRef);
pathToSave.prepend(newFirst);
}
Path.Element element = new Path.ChildAssocElement(assocRef);
pathToSave.prepend(element);
// store the path just built
completedPaths.add(pathToSave);
}
if (!hasParents && !parentAssocInfo.isRoot())
{
// We appear to have an orphaned node. But we may just have a temporarily out of sync clustered cache or a
// transaction that started ages before the one that committed the cache content!. So double check the node
// isn't actually deleted.
if (logger.isDebugEnabled())
{
logger.debug("Stale cache detected for Node #" + currentNodeId + ": removing from cache.");
}
invalidateNodeCaches(currentNodeId);
Status currentNodeStatus = getNodeRefStatus(currentNodeRef);
if (currentNodeStatus == null || currentNodeStatus.isDeleted())
{
// Force a retry. The cached node was stale
throw new DataIntegrityViolationException("Stale cache detected for Node #" + currentNodeId);
}
// We have a corrupt repository
throw new RuntimeException("Node without parents does not have root aspect: " + currentNodeRef);
}
// walk up each parent association
for (Map.Entry<Long, ChildAssocEntity> entry : parentAssocInfo.getParentAssocs().entrySet())
{
Long assocId = entry.getKey();
ChildAssocEntity assoc = entry.getValue();
ChildAssociationRef assocRef = assoc.getRef(qnameDAO);
// do we consider only primary assocs?
if (primaryOnly && !assocRef.isPrimary())
{
continue;
}
// Ordering is meaningless here as we are constructing a path upwards
// and have no idea where the node comes in the sibling order or even
// if there are like-pathed siblings.
assocRef.setNthSibling(-1);
// build a path element
Path.Element element = new Path.ChildAssocElement(assocRef);
// create a new path that builds on the current path
Path path = new Path();
path.append(currentPath);
// prepend element
path.prepend(element);
// get parent node pair
Pair<Long, NodeRef> parentNodePair = new Pair<Long, NodeRef>(
assoc.getParentNode().getId(),
assocRef.getParentRef());
// does the association already exist in the stack
if (assocIdStack.contains(assocId))
{
// the association was present already
logger.error(
"Cyclic parent-child relationship detected: \n" +
" current node: " + currentNodeId + "\n" +
" current path: " + currentPath + "\n" +
" next assoc: " + assocId);
throw new CyclicChildRelationshipException("Node has been pasted into its own tree.", assocRef);
}
if (isDebugEnabled)
{
logger.debug("\n" +
" Prepending path parent: \n" +
" Parent node: " + parentNodePair);
}
// push the assoc stack, recurse and pop
assocIdStack.push(assocId);
prependPaths(parentNodePair, currentRootNodePair, path, completedPaths, assocIdStack, primaryOnly);
assocIdStack.pop();
}
// done
}
/**
* @return Returns a node's parent associations
*/
private ParentAssocsInfo getParentAssocsCached(Long nodeId)
{
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
Pair<NodeVersionKey, ParentAssocsInfo> cacheEntry = parentAssocsCache.getByKey(nodeVersionKey);
if (cacheEntry == null)
{
invalidateNodeCaches(nodeId);
throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
}
return cacheEntry.getSecond();
}
/**
* Update a node's parent associations.
*/
private void setParentAssocsCached(Long nodeId, ParentAssocsInfo parentAssocs)
{
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
parentAssocsCache.setValue(nodeVersionKey, parentAssocs);
}
/**
* Helper method to copy cache values from one key to another
*/
private void copyParentAssocsCached(NodeVersionKey from, NodeVersionKey to)
{
ParentAssocsInfo cacheEntry = parentAssocsCache.getValue(from);
if (cacheEntry != null)
{
parentAssocsCache.setValue(to, cacheEntry);
}
}
/**
* Callback to cache node parent assocs.
*
* @author Derek Hulley
* @since 3.4
*/
private class ParentAssocsCallbackDAO extends EntityLookupCallbackDAOAdaptor<NodeVersionKey, ParentAssocsInfo, Serializable>
{
public Pair<NodeVersionKey, ParentAssocsInfo> createValue(ParentAssocsInfo value)
{
throw new UnsupportedOperationException("Nodes are created independently.");
}
public Pair<NodeVersionKey, ParentAssocsInfo> findByKey(NodeVersionKey nodeVersionKey)
{
Long nodeId = nodeVersionKey.getNodeId();
// Find out if it is a root or store root
boolean isRoot = hasNodeAspect(nodeId, ContentModel.ASPECT_ROOT);
boolean isStoreRoot = getNodeType(nodeId).equals(ContentModel.TYPE_STOREROOT);
// Select all the parent associations
List<ChildAssocEntity> assocs = selectParentAssocs(nodeId);
// Build the cache object
ParentAssocsInfo value = new ParentAssocsInfo(isRoot, isStoreRoot, assocs);
// Now check if we are seeing the correct version of the node
if (assocs.isEmpty())
{
// No results. Currently Alfresco has very few parentless nodes (root nodes)
// and the lack of parent associations will be cached, anyway.
// But to match earlier fixes of ALF-12393, we do a double-check of the node's details
NodeEntity nodeCheckFromDb = selectNodeById(nodeId, null);
if (nodeCheckFromDb == null || !nodeCheckFromDb.getNodeVersionKey().equals(nodeVersionKey))
{
// The node is gone or has moved on in version
invalidateNodeCaches(nodeId);
throw new DataIntegrityViolationException(
"Detected stale node entry: " + nodeVersionKey +
" (now " + nodeCheckFromDb + ")");
}
}
else
{
ChildAssocEntity childAssoc = assocs.get(0);
// What is the real (at least to this txn) version of the child node?
NodeVersionKey childNodeVersionKeyFromDb = childAssoc.getChildNode().getNodeVersionKey();
if (!childNodeVersionKeyFromDb.equals(nodeVersionKey))
{
// This method was called with a stale version
invalidateNodeCaches(nodeId);
throw new DataIntegrityViolationException(
"Detected stale node entry: " + nodeVersionKey +
" (now " + childNodeVersionKeyFromDb + ")");
}
}
// Done
return new Pair<NodeVersionKey, ParentAssocsInfo>(nodeVersionKey, value);
}
}
/*
* Bulk caching
*/
@Override
public void setCheckNodeConsistency()
{
if (nodesTransactionalCache != null)
{
nodesTransactionalCache.setDisableSharedCacheReadForTransaction(true);
}
}
@Override
public void cacheNodesById(List<Long> nodeIds)
{
/*
* ALF-2712: Performance degradation from 3.1.0 to 3.1.2
* ALF-2784: Degradation of performance between 3.1.1 and 3.2x (observed in JSF)
*
* There is an obvious cost associated with querying the database to pull back nodes,
* and there is additional cost associated with putting the resultant entries into the
* caches. It is NO MORE expensive to check the cache than it is to put an entry into it
* - and probably cheaper considering cache replication - so we start checking nodes to see
* if they have entries before passing them over for batch loading.
*
* However, when running against a cold cache or doing a first-time query against some
* part of the repo, we will be checking for entries in the cache and consistently getting
* no results. To avoid unnecessary checking when the cache is PROBABLY cold, we
* examine the ratio of hits/misses at regular intervals.
*/
if (nodeIds.size() < 10)
{
// We only cache where the number of results is potentially
// a problem for the N+1 loading that might result.
return;
}
int foundCacheEntryCount = 0;
int missingCacheEntryCount = 0;
boolean forceBatch = false;
List<Long> batchLoadNodeIds = new ArrayList<Long>(nodeIds.size());
for (Long nodeId : nodeIds)
{
if (!forceBatch)
{
// Is this node in the cache?
if (nodesCache.getValue(nodeId) != null)
{
foundCacheEntryCount++; // Don't add it to the batch
continue;
}
else
{
missingCacheEntryCount++; // Fall through and add it to the batch
}
if (foundCacheEntryCount + missingCacheEntryCount % 100 == 0)
{
// We force the batch if the number of hits drops below the number of misses
forceBatch = foundCacheEntryCount < missingCacheEntryCount;
}
}
batchLoadNodeIds.add(nodeId);
}
int size = batchLoadNodeIds.size();
cacheNodesBatch(batchLoadNodeIds);
if (logger.isDebugEnabled())
{
logger.debug("Pre-loaded " + size + " nodes.");
}
}
/**
* {@inheritDoc}
* <p/>
* Loads properties, aspects, parent associations and the ID-noderef cache.
*/
public void cacheNodes(List<NodeRef> nodeRefs)
{
/*
* ALF-2712: Performance degradation from 3.1.0 to 3.1.2
* ALF-2784: Degradation of performance between 3.1.1 and 3.2x (observed in JSF)
*
* There is an obvious cost associated with querying the database to pull back nodes,
* and there is additional cost associated with putting the resultant entries into the
* caches. It is NO MORE expensive to check the cache than it is to put an entry into it
* - and probably cheaper considering cache replication - so we start checking nodes to see
* if they have entries before passing them over for batch loading.
*
* However, when running against a cold cache or doing a first-time query against some
* part of the repo, we will be checking for entries in the cache and consistently getting
* no results. To avoid unnecessary checking when the cache is PROBABLY cold, we
* examine the ratio of hits/misses at regular intervals.
*/
if (nodeRefs.size() < 10)
{
// We only cache where the number of results is potentially
// a problem for the N+1 loading that might result.
return;
}
int foundCacheEntryCount = 0;
int missingCacheEntryCount = 0;
boolean forceBatch = false;
// Group the nodes by store so that we don't *have* to eagerly join to store to get query performance
Map<StoreRef, List<String>> uuidsByStore = new HashMap<StoreRef, List<String>>(3);
for (NodeRef nodeRef : nodeRefs)
{
if (!forceBatch)
{
// Is this node in the cache?
if (nodesCache.getKey(nodeRef) != null)
{
foundCacheEntryCount++; // Don't add it to the batch
continue;
}
else
{
missingCacheEntryCount++; // Fall through and add it to the batch
}
if (foundCacheEntryCount + missingCacheEntryCount % 100 == 0)
{
// We force the batch if the number of hits drops below the number of misses
forceBatch = foundCacheEntryCount < missingCacheEntryCount;
}
}
StoreRef storeRef = nodeRef.getStoreRef();
List<String> uuids = (List<String>) uuidsByStore.get(storeRef);
if (uuids == null)
{
uuids = new ArrayList<String>(nodeRefs.size());
uuidsByStore.put(storeRef, uuids);
}
uuids.add(nodeRef.getId());
}
int size = nodeRefs.size();
nodeRefs = null;
// Now load all the nodes
for (Map.Entry<StoreRef, List<String>> entry : uuidsByStore.entrySet())
{
StoreRef storeRef = entry.getKey();
List<String> uuids = entry.getValue();
cacheNodes(storeRef, uuids);
}
if (logger.isDebugEnabled())
{
logger.debug("Pre-loaded " + size + " nodes.");
}
}
/**
* Loads the nodes into cache using batching.
*/
private void cacheNodes(StoreRef storeRef, List<String> uuids)
{
StoreEntity store = getStoreNotNull(storeRef);
Long storeId = store.getId();
int batchSize = 256;
SortedSet<String> batch = new TreeSet<String>();
for (String uuid : uuids)
{
batch.add(uuid);
if (batch.size() >= batchSize)
{
// Preload
cacheNodesNoBatch(selectNodesByUuids(storeId, batch, Boolean.FALSE));
batch.clear();
}
}
// Load any remaining nodes
if (batch.size() > 0)
{
cacheNodesNoBatch(selectNodesByUuids(storeId, batch, Boolean.FALSE));
}
}
private void cacheNodesBatch(List<Long> nodeIds)
{
int batchSize = 256;
SortedSet<Long> batch = new TreeSet<Long>();
for (Long nodeId : nodeIds)
{
batch.add(nodeId);
if (batch.size() >= batchSize)
{
// Preload
cacheNodesNoBatch(selectNodesByIds(batch, Boolean.FALSE));
batch.clear();
}
}
// Load any remaining nodes
if (batch.size() > 0)
{
cacheNodesNoBatch(selectNodesByIds(batch, Boolean.FALSE));
}
}
/**
* Bulk-fetch the nodes for a given store. All nodes passed in are fetched.
*/
private void cacheNodesNoBatch(List<Node> nodes)
{
// Get the nodes
SortedSet<Long> aspectNodeIds = new TreeSet<Long>();
SortedSet<Long> propertiesNodeIds = new TreeSet<Long>();
Map<Long, NodeVersionKey> nodeVersionKeysFromCache = new HashMap<Long, NodeVersionKey>(nodes.size()*2); // Keep for quick lookup
for (Node node : nodes)
{
Long nodeId = node.getId();
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
nodesCache.setValue(nodeId, node);
if (propertiesCache.getValue(nodeVersionKey) == null)
{
propertiesNodeIds.add(nodeId);
}
if (aspectsCache.getValue(nodeVersionKey) == null)
{
aspectNodeIds.add(nodeId);
}
nodeVersionKeysFromCache.put(nodeId, nodeVersionKey);
}
if(logger.isDebugEnabled())
{
logger.debug("Pre-loaded " + propertiesNodeIds.size() + " properties");
logger.debug("Pre-loaded " + propertiesNodeIds.size() + " aspects");
}
Map<NodeVersionKey, Set<QName>> nodeAspects = selectNodeAspects(aspectNodeIds);
for (Map.Entry<NodeVersionKey, Set<QName>> entry : nodeAspects.entrySet())
{
NodeVersionKey nodeVersionKeyFromDb = entry.getKey();
Long nodeId = nodeVersionKeyFromDb.getNodeId();
Set<QName> qnames = entry.getValue();
setNodeAspectsCached(nodeId, qnames);
aspectNodeIds.remove(nodeId);
}
// Cache the absence of aspects too!
for (Long nodeId: aspectNodeIds)
{
setNodeAspectsCached(nodeId, Collections.<QName>emptySet());
}
// First ensure all content data are pre-cached, so we don't have to load them individually when converting properties
contentDataDAO.cacheContentDataForNodes(propertiesNodeIds);
// Now bulk load the properties
Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> propsByNodeId = selectNodeProperties(propertiesNodeIds);
for (Map.Entry<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> entry : propsByNodeId.entrySet())
{
Long nodeId = entry.getKey().getNodeId();
Map<NodePropertyKey, NodePropertyValue> propertyValues = entry.getValue();
Map<QName, Serializable> props = nodePropertyHelper.convertToPublicProperties(propertyValues);
setNodePropertiesCached(nodeId, props);
}
}
/**
* {@inheritDoc}
* <p/>
* Simply clears out all the node-related caches.
*/
public void clear()
{
clearCaches();
}
/*
* Transactions
*/
public Long getMaxTxnIdByCommitTime(long maxCommitTime)
{
Transaction txn = selectLastTxnBeforeCommitTime(maxCommitTime);
return (txn == null ? null : txn.getId());
}
public int getTransactionCount()
{
return selectTransactionCount();
}
public Transaction getTxnById(Long txnId)
{
return selectTxnById(txnId);
}
public List<NodeRef.Status> getTxnChanges(Long txnId)
{
return getTxnChangesForStore(null, txnId);
}
public List<NodeRef.Status> getTxnChangesForStore(StoreRef storeRef, Long txnId)
{
Long storeId = (storeRef == null) ? null : getStoreNotNull(storeRef).getId();
List<NodeEntity> nodes = selectTxnChanges(txnId, storeId);
// Convert
List<NodeRef.Status> nodeStatuses = new ArrayList<NodeRef.Status>(nodes.size());
for (NodeEntity node : nodes)
{
nodeStatuses.add(node.getNodeStatus());
}
// Done
return nodeStatuses;
}
public int getTxnUpdateCount(Long txnId)
{
return selectTxnNodeChangeCount(txnId, Boolean.TRUE);
}
public int getTxnDeleteCount(Long txnId)
{
return selectTxnNodeChangeCount(txnId, Boolean.FALSE);
}
public List<Transaction> getTxnsByCommitTimeAscending(
Long fromTimeInclusive,
Long toTimeExclusive,
int count,
List<Long> excludeTxnIds,
boolean remoteOnly)
{
// Pass the current server ID if it is to be excluded
Long serverId = remoteOnly ? serverId = getServerId() : null;
return selectTxns(fromTimeInclusive, toTimeExclusive, count, null, excludeTxnIds, serverId, Boolean.TRUE);
}
public List<Transaction> getTxnsByCommitTimeDescending(
Long fromTimeInclusive,
Long toTimeExclusive,
int count,
List<Long> excludeTxnIds,
boolean remoteOnly)
{
// Pass the current server ID if it is to be excluded
Long serverId = remoteOnly ? serverId = getServerId() : null;
return selectTxns(fromTimeInclusive, toTimeExclusive, count, null, excludeTxnIds, serverId, Boolean.FALSE);
}
public List<Transaction> getTxnsByCommitTimeAscending(List<Long> includeTxnIds)
{
return selectTxns(null, null, null, includeTxnIds, null, null, Boolean.TRUE);
}
public List<Long> getTxnsUnused(Long minTxnId, long maxCommitTime, int count)
{
return selectTxnsUnused(minTxnId, maxCommitTime, count);
}
public void purgeTxn(Long txnId)
{
deleteTransaction(txnId);
}
public static final Long LONG_ZERO = 0L;
public Long getMinTxnCommitTime()
{
Long time = selectMinTxnCommitTime();
return (time == null ? LONG_ZERO : time);
}
public Long getMaxTxnCommitTime()
{
Long time = selectMaxTxnCommitTime();
return (time == null ? LONG_ZERO : time);
}
/*
* Abstract methods for underlying CRUD
*/
protected abstract ServerEntity selectServer(String ipAddress);
protected abstract Long insertServer(String ipAddress);
protected abstract Long insertTransaction(Long serverId, String changeTxnId, Long commit_time_ms);
protected abstract int updateTransaction(Long txnId, Long commit_time_ms);
protected abstract int deleteTransaction(Long txnId);
protected abstract List<StoreEntity> selectAllStores();
protected abstract NodeEntity selectStoreRootNode(Long storeId);
protected abstract NodeEntity selectStoreRootNode(StoreRef storeRef);
protected abstract Long insertStore(StoreEntity store);
protected abstract int updateStoreRoot(StoreEntity store);
protected abstract int updateStore(StoreEntity store);
protected abstract Long insertNode(NodeEntity node);
protected abstract int updateNode(NodeUpdateEntity nodeUpdate);
protected abstract int updateNodes(Long txnId, List<Long> nodeIds);
protected abstract void updatePrimaryChildrenSharedAclId(
Long txnId,
Long primaryParentNodeId,
Long optionalOldSharedAlcIdInAdditionToNull,
Long newSharedAlcId);
protected abstract int deleteNodeById(Long nodeId, boolean deletedOnly);
protected abstract int deleteNodesByCommitTime(boolean deletedOnly, long maxTxnCommitTimeMs);
protected abstract NodeEntity selectNodeById(Long id, Boolean deleted);
protected abstract NodeEntity selectNodeByNodeRef(NodeRef nodeRef, Boolean deleted);
protected abstract List<Node> selectNodesByUuids(Long storeId, SortedSet<String> uuids, Boolean deleted);
protected abstract List<Node> selectNodesByIds(SortedSet<Long> ids, Boolean deleted);
protected abstract Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Set<Long> nodeIds);
protected abstract Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Long nodeId);
protected abstract Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Long nodeId, Set<Long> qnameIds);
protected abstract int deleteNodeProperties(Long nodeId, Set<Long> qnameIds);
protected abstract int deleteNodeProperties(Long nodeId, List<NodePropertyKey> propKeys);
protected abstract void insertNodeProperties(Long nodeId, Map<NodePropertyKey, NodePropertyValue> persistableProps);
protected abstract Map<NodeVersionKey, Set<QName>> selectNodeAspects(Set<Long> nodeIds);
protected abstract void insertNodeAspect(Long nodeId, Long qnameId);
protected abstract int deleteNodeAspects(Long nodeId, Set<Long> qnameIds);
protected abstract void selectNodesWithAspects(
List<Long> qnameIds,
Long minNodeId, Long maxNodeId,
NodeRefQueryCallback resultsCallback);
protected abstract Long insertNodeAssoc(Long sourceNodeId, Long targetNodeId, Long assocTypeQNameId, int assocIndex);
protected abstract int updateNodeAssoc(Long id, int assocIndex);
protected abstract int deleteNodeAssoc(Long sourceNodeId, Long targetNodeId, Long assocTypeQNameId);
protected abstract int deleteNodeAssocsToAndFrom(Long nodeId);
protected abstract int deleteNodeAssocsToAndFrom(Long nodeId, Set<Long> assocTypeQNameIds);
protected abstract int deleteNodeAssocs(List<Long> ids);
protected abstract List<NodeAssocEntity> selectNodeAssocsBySource(Long sourceNodeId, Long typeQNameId);
protected abstract List<NodeAssocEntity> selectNodeAssocsByTarget(Long targetNodeId, Long typeQNameId);
protected abstract NodeAssocEntity selectNodeAssocById(Long assocId);
protected abstract int selectNodeAssocMaxIndex(Long sourceNodeId, Long assocTypeQNameId);
protected abstract Long insertChildAssoc(ChildAssocEntity assoc);
protected abstract int deleteChildAssocById(Long assocId);
protected abstract int updateChildAssocIndex(
Long parentNodeId,
Long childNodeId,
QName assocTypeQName,
QName assocQName,
int index);
protected abstract int updateChildAssocsUniqueName(Long childNodeId, String name);
protected abstract int deleteChildAssocsToAndFrom(Long nodeId);
protected abstract ChildAssocEntity selectChildAssoc(Long assocId);
protected abstract List<ChildAssocEntity> selectChildNodeIds(
Long nodeId,
Boolean isPrimary,
Long minAssocIdInclusive,
int maxResults);
protected abstract List<NodeIdAndAclId> selectPrimaryChildAcls(Long nodeId);
protected abstract List<ChildAssocEntity> selectChildAssoc(
Long parentNodeId,
Long childNodeId,
QName assocTypeQName,
QName assocQName);
/**
* Parameters are all optional except the parent node ID and the callback
*/
protected abstract void selectChildAssocs(
Long parentNodeId,
Long childNodeId,
QName assocTypeQName,
QName assocQName,
Boolean isPrimary,
Boolean sameStore,
ChildAssocRefQueryCallback resultsCallback);
protected abstract void selectChildAssocs(
Long parentNodeId,
QName assocTypeQName,
QName assocQName,
int maxResults,
ChildAssocRefQueryCallback resultsCallback);
protected abstract void selectChildAssocs(
Long parentNodeId,
Set<QName> assocTypeQNames,
ChildAssocRefQueryCallback resultsCallback);
protected abstract ChildAssocEntity selectChildAssoc(
Long parentNodeId,
QName assocTypeQName,
String childName);
protected abstract void selectChildAssocs(
Long parentNodeId,
QName assocTypeQName,
Collection<String> childNames,
ChildAssocRefQueryCallback resultsCallback);
protected abstract void selectChildAssocsByPropertyValue(
Long parentNodeId,
QName propertyQName,
NodePropertyValue nodeValue,
ChildAssocRefQueryCallback resultsCallback);
protected abstract void selectChildAssocsByChildTypes(
Long parentNodeId,
Set<QName> childNodeTypeQNames,
ChildAssocRefQueryCallback resultsCallback);
protected abstract void selectChildAssocsWithoutParentAssocsOfType(
Long parentNodeId,
QName assocTypeQName,
ChildAssocRefQueryCallback resultsCallback);
/**
* Parameters are all optional except the parent node ID and the callback
*/
protected abstract void selectParentAssocs(
Long childNodeId,
QName assocTypeQName,
QName assocQName,
Boolean isPrimary,
ChildAssocRefQueryCallback resultsCallback);
protected abstract List<ChildAssocEntity> selectParentAssocs(Long childNodeId);
/**
* No DB constraint, so multiple returned
*/
protected abstract List<ChildAssocEntity> selectPrimaryParentAssocs(Long childNodeId);
protected abstract int updatePrimaryParentAssocs(
Long childNodeId,
Long parentNodeId,
QName assocTypeQName,
QName assocQName,
String childNodeName);
/**
* Moves all node-linked data from one node to another. The source node will be left
* in an orphaned state and without any attached data other than the current transaction.
*
* @param fromNodeId the source node
* @param toNodeId the target node
*/
protected abstract void moveNodeData(Long fromNodeId, Long toNodeId);
protected abstract void deleteSubscriptions(Long nodeId);
protected abstract Transaction selectLastTxnBeforeCommitTime(Long maxCommitTime);
protected abstract int selectTransactionCount();
protected abstract Transaction selectTxnById(Long txnId);
protected abstract List<NodeEntity> selectTxnChanges(Long txnId, Long storeId);
/**
* @param txnId the transaction ID (never <tt>null</tt>)
* @param updates <tt>TRUE</tt> to select node updates, <tt>FALSE</tt> to select
* node deletions or <tt>null</tt> to select all changes.
* @return Returns the number of nodes affected by the transaction
*/
protected abstract int selectTxnNodeChangeCount(Long txnId, Boolean updates);
protected abstract List<Transaction> selectTxns(
Long fromTimeInclusive,
Long toTimeExclusive,
Integer count,
List<Long> includeTxnIds,
List<Long> excludeTxnIds,
Long excludeServerId,
Boolean ascending);
protected abstract List<Long> selectTxnsUnused(Long minTxnId, Long maxCommitTime, Integer count);
protected abstract Long selectMinTxnCommitTime();
protected abstract Long selectMaxTxnCommitTime();
}