mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-06-30 18:15:39 +00:00
48055: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46833: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 30799: THOR-172: Switch Tenant via public API 46836: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 30853: Initial implementation of THOR-209. Webscript to get invitation/invitee status. 30855: More on THOR-209. Added siteTenantTitle to the webscript response. 30858: Apply generated cloud license 30859: Miscellaneous tidy-ups and refactorings, additional documentation and some webscript JSON additions. All as part of THOR-209. 30860: Miscellaneous doc improvements around the MT/Activiti workarounds. 30861: Removing unnecessary TenantUtil.runas in test code. 30863: THOR-204. Dev mode option to send invite/sign-up emails to spring-injected address. 30865: Temporarily disable subscriptions (followers) - pending ALF-9957 30866: THOR-175: Set and enforce file space quota for tenant 30868: Deleted obsolete/empty dir 30869: THOR-210: disable jobs that are not used/required (eg. AVM orphan reaper) 30870: THOR-209. Have fixed up issue with getting properties from completed workflow instances. Changed invitation to use pathInstanceId instead of taskId as the 'id' for these workflows. Now consistent with signup. Commented in the test that calls invitee-status.get 30871: THOR-209. Adding the inviteeIsActivated value to the webscript response. 30872: THOR-204. When emails are sent to the dev-only, spring-injected email address, the subject is now prefixed with the orig 30879: THOR-209. Making sure inviteeIsActivated is present for both in-flight and completed workflows. 30883: Resolve THOR-212 30895: THOR-172: Switch Tenant via public API 30896: THOR-209. Renaming some files so that they refer to invitation status rather than invitee status. Also added some documentation to make this dicstinction clearer. This is not a general purpose script to get the status of an invitee to a site. It is only for checking if a particular invitation workflow is complete and then getting some additional state data. 30897: THOR-175: Set and enforce file space quota for tenant 30900: Changing invitation-status webscript to auth=none; runas=Admin to support invitation flow of exteernal users. Part of THOR-209. 46845: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 30967: Invite & signup improvemengts 30969: Share Activities 30976: Remove unreliable hosts from isReachableDomain test 48066: Merged DEV/CONV_V413 to DEV/CONV_HEAD (RECORD ONLY) 46857: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35731: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31149: Initial Commit of Analytics Service 31150: Second draft of Analytics Service 31151: First cut of forms runtime supporting balloons on "blur" event as requested 31163: FORMS RUNTIME CHECKPOINT - before making the yellow mandatory only being displayed "on load and until focused" 31168: Refactored Analytics Service to be static 31170: Forms runtime as agreed in meeting 48067: Merged DEV/CONV_V413 to DEV/CONV_HEAD (RECORD ONLY) 46861: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35752: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31220: THOR-49. Implementation of Reset Forgotten Password workflow. 31227: (RECORD ONLY) Fix merge error 31237: Add email validation to registration and invite services: 31239: THOR-219: Merge fix (re-disable Repo<->SOLR ssl config) 48069: Merged DEV/CONV_V413 to DEV/CONV_HEAD (RECORD ONLY) 46864: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35754: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31240: MultiSelectAutoComplete now has configurable validation (& tooltips) 31241: Tenant site count usage/quota - exposed via Account API 31250: Made events enumerations 31251: Forgot to add AnalyticsEvent class to previous commit 31271: Attempt at fixing test dependencies and remove intermittent test 48070: Merged DEV/CONV_V413 to DEV/CONV_HEAD (NOTE! Added TenantXxxx classes and change in FormUIGet Will be removed in later revisions) 46911: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35757: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31367: Merged BRANCHES/DEV/THOR1SURF to BRANCHES/DEV/THOR1: 30971: (RECORD ONLY) Creating SURF update branch for THOR1 30979: Commit initial Surf lib changes, Cloud classes and config overrides 30980: Add custom Cloud Surf authenticator, connector, remote store, user and user factory 31015: Renamed classes and references from Cloud to Tenant, custom page view, page view resolvers, URLModel + factory, URLHelper + factory 31076: Successful signup and page redirection 31091: Correct redirects from <application context> and <application context>/<tenant name> URLs 31098: Updated Surf libs 31132: Tenant specific implementation of PathStoreObjectPersister - Surf modelobject cache is now partitioned by the tenant name. 31133: Updated Surf libs and JavaDoc updates 31155: Initial code to handle attempted access to unauthorised tenants, secondary tenants added to TenantUser and page/activations filter rule 31210: Fixed 401 & 409 errors on remote GET/POST calls. Logout redirection support. 31229: Signup and invitiation completion updates 31242: Fixed up invitation, signup and tenant switching problems 31270: Fixed forms issue (can now create folders in doc lib) 31277: THOR-207. Invitation workflows now run in the inviter's tenant rather than the default tenant. This is checked in on a side-branch because the invitation email's accept/reject links include the tenantId and this tenant-aware Share URL is not yet supported on the THOR1 branch. I removed various TenantUtil.runAsWork calls which were causing the workflow to run on the default tenant rather then the current tenant. SendCloudInvitationEmailDelegate.createInvitationUrl now includes the tenantId in the Share URL it generates. Added new test cases at the Java API level. (Was formerly just at REST API level). 31286: (RECORD ONLY) Reset solrcore.properties files 31297: Fixed FlashUpload problem 31298: Fixed application context only login 31302: Fixed no user profile image url issue 31306: Updates to TenantUserFactory to defensively handle missing tenant data 31326: Repo switch tenant fixes: 31356: Resolve switch tenant niggles, with assistance from Erik: 48072: Merged DEV/CONV_V413 to DEV/CONV_HEAD 46934: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 46930: Adding extension point to forms runtime's FormUIGet for modifying submission url 46937: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35762: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31408: Latest Spring Surf Libs (including SubComponentEvaluator param tokenization fix for THOR) 31409: Tenant usage/quota -> person count 31412: RM module cleanup & almost finsihed THOR-287 & THOR-288 31434: Fix Thor Share eclipse project 48073: CONV: Fix slingshot eclipse .classpath (add freemarker dep) 48074: Merged DEV/CONV_V413 to DEV/CONV_HEAD 46940: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 35766: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31450: Additional DB query support in Repo/Core 31451: Tenant people count (internal + total) usage/quota 31453: Fix issue uploading small files which resulted in zero byte content 31456: Fix paging and total count (1000+) when listing accounts 31457: THOR-312. Addition of NETWORK_ADMINS group authority. 31461: THOR-314. I've overridden people.get with a cloud-specific template. This adds an isExternal JSON field to each person object. 31463: THOR-316 webscript filter on people.get for isInternal, isExternal. Overridden people.get.desc.xml and .js in the cloud module in order to add query param and add additional filtering. 31464: Base work for THOR-178 "F5: Existing user has forgotten password and needs to reset it" 31468: THOR-321 Create cloud:networkAdmin aspect. 31469: THOR-315 Return isNetworkAdmin in overridden people.get webscript. 31470: THOR-275: Add simple caching (for PropertyUniqueContext - used via AttributeService) 31471: THOR-318 people.get has new cloud query parameter 'networkAdmin' 31477: THOR-275: temp build fix 31479: THOR-324 Demote user from admin. New method on RegistrationService to demote a user from NetworkAdmin and tests. 31484: THOR-319. Fixing maxResults on people.get when internal/external/admin filtering is applied. This issue is not really resolved, but I've commented the code to illuminate the issue. 31485: THOR-275: fix build/test 31486: Working forgot password for THOR-178 (problem accessing the reset-password email link though) 31488: THOR-184: Disable user usages 31495: F156: Allow super system admin to login to any tenant 31496: Fix for personExists since hiding admin 31500: THOR-178 31501: THOR-329 Add a get-reset-password status webscript. 31503: Finished forgot password flow THOR-178 31507: THOR-328: add fixed adjustment (for people usage) 31508: Back out some of the hidden admin changes 31509: Revert mistaken check 31510: THOR-326 Changes to DAO layer to allow update of account type. 31513: Cloud Console updates 31514: Switch Network now uses tenants from the user object (instead of making a remote call) 31515: THOR-326 Changes to the REST & Service layer to allow update of account type. 48075: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (effectively record-only - no changes) 46849: Merged PATCHES/V4.1.3 to DEV/CONV_V413 46779: ALF-17967: Error in org.alfresco.repo.workflow.WorkflowServiceImpl.getPooledTasks on StartUp. - Improved fix that uses the bridge table cache if it is available - Groups queried for pooled tasks still limited to 100 by default but can be configured with system.workflow.maxAuthoritiesForPooledTasks - Overall number of results can be cut off with system.workflow.maxPooledTasks 48076: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46855: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 35706: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31049: THOR-175: set and enforce per-tenant quota 31053: THOR-204: Add dev email mode option 48077: Merged DEV/CONV_V413 to DEV/CONV_HEAD (RECORD ONLY) 46944: Merge fallout - fix compile error. 48078: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (already fixed - no changes) 46858: Fix compile error 48079: Merged DEV/CONV_V413 to DEV/CONV_HEAD 46953: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35767: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31516: Hidden admin - attempt 2 31517: THOR-326. Update to REST-cient rcq file following 31515, which allows for account upgrade. 31518: THOR-326. DaveC asked me to move the paid business account type out of test config and into product config. 31519: After tenant switch the client side resources are more sensitive (new requires /res) which it didn't before. This solves the webpreview bug and some other minor stuff. 31520: THOR-175: Set and enforce file space quota for tenant 31522: THOR-330. Return Account Class data in Account REST API. 31523: THOR-330. Added new rsp data into desc.xml sample response. 31524: THOR-322: refactor tenant file usage/quota 31525: Skip activity post lookups that have exceptions 31526: Root webdav to st:sites for now (as per current beta.alfresco.com) 31528: THOR-323 & THOR-324 Promotion and demotion of users to/from NetworkAdmin. 31534: Account Summary now also displays name & summary 31535: Fix for THOR-320. Alfresco logo image in the various Cloud emails is broken. 31538: Account Summary now handles -2 & MultiSelectAutoComplete doesn't bounce when selecting first item 31540: Implementation of THOR-335 webscript for account-types.get 31541: Account Summary now displays date correctly 31542: Some paths to client side resources that were missing "/res" in the path 31547: Various label changes according to Kathryns docs & some new login/forgot password links in invite/signup forms 31555: Refactored Analytics Service to send JSON Analytics properties 31557: Some changes to cloud email templates following feedback from Kathryn, Erik. 31558: THOR-322: refactor tenant file usage/quota 31559: Turned 'sign up' email URLs into links rather than text. Yes, we'll make these buttons at some point but I just want them to be clickable for now. 48080: Merged DEV/CONV_V413 to DEV/CONV_HEAD (UI ONLY) 46954: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35771: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31563: THOR-123: modules are no longer started for each tenant 31566: Update account class display names: 31567: THOR-123: temp' put back "applyToTenants=true" ... 31575: THOR-123: pre-req 31579: Fix issue where setting a preference meant that the person could no longer login: 31581: Addition of isNetworkAdmin, accountClassName and accountClassDisplayName to the metadata.get webscript, as required by Erik. 31582: Re-enabling RenditionServiceIntegrationTest which was failing. See THOR-106. 31584: THOR-123: pre-req 31585: THOR-347: disable test (pending this JIRA) - ChainingUserRegistrySynchronizerTest 31590: Account changes 31591: Upgrade accound button is now a mailto link pointing to sales@alfresco.com 31592: Added isNetworkAdmin 31593: Made sure tooltips are hidden when a dialog/overly is showed/hidden 31594: Disabling test again pending proper fix. THOR-106 31595: Reduce log level 31600: THOR-123: mark modules with "applyToTenants=false" 31601: Tooltips now dissapear when panel/overlays are destroyed (not only hidden) 48081: Merged DEV/CONV_V413 to DEV/CONV_HEAD 46955: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35779: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31605: (RECORD ONLY) THOR-336. Fixing /res/themes URLs in activity emails. Fix /res/themes URL in newly located activity emails. 48088: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46874: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 35709: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31054: Fix for email templates (getDirectReadableChannel -> File does not exist) 46875: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 35711: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31055: Re-enable activity feed notifications and subscriptions (followers) 48094: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46894: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 35759: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31386: Added some tenancy-awareness to ActionService. 31388: Addition of accountTypeId to metadata.get webscript. 31391: Fix for unreported issue that arises from the invitation workflow having moved from the system to the inviter tenant. 31392: Build fixes: Add pseudo-support for tenant switching in web script test f/w 31393: The final fix for the 'external user invites other external user' scenario. 31398: Tenant usage/quota - site count 31405: Build fix for failing ActionService tests. Compensating actions were not running on the correct tenant. 31407: Resolve THOR-248: Extensions is not deployed as part of the build 48095: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (repo pre-merge) 46911: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35757: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31367: Merged BRANCHES/DEV/THOR1SURF to BRANCHES/DEV/THOR1: 30971: (RECORD ONLY) Creating SURF update branch for THOR1 30979: Commit initial Surf lib changes, Cloud classes and config overrides 30980: Add custom Cloud Surf authenticator, connector, remote store, user and user factory 31015: Renamed classes and references from Cloud to Tenant, custom page view, page view resolvers, URLModel + factory, URLHelper + factory 31076: Successful signup and page redirection 31091: Correct redirects from <application context> and <application context>/<tenant name> URLs 31098: Updated Surf libs 31132: Tenant specific implementation of PathStoreObjectPersister - Surf modelobject cache is now partitioned by the tenant name. 31133: Updated Surf libs and JavaDoc updates 31155: Initial code to handle attempted access to unauthorised tenants, secondary tenants added to TenantUser and page/activations filter rule 31210: Fixed 401 & 409 errors on remote GET/POST calls. Logout redirection support. 31229: Signup and invitiation completion updates 31242: Fixed up invitation, signup and tenant switching problems 31270: Fixed forms issue (can now create folders in doc lib) 31277: THOR-207. Invitation workflows now run in the inviter's tenant rather than the default tenant. This is checked in on a side-branch because the invitation email's accept/reject links include the tenantId and this tenant-aware Share URL is not yet supported on the THOR1 branch. I removed various TenantUtil.runAsWork calls which were causing the workflow to run on the default tenant rather then the current tenant. SendCloudInvitationEmailDelegate.createInvitationUrl now includes the tenantId in the Share URL it generates. Added new test cases at the Java API level. (Was formerly just at REST API level). 31286: (RECORD ONLY) Reset solrcore.properties files 31297: Fixed FlashUpload problem 31298: Fixed application context only login 31302: Fixed no user profile image url issue 31306: Updates to TenantUserFactory to defensively handle missing tenant data 31326: Repo switch tenant fixes: 31356: Resolve switch tenant niggles, with assistance from Erik: 48109: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46917: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts (not mergeinfo/slingshot/web-framework-commons) 35766: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 46918: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts (not mergeinfo/slingshot) 35767: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 46919: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 35768: Fix compile issue from merge 46921: Merge fallout - fix compile error. 46949: Test fallout 47126: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35954: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35960: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35961: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35962: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35963: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35964: Spring Surf library refresh 35995: Fix merge issue 35999: Fix merge issue 47144: Fix merge/test failures (WCMTestSuite) 47539: CLOUD-1375 - fix WCM unit test fallout: SandboxServiceImplTest.testDeleteSandbox + WebProjectServiceImplTest.testDeleteWebProject 48111: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46954: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35771: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31563: THOR-123: modules are no longer started for each tenant 31566: Update account class display names: 31567: THOR-123: temp' put back "applyToTenants=true" ... 31575: THOR-123: pre-req 31579: Fix issue where setting a preference meant that the person could no longer login: 31581: Addition of isNetworkAdmin, accountClassName and accountClassDisplayName to the metadata.get webscript, as required by Erik. 31582: Re-enabling RenditionServiceIntegrationTest which was failing. See THOR-106. 31584: THOR-123: pre-req 31585: THOR-347: disable test (pending this JIRA) - ChainingUserRegistrySynchronizerTest 31590: Account changes 31591: Upgrade accound button is now a mailto link pointing to sales@alfresco.com 31592: Added isNetworkAdmin 31593: Made sure tooltips are hidden when a dialog/overly is showed/hidden 31594: Disabling test again pending proper fix. THOR-106 31595: Reduce log level 31600: THOR-123: mark modules with "applyToTenants=false" 31601: Tooltips now dissapear when panel/overlays are destroyed (not only hidden) 46956: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35782: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31607: "Hide everything but the doclib" customizations - Dashlets adjustments * Addons RSS feed - hidden * Site Calendar - hidden * Content I'm editing - added <@markup> extension points so blog, wiki & forum sections are hidden by cloud extension module * Site Data List - hidden * Site Links - hidden * Wiki - hidden * User Calendar - hidden - URL rewrites * Forgot & reset password urls now prettyfied, not using "-default-/" - Duplicated slingshot presets to avoid future slingshot changes popping up in the cloud 31611: MT: fix ability to delete a disabled tenant 31612: THOR-339: Disable/enable logins for a network (account update) 31621: THOR-106. Taking a failing test class out again, pending fix. Hmmmm. 31623: THOR-357 - support shared CMIS dictionary 48112: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (no changes) 46957: Test fallout 48113: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (no changes - already pre-merged) 46911: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35757: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31367: Merged BRANCHES/DEV/THOR1SURF to BRANCHES/DEV/THOR1: 30971: (RECORD ONLY) Creating SURF update branch for THOR1 30979: Commit initial Surf lib changes, Cloud classes and config overrides 30980: Add custom Cloud Surf authenticator, connector, remote store, user and user factory 31015: Renamed classes and references from Cloud to Tenant, custom page view, page view resolvers, URLModel + factory, URLHelper + factory 31076: Successful signup and page redirection 31091: Correct redirects from <application context> and <application context>/<tenant name> URLs 31098: Updated Surf libs 31132: Tenant specific implementation of PathStoreObjectPersister - Surf modelobject cache is now partitioned by the tenant name. 31133: Updated Surf libs and JavaDoc updates 31155: Initial code to handle attempted access to unauthorised tenants, secondary tenants added to TenantUser and page/activations filter rule 31210: Fixed 401 & 409 errors on remote GET/POST calls. Logout redirection support. 31229: Signup and invitiation completion updates 31242: Fixed up invitation, signup and tenant switching problems 31270: Fixed forms issue (can now create folders in doc lib) 31277: THOR-207. Invitation workflows now run in the inviter's tenant rather than the default tenant. This is checked in on a side-branch because the invitation email's accept/reject links include the tenantId and this tenant-aware Share URL is not yet supported on the THOR1 branch. I removed various TenantUtil.runAsWork calls which were causing the workflow to run on the default tenant rather then the current tenant. SendCloudInvitationEmailDelegate.createInvitationUrl now includes the tenantId in the Share URL it generates. Added new test cases at the Java API level. (Was formerly just at REST API level). 31286: (RECORD ONLY) Reset solrcore.properties files 31297: Fixed FlashUpload problem 31298: Fixed application context only login 31302: Fixed no user profile image url issue 31306: Updates to TenantUserFactory to defensively handle missing tenant data 31326: Repo switch tenant fixes: 31356: Resolve switch tenant niggles, with assistance from Erik: 48114: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46962: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 40147: (RECORD ONLY) French: Cloud Translation update from Gloria (based on EN rev38372) 42709: (RECORD ONLY) FRENCH: Translation updates based on EN r42416 42871: (RECORD ONLY) GERMAN: Cloud Translation, based on r 42416 42879: (RECORD ONLY) SPANISH: Cloud Translation, based on r 42416 42890: (RECORD ONLY) ITALIAN: Cloud Translation, based on r 42416 43879: (RECORD ONLY) FRENCH: Translation updates based on EN r43703 43983: (RECORD ONLY) GERMAN: Translation updates based on EN r43703 43984: (RECORD ONLY) SPANISH: Translation updates based on EN r43703 43985: (RECORD ONLY) FRENCH: Translation updates based on EN r43703, includes file missing from previous commit. 43986: (RECORD ONLY) ITALIAN: Translation updates based on EN r43703. 43987: (RECORD ONLY) JAPANESE: Translation updates based on EN r43703. 44031: (RECORD ONLY) JAPANESE: Translation updates based on EN r43703. Corrects file missed from previous commit. 44032: (RECORD ONLY) GERMAN: Translation updates based on EN r43703. Corrects missing line break. 45329: (RECORD ONLY) FRENCH: Cloud translation updates based on EN r45266 45330: (RECORD ONLY) GERMAN: Cloud translation updates based on EN r45266 45332: (RECORD ONLY) SPANISH: Cloud translation updates based on EN r45266 45333: (RECORD ONLY) JAPANESE: Cloud translation updates based on EN r45266 45427: (RECORD ONLY) SPANISH: Cloud 1 translation updates based on EN r45266 45718: (RECORD ONLY) ITALIAN: Translation updates based on EN r45266 (missed from previous bundle import) 45838: (RECORD ONLY) FRENCH: Cloud Translation update based on EN r45266 45966: (RECORD ONLY) Translation update to fix CLOUD-1270 in FR and ES 46365: (RECORD ONLY) ALL LANG: Translation updates based on EN r46289 46366: (RECORD ONLY) ALL LANG: Updates copyright year to 2013 46377: (RECORD ONLY) ALL LANG: Adds strings missing from previous commit. 47192: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: (record-only - WebDAV mostly resolved as part of 36117 merge) 36408: (RECORD ONLY) Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 36404: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/THOR1: 36060: THOR-1373: Proxied WebDAV must generate correct URLs when URL-rewriting is used. 36083: THOR-1373: Proxied WebDAV must generate correct URLs when URL-rewriting is used. 47369: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 41180: (RECORD ONLY) Merged BRANCHES/DEV/FEATURES/CLOUD1_CLOUDSYNC to BRANCHES/DEV/CLOUD1: 40482: ALF-13998: 'No items' error is highlighted in red, even that is not sever error. - ALF-15453: Incorrect manage permissions working for a file/folder Merged BRANCHES/DEV/FEATURES/CLOUD1_CLOUDSYNC to BRANCHES/DEV/CLOUD1: 40486: ALF-15453: Incorrect manage permissions working for a file/folder 47377: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 41048: (RECORD ONLY) Merged DEV/V4.1-BUG-FIX to DEV/CLOUD1 40382: Fix for ALF-15491 SOLR is generating queries for lucene style cross-language support 40632: Fix for ALF-15487 Search not working for queries containing 3-digit versions Fix for ALF-15356 SOLR doesn't support searching by cm:name of file with underscore and dots 40662: Eclipse classpath fixes 41032: Fix for ALF-15753 Infinite loop during Solr ACL indexing when ACL Changeset batch is empty 47393: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged DEV/CLOUD1-BUG-FIX into DEV/CLOUD1: 41674: ALF-15967: Using START_USER_ID_ instead of "initiator" property to query process instances started by user X to prevent extra joins + removed unused constants 41650: Fixed CLOUD-667: Merged fix for ALF-14438 into CLOUD1-BUG-FIX + using START_USER_ID_ instead of custom "initiator" property to query initiator to boost performance even more 47412: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 42252: (RECORD ONLY) Merged BRANCHES/DEV/V4.1-BUG-FIX to BRANCHES/DEV/CLOUD1 42233: Fix for ALF-16164 Cloud monitoring of SOLR is CPU intensive due to its repeated use of the SOLR stats page and related CLOUD-760 Cloud monitoring of SOLR is CPU intensive due to its repeated use of the SOLR stats page 47429: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 42200: Merged DEV/CLOUD1-BUG-FIX into DEV/CLOUD1: Record-only (r41650 and r41674) 47433: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: CLOUD-808: Fix for timer deploying MT-process when shared is required caused test to fail 47435: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/CLOUD1_CORS to BRANCHES/DEV/CLOUD1: 43100: Update the salesforce amp to include the CORS Filter 43101: Update web.xml to enable to the CORS Filter with filter-mapping 43117: Add updated amp with removed CORS Filter. CORS Filter is now available in 3rd-party libs 43118: [CLOUD-724] Add CORS Filter jar to 3rd-party libs 43119: [CLOUD-724] Add missing jar java-property-utils-1.6.jar to 3rd-party libs 47485: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 44203: (RECORD ONLY) Merged BRANCHES/V4.1 to BRANCHES/DEV/CLOUD1 44200: Probable fix for ALF-16895 SOLR: Cannot find files after restart and reindex solr 44276: (RECORD ONLY) Merged BRANCHES/V4.1 to BRANCHES/DEV/CLOUD1 44275: Part 2 for ALF-16895 SOLR: Cannot find files after restart and reindex solr - fix initial cache state to cope with duplicate leaf/aux doc entries. 44314: (RECORD ONLY) Merged BRANCHES/V4.1 to BRANCHES/DEV/CLOUD1 44312: Part 3 for ALF-16895 SOLR: Cannot find files after restart and reindex solr - fix incremental cache state to cope with duplicate leaf/aux doc entries. 47523: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 44573: (RECORD ONLY) Merged BRANCHES/DEV/CLOUD1_SP to BRANCHES/DEV/CLOUD1: 44572: Clean up of unused files. 44576: (RECORD ONLY) Merged BRANCHES/DEV/CLOUD1_CORS to BRANCHES/DEV/CLOUD1: 44518: [CLOUD-955] Change CORS filter-mapping to use servelet instead of url 44691: (RECORD ONLY) Merged BRANCHES/DEV/CLOUD1_CORS to BRANCHES/DEV/CLOUD1: 44688: (RECORD ONLY) Rebase CLOUD1_CORS with CLOUD1 44689: [CLOUD-1072] Add public api url to CORS filter mapping. Move CORS filter mapping to live above the publicapi filter mappings. OPTIONS calls made to the CORS filter should be evaluated before Layer7 authentication. 47548: Merged DEV/CLOUD2 to DEV/CONV_V413 46931: Overriding form runtime's submissionUrl using extesnion point in FormUIGet 46984: Overriding entire sent-invites.js (instead of modifying the core slingshot code) with a copy of the core code modified to work with the cloud invite apis. 46986: Overriding help pages config in cloud-config.xml (rather than modifying the core files!) 47553: Merged DEV/CLOUD2 to DEV/CONV_V413 47421: Overriding entire sent-invites.get ftl & properties (instead of modifying the core slingshot code) with a copy of the core code modified to fit the cloud requirements. 47442: Add web overlay for share + tune embedded librairies 47455: Add dependency on jetty-webapp to compile the tests 48115: CONV: Fix cache defs (propertyUniqueContextCache & siteNodeRefCache) 48117: Merged DEV/CONV_V413 to DEV/CONV_HEAD 46959: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35790: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31624: Resolve THOR-302: transformation-client-1.0.0-SNAPSHOT.jar not in alfresco/WEB-INF/lib: 31632: More reliable test, hopefully 31644: Fix unreported issue in aws-context.xml.sample (not well-formed XML) 31645: Customized invite links to use "cloud dialog" instead of "invite page" for the following components: 31662: New Analytics events and tests 31663: New Analytics events and tests 31678: Addition of isExternal data to site membership webscripts. 48118: Merged DEV/CONV_V413 to DEV/CONV_HEAD 46960: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35791: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31684: THOR-367 - #1 Within Site - Remove all page components other than site dashboard, document library and members - Remove customize site 31685: New lightweight webscript to retrieve user/network metadata about the currently authenticated user in the current tenant. 31693: THOR-365: Private site cannot be access (since surf-config is not imported) - causes: Could not resolve view with name ... 31695: THOR-367 - #2 Document Library - remove Create Content... menu - remove actions: manage aspects, change type, publish, manage rules (for folders) - document-details page: remove publishing history panel 31697: For reference only: update description of cmis/test webscript 48119: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 46972: Fix test fallout (re: THOR-293) 48120: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (repo pre-merge)§ 47001: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35798: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31805: Adding utility method to our CollectionUtils class that I need as part of pending invitations work (THOR-373). 31809: Parameterized signup url & email 31812: THOR-373 Pending invitations. 31814: Made changes to way aid is captured ready for allowing events to override aid if needed 31820: Mapping of network admin to system admin part 1: 35801:Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31829: Fixed THOR-352 "Incorrect validation of emails on "Forgot Password" page" 31830: (RECORD ONLY) Exclude ExportDbTest; issues with MySQL 31831: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: 31784: Fix up unit test. 31833: Email validation now allows 7 character long top level domain (so we can do tests with example) 31834: New form colors for invalid & mandatory fields 31837: THOR-327 - remove bootstrapped guest / guest@<tenant> 31838: THOR-327 - remove bootstrapped guest / guest@<tenant> 31844: Added missing headers to Java files. 31845: Mapping of network admin to system admin part 2: 31846: Addition of very basic test script for the Script API of AnalyticsService. 35803: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31853: Forms refactor first cut - for review 31855: THOR-387. Analytics event for user activation is sent. 31858: THOR-387. Fixing a corner-case bug in SendAnalyticsRequest. 31863: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: 31841: Build Fix 31868: THOR-361: Fix /service/index 31881: THOR-387. Adding analytics event for site invitation. 31882: THOR-387. Fixing analytics event for site invitation. 31883: THOR-66: disable some of the /alfresco (web.xml) servlet mappings 31884: THOR-387. Analytic event callouts for site invitation response. 31899: Revert solrcore.properties checkin 31900: THOR-249: override edition interceptor 31901: Fix for THOR-396. Spelling mistake on signup screen. 31902: Resolve THOR-251: Update the Help URLs for Cloud 31904: Resolve THOR-403: -system- tenant not found logged from server 31918: Create site form tweak (manual form.validate() call required since javascript is changing a another fields value) 31919: Logout page refactoring 31925: Create site now resets form before show using forms-runtime's new "reset" method 31926: Disable flash upload 31927: THOR-363: increase initial file quota 31930: Updated SimpleDB service so you can set the SimpleDB domain to record events too 47003: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35804: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31933: THOR-387. Analytics. Added analytic call for account registration (the initial signup, not the activation, which was added previously). This adds a new mandatory parameter to the signup webscript: "source" as well as various new optional parameters. The same parameter is now mandatory on the RegistrationService. Impacts on test code. Changed the rest-client .rcq file to show new required parameter. Changed AnalyticsProperties to take the Object wrappers for primitives as these are optional and so we need to be able to pass null. Added a new (hidden) field to Erik's signup Share page to send an appropriate value for the signup. 31939: THOR-404: disable JBPM 31943: THOR-387. Analytics. I've overridden upload.post.js to add analytics data for file uploads. 31946: Fixed THOR-385 "Account summary file usage bar does not display for any theme other than the default theme" 31947: Fixed THOR-308 "Invite user drop-down works incorrectly" 31948: Resolve THOR-384: It is impossible to create user administrator@'domain': 31949: Follow-up fix for case sensitive user names 31953: THOR-311: It is impossible to create workflow when 'Send Email Notifications' flag is checked: 31959: Removing change-password override since user shall be able to change his password 31961: Fix tests after recent username/email address changes 31966: Grey Theme 31979: Dropping Analytics logging level down to 'warn' from 'debug'. 31982: Fixed THOR-419 "UI edits required" 31983: Fixed THOR-419 "UI edits required" part 2 32003: THOR-422. Spurious error logging during signup/registration (not activation). This was because the analytics event action code assumed the user exists, which they don't do at registration, of course. 32004: Resolve undefined undefined seen in invite signup dialog 32006: Restricted tentant component now displays dialog instead of gray page 32007: THOR-300: fix AWS config 32013: Fixed THOR-353 "No validation for the fields on the "Reset Password" page" 32014: Fixed THOR-423 "Removing the yellow "Welcome to your dashboard, firstname, lastname" causes error" 32018: Made sure new cloud theme (greyTheme) also has new theme border & bgs (making the account quota being displayed) 48122: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47007: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 35817: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32250: (RECORD ONLY) Merged /HEAD to BRANCHES/DEV/THOR1: (ok'ed with DC/DG) 31750: Solr: Fix owner Id cache 31751: Fix for ALF-11104: add authenticated user to authorisations list in PermissionService + fix inconsistency in AuthorityService 31760: Correct Fix for SOLR owner ID cache 32172: Fixes for: ALF-11521 Protect SOLR running against the wrong Alfresco DB ALF-11602 Solr Core Tracker - does not need to re-init CMIS dictionary (when there are no model changes) ALF-11621 SOLR old versions of tracked models are not getting deleted when models are updated 32234: Fix for ALF-11568 SOLR indexing is ignoring properties that are indexed but not tokenised and not stored - was WCMQS navigation is broken 32256: THOR-488. Tidy up account types. 32258: Login analytics event. 32260: Reduce logging on startup for enabled tenants (see also THOR-475 / THOR-81) 32262: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: 32139: Fix for ALF-11599 - Section ''Others are Editing'' shows documents that should not be present 48123: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (repo pre-merge) 47038: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35811: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32019: Merged rev 32016 from THORSURF1 32021: THOR-428: Fix activity feed email notifications (to contain network/tenant ctx) 32024: Fixed "THOR-424 'Upload File' button is disabled in FF for the second and futher uploads" 32026: Restricted tenant page now has link back to users home dashboard so he doesn't feel stuck 32029: Fixed GetRequest test to ignore uid's that aren't emails (like admin) 32030: THOR-310: Override getCacheKey method from AbstractCachedViewResolver to ensure that each tenant gets their own cached copy of each Share page (this ensures that nested Component config gets processed for all tenants) 32031: Resolve THOR-417 Workflow notification emails do not take into account tenant in their urls back to Share 47039: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35812: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32041: Label changes according to Kathryn's "UI Text_scenario 5.docx" 32052: THOR-405: Fix 'contentstore.deleted' to be on S3 (albeit co-mingled) 32058: Removed unnecessary borders from profile pages 32065: Fix build issue where cloud share war was not being cleaned before build 32066: Fix those pesky solrcore properties 32071: THOR-461: fix following email notification (to contain network/tenant ctx) 32076: Fix to disable error on unit tests 32077: Added logging to NullPointerException fix 35814: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32103: Finally! A fix for THOR-193. :) 32119: Fix for setting theme as network admin 32120: Improved text on upgrade account page 32124: Refactored CloudInvitationService Integration Tests to allow for easier expansion and then I expanded. 32130: Fix for THOR-457. Already have an account email template needs updating/fixing. 32135: THOR-464 Fix "ThumbnailRegistry init does not scale with # of tenants" 32140: Apply Beta logos and adjust about dialog for cloud 35815: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32144: THOR-438: Latest Spring Surf libs (fix relative URI login redirect problem caused by un-encoded URI) 32147: THOR-475 - improvement(s) to trim time to create tenant 32148: THOR-475 - improvement(s) to trim time to create tenant 32154: GreyTheme updates 32157: THOR-430: Forgot password dialog: UI text not what was suggested 32159: GreyTheme updates 32174: THOR-454 - User can find content stored in Company Home/Data Dictionary via Advanced Search 32176: Signup page now cloud.alfresco.com 32179: THOR-475 - improvement(s) to trim time to create tenant 32184: Remove jargon from workflow names and descriptions 32185: Pesky solrcore.properties 35816: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32188: THOR-478: Updated Spring Surf libs - fixed relative URL redirect after login including support for @ symbol in URL 32195: Fix for THOR-379. Pending invitations UI show invitee emails as links to profile pages - even for non-existent users. Added yet more data to the CloudInvitation REST API: inviteeIsMember which tells caller whether the invitee is already a member of the tenant in which the invitation is running. Returning this flag through the Java API & REST API Tweaks to the Share JS so that it renders a <span> for invitees who are not members and an <a> for those who are. 32198: Replace workflow text with task related text 32202: Resolve THOR-481: Moving or copying content always shows error popup but always succeeds 32204: Build fix 32238: THOR-290: Configurable google-analytics tracking code script insertion 32239: Tidying up some UI text. Missing apostrophes, invitation instead of invite. 32241: THOR-471: Added GetSatisfaction feedback widget 35818: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32266: Addition of createSite analytics recording. 32268: THOR-505: Disable (turnoff autostart) of unused subsystems 32270: Adding in some theme colors that dissapeared (will make the quota bar get displayed again) 32272: Resolve THOR-354: (None) displayed for network administrators 32273: THOR-499: New Relic monitoring updates 32279: Implemented THOR-508 "Accept terms & conditions checkbox & link on the complete profile pages" 32280: Fixed THOR-474 "Password Strength indicator does not conform with other leading website password indicators" 48125: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: (repo pre-merge) 47053: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32377: (RECORD ONLY) THOR-565: fix unfortunate type that affects activity permissions (for connected users - either via site membership or followers) 32378: CollectionUtils method for collection intersection. Should be merged to HEAD. 32383: THOR-572: remove unused JBPM servlets (deployprocess, workflowdefinitionimage) 32384: Fixed THOR-549 "Google Analytics Installed but not seeing any events raised on GA reports" 32389: Fix for THOR-567 "userprofile broken" 32401: THOR-525 - fix MT-specific issue (deleting site does not clear associated activities within tenant) 32409: THOR-66: disable WebDAVServlet (does not need to load-on-startup) + a few others 32414: Theme updates from linton 32423: Fixed THOR-661 "Limit number of simultaneous connections in drag n drop upload" 32424: THOR-81: support for signup/activate scaling tests 48126: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47058: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts (not mergeinfo/slingshot/web-framework-commons/3rd-party) 35827: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35828: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35829: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35830: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35831: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35832: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 48129: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47067: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35844: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35845: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35846: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35847: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35848: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35849: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35850: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35853: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35854: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35855: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 47069: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35860: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33057: Refactored Slingshot overrides so that they are now in the Thor-Share private module. This has been done to reduce conflict issues when merging back into HEAD. The overrides are now in the correct locations (the only files that could not be moved to the private module are urlrewrite.xml and surf.xml). 35870: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: (part 1 - repository project) 33022: THOR-662: Email templates should load/resolve (initially) from classpath 47071: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35877: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33090: ALF-10826: hidden aspect 33091: THOR-416: fix surf-config folder (appears where it shouldn't) 33093: Sweep through email templates. 47072: Fix merge error (FeedCleaner) 47073: Fix merge error (WorkflowTestSuite) 47074: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35881: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33104: Tweak to invitation email template 33112: Refactored impl of THOR-694 so that content limit of 25Mb is on by default in THOR for both local FS and S3-based FS. Changed ContentLimitProvider bean to take String limit, rather than long - to allow empty string value on core Alfresco. Set the limit to the empty string in core Alfresco, which means 'no limit'. Applied the limit always. Set the limit to 25Mb in Thor/alfresco-global.properties Fixed a minor bug in error reporting due to previous exception renaming. 47076: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35885: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33134: THOR-874: Updated Surf libs Fixes ArrayIndexOutOfBoundsException caused by multiple threads apply i18n extensions to a WebScript 33135: Missing WebScripts sources JAR from r33134 33153: Resolve THOR-551: Password Hashes Need Review 33154: Update to latest email blacklist 33155: Fixed THOR-534 "Login Box shows scroll bars" 33156: Build fix for tests failing due to recent password changes 33157: Build fix for updated email blacklist 33172: THOR-776: Re-implement Share override as guided by Erik 33173: THOR-831: Text in Someone 35886: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33174: Latest SpringSurf libs - improved RemoteClient reused of connections per request thread. 33176: THOR-833: Search: clicking on All Sites returns no results 48131: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47098: (RECORD ONLY) Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35895: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 34105: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/THOR1: 33267: (RECORD ONLY) Created branch THOR1_SPRINTS (from THOR1 r33255) 33269: Snapshot of simple redeploy shell script (for AWS mini-dev/test env) 33272: JMeter test script 34106: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/THOR1: 33313: THOR-928: Added caching for i18n bundles provided by extensibility modules (latest Surf libs, r980) 48133: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47097: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35906: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35907: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 47099: Fix merge/compile error. 47103: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35913: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35914: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35915: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35916: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35917: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 47111: Fix merge error 47115: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35930: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35933: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35934: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 47132: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 36053: 1st pass at upgrading to latest Spring Surf 36059: Fix CloudInvitationService tests for cloud1 47133: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 48135: Merged DEV/CONV_V413 to DEV/CONV_HEAD 46977: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35792: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31724: Can't compare pages using page.url.uri anymore since that doesn't include the tentant, now skips that part of the url and uses page.id instead. 31733: Add account info to user network web script 31736: Refactored RegistrationServiceImpl.promote... so that it uses the presence of cloud:personExternal aspect to prevent promotion of external users rather than account-based data. 35794: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31744: Account relates to url & various Share features now hidden in cloud 31746: 1/5 for THOR-341 "F147: Share features are disabled for external network member" 35796:Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31756: THOR-265: Currently the first user to sign up to a dmain becomes the domain admin, and can view the full admin console. Is this going to change? 31771: Added stub for SimpleDBAnalytics Service 31772: Final interfaces and integration with SimpleDB for Analytics 31774: Modified landing_time key for MixPanel 31776: Implemented #3 for THOR-341 "F147: Share features are disabled for external network member" 31777: Resolve test classpath since introduction of new thor libs 31779: Implemented #3 for THOR-341 "F147: Share features are disabled for external network member" part 2 31781: Resolve issue getting access to account settings when network admin of paid business account 31783: Implemented #2 for THOR-341 "F147: Share features are disabled for external network member" 31794: Minor changes after review with DavidC and NeilM 31797: Collaboration title improvement: Now hiding the html elements used to build the menu until the menu is created so ui doesn't bump and look ugly. 31799: Part #6 of THOR-367 "F60: Remove Share features not required for Cloud" - 6) Document Selectors - root is Sites folder 31801: Removed ugly "extra" borders around some of the input fields in the user profile form 31802: Fixed part #7 of THOR-367 "F60: Remove Share features not required for Cloud" - 7) Edit Profile - remove edit email from edit profile form 31804: Fixed part #5 of THOR-367 "F60: Remove Share features not required for Cloud" - 5) Move... / Copy... dialog - remove repository - remove my user home 48136: Merged DEV/CONV_V413 to DEV/CONV_HEAD 47001: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35798: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31805: Adding utility method to our CollectionUtils class that I need as part of pending invitations work (THOR-373). 31809: Parameterized signup url & email 31812: THOR-373 Pending invitations. 31814: Made changes to way aid is captured ready for allowing events to override aid if needed 31820: Mapping of network admin to system admin part 1: 35801:Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31829: Fixed THOR-352 "Incorrect validation of emails on "Forgot Password" page" 31830: (RECORD ONLY) Exclude ExportDbTest; issues with MySQL 31831: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: 31784: Fix up unit test. 31833: Email validation now allows 7 character long top level domain (so we can do tests with example) 31834: New form colors for invalid & mandatory fields 31837: THOR-327 - remove bootstrapped guest / guest@<tenant> 31838: THOR-327 - remove bootstrapped guest / guest@<tenant> 31844: Added missing headers to Java files. 31845: Mapping of network admin to system admin part 2: 31846: Addition of very basic test script for the Script API of AnalyticsService. 35803: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31853: Forms refactor first cut - for review 31855: THOR-387. Analytics event for user activation is sent. 31858: THOR-387. Fixing a corner-case bug in SendAnalyticsRequest. 31863: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: 31841: Build Fix 31868: THOR-361: Fix /service/index 31881: THOR-387. Adding analytics event for site invitation. 31882: THOR-387. Fixing analytics event for site invitation. 31883: THOR-66: disable some of the /alfresco (web.xml) servlet mappings 31884: THOR-387. Analytic event callouts for site invitation response. 31899: Revert solrcore.properties checkin 31900: THOR-249: override edition interceptor 31901: Fix for THOR-396. Spelling mistake on signup screen. 31902: Resolve THOR-251: Update the Help URLs for Cloud 31904: Resolve THOR-403: -system- tenant not found logged from server 31918: Create site form tweak (manual form.validate() call required since javascript is changing a another fields value) 31919: Logout page refactoring 31925: Create site now resets form before show using forms-runtime's new "reset" method 31926: Disable flash upload 31927: THOR-363: increase initial file quota 31930: Updated SimpleDB service so you can set the SimpleDB domain to record events too 48137: Merged DEV/CONV_V413 to DEV/CONV_HEAD 47003: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35804: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 31933: THOR-387. Analytics. Added analytic call for account registration (the initial signup, not the activation, which was added previously). This adds a new mandatory parameter to the signup webscript: "source" as well as various new optional parameters. The same parameter is now mandatory on the RegistrationService. Impacts on test code. Changed the rest-client .rcq file to show new required parameter. Changed AnalyticsProperties to take the Object wrappers for primitives as these are optional and so we need to be able to pass null. Added a new (hidden) field to Erik's signup Share page to send an appropriate value for the signup. 31939: THOR-404: disable JBPM 31943: THOR-387. Analytics. I've overridden upload.post.js to add analytics data for file uploads. 31946: Fixed THOR-385 "Account summary file usage bar does not display for any theme other than the default theme" 31947: Fixed THOR-308 "Invite user drop-down works incorrectly" 31948: Resolve THOR-384: It is impossible to create user administrator@'domain': 31949: Follow-up fix for case sensitive user names 31953: THOR-311: It is impossible to create workflow when 'Send Email Notifications' flag is checked: 31959: Removing change-password override since user shall be able to change his password 31961: Fix tests after recent username/email address changes 31966: Grey Theme 31979: Dropping Analytics logging level down to 'warn' from 'debug'. 31982: Fixed THOR-419 "UI edits required" 31983: Fixed THOR-419 "UI edits required" part 2 32003: THOR-422. Spurious error logging during signup/registration (not activation). This was because the analytics event action code assumed the user exists, which they don't do at registration, of course. 32004: Resolve undefined undefined seen in invite signup dialog 32006: Restricted tentant component now displays dialog instead of gray page 32007: THOR-300: fix AWS config 32013: Fixed THOR-353 "No validation for the fields on the "Reset Password" page" 32014: Fixed THOR-423 "Removing the yellow "Welcome to your dashboard, firstname, lastname" causes error" 32018: Made sure new cloud theme (greyTheme) also has new theme border & bgs (making the account quota being displayed) 48147: CONV: fix merge issue - remove duplicate prop def (contentLimitProvider) 48148: Merged DEV/CONV_V413 to DEV/CONV_HEAD merge fix for r48072 48149: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47111: Fix merge error 47115: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 35930: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35933: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35934: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 47132: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: - pre-merge of repo parts 36053: 1st pass at upgrading to latest Spring Surf 36059: Fix CloudInvitationService tests for cloud1 47133: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 48150: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: (effectively RECORD ONLY - no changes) 47173: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: 36232: MT - fix pop of tenant ctx (to match push) 48154: Merged DEV/CONV_V413 to DEV/CONV_HEAD 47038: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35811: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32019: Merged rev 32016 from THORSURF1 32021: THOR-428: Fix activity feed email notifications (to contain network/tenant ctx) 32024: Fixed "THOR-424 'Upload File' button is disabled in FF for the second and futher uploads" 32026: Restricted tenant page now has link back to users home dashboard so he doesn't feel stuck 32029: Fixed GetRequest test to ignore uid's that aren't emails (like admin) 32030: THOR-310: Override getCacheKey method from AbstractCachedViewResolver to ensure that each tenant gets their own cached copy of each Share page (this ensures that nested Component config gets processed for all tenants) 32031: Resolve THOR-417 Workflow notification emails do not take into account tenant in their urls back to Share 47039: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35812: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32041: Label changes according to Kathryn's "UI Text_scenario 5.docx" 32052: THOR-405: Fix 'contentstore.deleted' to be on S3 (albeit co-mingled) 32058: Removed unnecessary borders from profile pages 32065: Fix build issue where cloud share war was not being cleaned before build 32066: Fix those pesky solrcore properties 32071: THOR-461: fix following email notification (to contain network/tenant ctx) 32076: Fix to disable error on unit tests 32077: Added logging to NullPointerException fix 35814: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32103: Finally! A fix for THOR-193. :) 32119: Fix for setting theme as network admin 32120: Improved text on upgrade account page 32124: Refactored CloudInvitationService Integration Tests to allow for easier expansion and then I expanded. 32130: Fix for THOR-457. Already have an account email template needs updating/fixing. 32135: THOR-464 Fix "ThumbnailRegistry init does not scale with # of tenants" 32140: Apply Beta logos and adjust about dialog for cloud 35815: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32144: THOR-438: Latest Spring Surf libs (fix relative URI login redirect problem caused by un-encoded URI) 32147: THOR-475 - improvement(s) to trim time to create tenant 32148: THOR-475 - improvement(s) to trim time to create tenant 32154: GreyTheme updates 32157: THOR-430: Forgot password dialog: UI text not what was suggested 32159: GreyTheme updates 32174: THOR-454 - User can find content stored in Company Home/Data Dictionary via Advanced Search 32176: Signup page now cloud.alfresco.com 32179: THOR-475 - improvement(s) to trim time to create tenant 32184: Remove jargon from workflow names and descriptions 32185: Pesky solrcore.properties 35816: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32188: THOR-478: Updated Spring Surf libs - fixed relative URL redirect after login including support for @ symbol in URL 32195: Fix for THOR-379. Pending invitations UI show invitee emails as links to profile pages - even for non-existent users. Added yet more data to the CloudInvitation REST API: inviteeIsMember which tells caller whether the invitee is already a member of the tenant in which the invitation is running. Returning this flag through the Java API & REST API Tweaks to the Share JS so that it renders a <span> for invitees who are not members and an <a> for those who are. 32198: Replace workflow text with task related text 32202: Resolve THOR-481: Moving or copying content always shows error popup but always succeeds 32204: Build fix 32238: THOR-290: Configurable google-analytics tracking code script insertion 32239: Tidying up some UI text. Missing apostrophes, invitation instead of invite. 32241: THOR-471: Added GetSatisfaction feedback widget 35818: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32266: Addition of createSite analytics recording. 32268: THOR-505: Disable (turnoff autostart) of unused subsystems 32270: Adding in some theme colors that dissapeared (will make the quota bar get displayed again) 32272: Resolve THOR-354: (None) displayed for network administrators 32273: THOR-499: New Relic monitoring updates 32279: Implemented THOR-508 "Accept terms & conditions checkbox & link on the complete profile pages" 32280: Fixed THOR-474 "Password Strength indicator does not conform with other leading website password indicators" 47053: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32377: (RECORD ONLY) THOR-565: fix unfortunate type that affects activity permissions (for connected users - either via site membership or followers) 32378: CollectionUtils method for collection intersection. Should be merged to HEAD. 32383: THOR-572: remove unused JBPM servlets (deployprocess, workflowdefinitionimage) 32384: Fixed THOR-549 "Google Analytics Installed but not seeing any events raised on GA reports" 32389: Fix for THOR-567 "userprofile broken" 32401: THOR-525 - fix MT-specific issue (deleting site does not clear associated activities within tenant) 32409: THOR-66: disable WebDAVServlet (does not need to load-on-startup) + a few others 32414: Theme updates from linton 32423: Fixed THOR-661 "Limit number of simultaneous connections in drag n drop upload" 32424: THOR-81: support for signup/activate scaling tests 48157: Merged DEV/CONV_V413 to DEV/CONV_HEAD Fixing merge issue from r48135 48158: Merged DEV/CONV_V413 to DEV/CONV_HEAD (RECORD ONLY) 47046: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32281: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1 (ok'd with DC): 32242: ALF-11664 Moderated sites should use site.public.group (from SysAdminParams) for setting the group with general access, as Public sites already do, rather than hard coding the EVERYONE group 32283: Added "guest" to the list of blocked usernames, thereby showing failure to register guest@tenant.com, rather than allowing it and failing to activate the account later. 32285: THOR-505: Disable (turnoff autostart) of unused subsystems 32286: Fix NPE in AnalyticsProperties when empty json provided 32287: THOR-508 "Accept terms & conditions checkbox & link on the complete profile pages" 32289: Miscellaneous changes to account types & classes. 32290: Change "recent activities" email notification interval from hourly to daily 32292: Resolve THOR-516: Check all email template URLs point to cloud.alfresco.com not www.alfresco.me 32293: THOR-517. Insert Signup Analytics Event into Site Invite process. 32305: Fixed THOR-306 "Invite user autocomplete not working correctly" 32308: THOR-529: Red "No items" in doc lib when adding 1st document into a x-network site (WebDAV error in log) 32309: Fixed THOR-306 "Invite user autocomplete not working correctly" 32314: THOR-520: Change workflow in tooltip text to task 32318: THOR-532: Improve auto-generate of home site shortname (in case of clash) 32339: Updated analytics events to include parameter 32342: Additional debug logging as part of THOR-544. 32376: THOR-574: Accept invite while logged in displays 'you've declined...' message 48163: Merge CONV_V413 to CONV_HEAD 46713: Set Maven version in POM files to 4.1.3-CONV-SNAPSHOT 46741: Deploy SPP jar file (aka VTI) into Maven repository as well 47440 RECORD ONLY: Declare dependency on Surf 1.2.0-SNAPSHOT in Maven poms 47450 RECORD ONLY: Bring Chemistry OpenCMIS libs back into the wars 47579 RECORD ONLY: Switch Chemistry OpenCMIS version to a custom 0.8.0-20120706 47646: POM dependency: use 4.2-min version of netcdf rather than 4.2, which embeds an old commons-codec 47683: Create a jar holding the sharepoint config, for use with CLOUD2 47740 RECORD ONLY: Merge V4.1-BUG-FIX to CONV_V413 46360: ALF-17697: Create proper source jars, to deploy to Maven repository 47964: Filter servlet-api from dependencies 48166: Merged DEV/CONV_V413 to DEV/CONV_HEAD 47064: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35827: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32446: Fixed THOR-658 "File Upload Limits" 32455: Tweak logging (S3 exists check -> debug) 32462: Move tenant enabled check from low level services to web script entry point: 32467: THOR-666: Improve startTenants - do not need to re-update enable/disable flag on startup 32474: JMeter test script updates (#3) 32485: Fix to ensure the HTML upload POSTed response can return html content type. 32486: JMeter test script updates (#4) 47084: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35828: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32491: Fix account signup since change to tenant authentication (which is now slightly stricter: 32518: Fix for mixed cased usernames login problems 32523: Add db pool validate query 32546: Minor - remove unecessary call to getObjectDetails (to avoid calling twice for non-existent object) 32556: Fixed tenant url edge cases and followed up a fix started by DavidC for signup logins 32560: Fixed tenant url edge cases and followed up a fix started by DavidC for signup logins - part 2 35829: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32571: Resolve THOR-653: Uploads consume disk space in /var/cache/tomcat6/ 32572: Fixed THOR-563 "UI: CSS / layout issue on profile page" 35830:Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32577: THOR-682: refactor Tenant/S3 routing content store (it is now self-routing based on S3 content url) 32580: Added missing init-method attributes to key CachingContentStore components. 32583: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: 32321: ALF-11700: Possible to generate feed entries with malformed NodeRefs 32593: THOR-688 Analytics to support various URLs not just "website". Addition of optional sourceUrl paramater to account-signup analytics. 32603: Added file size limitation and hooked in html upload to the form validations w backgrounds and tooltips. 32629: THOR-199: Fix create user (activate) sometimes has to retry - due to: "Deadlock ... alfresco.permissions.insert_AclMember-Inline" 32654: THOR-692: Disable (auto) home folder creation 35831: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32661: Latest SpringSurf libs: 35832: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32666: Build: add cloud/cloud-share to ant clean-modules/clean 32672: Ensure that application context is available for TenantAlfrescoAuthenticator in TenantUserFactory 32675: THOR-536: Added TenantPageTypeViewResolver 32686: JMeter test script updates (#5) 32700: THOR-689: DevTest: 2 uploads failed (out of 10000) - missing retry ? 35844:Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32702: JMeter test script update 32718: THOR-691: Feed Notifier sends emails on startup of Server 32756: Fixed THOR-556 "Can't view members in a public Site" 35845: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: (+ resolved conflicts w/ 4.0.1) 32032: THOR-370: Add tenant-switching to /cmisatom (OpenCMIS-based v4.x impl => AlfrescoCmisService) 35846: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32759: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1 32757: Fix for ALF-9365 32761: Changed restricted tenant to appear as page not found 32763: THOR-792: Ensure that failed login returns to login page 32769: Updated networks icon 32770: Authentication updates: Unauthenticated requests to inaccessible tenants (either that don't exist or not authorized to access) will be prompted for authentication and if credentials are valid the "Page Not Found" page will be shown, but authentication will have completed and user can return to their home dashboard via link provided 32785: Fix for THOR-798 32789: THOR-796: reduce startup time (1000s of tenants) 35847: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32790: THOR-480: Spring Surf lib updates - ensure that i18n properties extensions degrade the specificity of the locale to ensure that no message keys are shown (unless the message genuinely doesn't exist) 32798: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: (fix for THOR-721) 32245: Unit tests for ALF-10343, with the problematic parts commented out pending a fix 32251: ALF-11664 site.public.group (via SysAdminParams.SitePublicGroup) should be used when updating site visibilities, as it is for creating sites 35848: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32805: Latest SpringSurf libs: 47085: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35849: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32807: Fixed invalid network switching URLs 32808: Updated Spring Surf libs to that revert invalid changes to relativeUri determining method 32837: Set Alfresco connector reconnect timeout to zero 32856: Fix for THOR-801. Trying to access the archive as 'admin' gives error. 35850: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 32858: (RECORD ONLY) Merged HEAD to BRANCHES/DEV/THOR1: (pull in some pre-reqs for cleaner merge of ALF-10826) 31864: ALF-10686 - Original modification date is lost when files are copied into Alfresco via CIFS 31934: Update stale File State Cache. 32068: ALF-10941 - CIFS Open file from excel 32097: build fix. 32131: ALF-10902 - No friendly notification occurs when Editor or Collaborator tries to delete content 32132: Open read-only for attributes only. 32182: ALF-10963 Cannot overwrite files on CIFS share with Notepad++ 32876: THOR-784: Fix 'Accounts API loading is very slow' (get page of accounts) 32939: THOR-480: Latest Spring Surf libs - fix i18n extensibility problems. 32948: THOR-859: Performance: Disable rules service 32953: THOR-863: Performance: loadUserByUsername -> isAdminAuthority 32959: (RECORD ONLY) Merged HEAD to DEV/THOR1 32958: (record-only) Merged Dev/THOR1 to HEAD 32945: Fix for ALF-12122 Some CMIS queries with SOLR are not returning correct results 35853: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33024: Fixed THOR-670 "Incorrect window title for 'Task History' page" 33027: Missing merge info for r32694 35854: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33033: Latest SpringSurf libs: 35855: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33038: Minor: do not bootstrap web script readme x2 into Data Dictionary (when creating tenant) 33039: Resolve THOR-839: Following webscripts doesn't set Content-Type response header 33040: Fixed THOR-817 "Issues with "invite user" email autocomplete field" 33041: Fixed THOR-789 "Mix of languages" 33042: Resolved THOR-849: Upload issue ? - Failed to get content ... (No such file or directory) ... x22 48169: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47176: (RECORD ONLY) Fix Eclipse .classpath to match Spring Surf libs 48170: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 48168: CONV: Fix NPE in get people CQ 48183: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47184: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 35989: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/THOR1: 34153: Minor: THOR-5: MT-aware immutable singletons (spp/vti) 34161: Prevent session timeout redirect problem resulting from clicking user link in activities feed 34183: Part one of THOR-1129. 34185: Part two of THOR-1129. The Thor-specific parts. 34199: Fix for THOR-106 a failing test case that was switched off. 34202: THOR-106 addendum. Editing build.xml to put the test class back in to the build. 34211: BM: sync ThorTest (additional coverage) 34308: Merged HEAD to THOR1_SPRINTS 34250: Fixed THOR-1137 "Make Spring Surf enable-auto-deploy-modules by default" 34540: Share UI - copyright should be 2012 (related to THOR-1015) 35286: Resolve THOR-1242: Update Beta Logo 48187: Merged DEV/CONV_V413 to DEV/CONV_HEAD 47086: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35860: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33057: Refactored Slingshot overrides so that they are now in the Thor-Share private module. This has been done to reduce conflict issues when merging back into HEAD. The overrides are now in the correct locations (the only files that could not be moved to the private module are urlrewrite.xml and surf.xml). 35870: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: (part 1 - repository project) 33022: THOR-662: Email templates should load/resolve (initially) from classpath 35877: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33090: ALF-10826: hidden aspect 33091: THOR-416: fix surf-config folder (appears where it shouldn't) 33093: Sweep through email templates. 35881: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33104: Tweak to invitation email template 33112: Refactored impl of THOR-694 so that content limit of 25Mb is on by default in THOR for both local FS and S3-based FS. Changed ContentLimitProvider bean to take String limit, rather than long - to allow empty string value on core Alfresco. Set the limit to the empty string in core Alfresco, which means 'no limit'. Applied the limit always. Set the limit to 25Mb in Thor/alfresco-global.properties Fixed a minor bug in error reporting due to previous exception renaming. 35885: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33134: THOR-874: Updated Surf libs Fixes ArrayIndexOutOfBoundsException caused by multiple threads apply i18n extensions to a WebScript 33135: Missing WebScripts sources JAR from r33134 33153: Resolve THOR-551: Password Hashes Need Review 33154: Update to latest email blacklist 33155: Fixed THOR-534 "Login Box shows scroll bars" 33156: Build fix for tests failing due to recent password changes 33157: Build fix for updated email blacklist 33172: THOR-776: Re-implement Share override as guided by Erik 33173: THOR-831: Text in Someone 47096: Fix merge compile issue 47100: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35906: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33054: THOR-796: slow startup time (on QA env with ~ 10k tenants) 33055: Implementation of THOR-694. File size upload limit within ContentStore. 35907: Merged BRANCHES/DEV/THOR1 to BRANCHES/DEV/CLOUD1: 33213: THOR-833: wip 33214: Allow for workflows which may have a reference to a repo based email template 33228: Added extension points for links in user profile toolbar 33230: Added extension points for links in user profile toolbar part 2 33232: Fixed THOR-907 "Remove Share functionality which allows access to people profiles outside of your site memberships" 33233: Fixed THOR-907 "Remove Share functionality which allows access to people profiles outside of your site memberships" part 2 33234: Extension points in members bar now ft the pattern of user profile toolbar. 33236: Fixed THOR-907 "Remove Share functionality which allows access to people profiles outside of your site memberships" part 3 33241: THOR-908 - wip 33243: THOR-908 / THOR-64 - wip 33253: Fixed THOR-907 "Remove Share functionality which allows access to people profiles outside of your site memberships" part 4 33255: Fixed THOR-907 Remove Share functionality which allows access to people profiles outside of your site memberships part 5 47169: Merged from BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413 35913: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33410: Merged form THOR1_SHARE_PERFORMANCE to THOR1_SPRINTS 33111 Branch for testing out new Spring Surf client side resource improvements 33291 Share client side resource handling to avoid stale client side cache part 1 - New spring surf libs with <@script> & <@link> directives that adds the checksum of the file to avoid client cache beoming stale - Refactored most old <script> to become <@script> - Refactored most old <link> to become <@link> - Removed old <@link> macro from alfresco-temaplte.ftl, resources.get.html.ftl & corm-console.ftl (now using the directive instead) - i18n messages now imported by <@generateMessages> directive to avoid stale cache - Added calendar, cookie, resize & uploader yui modues to yui common to decrease the number of .js files requested - Added new YUI module filter that adds "-min.js?v=<YAHOO.VERSION>" to stop a yui resources being stale after a yui upgrade 33307 Share client side resource handling to avoid stale client side cache part 2 - Made TinyMCE avoid becoming stale after a new release 33334 Share client side resource handling to avoid stale client side cache part 2 - New surf libs with <@checksumResource> directive used by ie6.css, ie7.css, ipad.css & tiny_mce.js to avoid manual change of version number in script import 33368 Share client side resource handling to avoid stale client side cache part 3 - Avoiding re-load of .js, .css & images (referenced from a .css) when switching tenants (note images that have been referenced using <img src=""> will get reloaded) 33405 CSS import duplication fix 35914: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33417: Thor JMeter test script tweaks 33420: THOR-1000: Solr tracking: NodeContentGet should not create (empty) temp file if there is no transformer (eg. for image node) 33434: The <#if> statement & <script> element for google analytics wasn't in sync, causing a closing </script> element always being printed. 33440: Latest SpringSurf libs - performance and thread safety improvements. 33458: ThorTest-preReg (JMeter) test update 33460: Latest SpringSurf libs: 33466: THOR-1002: Updated enterprise overlay 33480: Latest SpringSurf libs - Surf performance improvements from Thor high load profiling in Jmeter/Jprofiler 35915: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33493: THOR-979: HTML5 upload support 33505: THOR-983: Preload images, JS and CSS for basic dashboards, document library and document details (from login page) 33518: THOR-979: HTML5 upload tweaks (upload doesn't start automatically when updating to give opportunity to set version type and add comment 33520: THOR-900: Modified header.get.html.ftl to ensure that user name is URL encoded (so that the "@" symbol in the user name becomes "%40" to ensure that timeout redirects work) 33527: THOR-1027: Header Alfresco image now links back to application context and about dialog is now linked from footer Alfresco image 33551: THOR-1007: Fixed upload hang on FireFox when uploading folders 35916: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33556: Merged BRANCHES/DEV/THOR1_INVITATION to BRANCHES/DEV/THOR1_SPRINTS: 33386: Branch for Invitation enhancements 33474: THOR-1006. Part 1. Services-level changes to support invitation enhancements. 33475: THOR-1006. Documentation on the desc.xml. 33476: THOR-1006. Commenting out some unfinished code to avoid any unwanted side-effects. Still to do: get the authentication check working and ensure no unexpected side-effects. 33483: THOR-1006. Completion of basic services changes to support 'accept invitation on alternate email'. Note! The authentication of the alternative email's password is NOT YET IMPLEMENTED due to a repo dependency. This MUST be implemented before merge to THOR1_SPRINTS. I'll create a new JIRA. 33511: Fix for THOR-1017. 33525: THOR-1017. Slight improvement to desc.xml doc. HTTP status codes in response. 33529: Fixed THOR-980 & THOR-1024 & THOR-1025 33553: Fixed THOR-980 "F14: Allow users to login using existing email address if invite is sent to wrong email address and they already have an account" 33571: Fixed HTML5 uploader to work with profile avatar image upload 33585: Thor JMeter test script tweaks 33596: THOR-1035: Enabled HTML5 uploader for application logo upload 33598: THOR-1031: Reduced HTML5 checks for uploader to ensure that it works for Safari on Mac 33603: THOR-1039: Updated UX for HTML5 upload when 0kb files are selected 33606: THOR-1037: Updated variable titles for HTML5/DND upload dialog to support update 35917: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33615: Latest SpringSurf libs - performance and concurrency improvements 33690: Resolve THOR-1003: Forgotten Password email is case sensitive 33692: Merged BRANCHES/DEV/THOR1_PRIVACY to BRANCHES/DEV/THOR1_SPRINTS: 33488: Reversed merge revisions related to THOR-907 - 33232, 33233, 33236 - hand tweaks related to 33253, 33255. 33492: Removed unused import of com.sun... class 33497: Fixes and improvements to user profile page loading - reducing remote calls required and refactoring link build code. Fixed a issue with displaying the Following link on other users profile page. 33506: THOR-1020: people visibility 33509: Reduced remote calls required to build user profile page. 33519: THOR-985, THOR-986 33542: THOR-989 - Added extensibility hooks to People Finder component 33558: THOR-1014: Profile visibility -1st cut for THOR-993 (/api/people) 33564: THOR-1014: Profile visibility - THOR-992 (/webframework/content/metadata?user=) 33569: Implemented THOR-985, THOR-986, THOR-989 33572: Performance improvement to remove the need for a share->repo call for each page or ajax request to resolve account class name. 33579: THOR-1020: cloud people API (re: visibility) 33599: Share Thor performance improvements - removed the need to call /internal/cloud/current-user inside various common components - now using cached data in user object. 33608: THOR-1014: Profile visibility - fix PeopleRestApiTest 33625: THOR-1020: people visibility 33632: THOR-984 - Hide Account Settings screen from External Users. 33636: THOR-1014: profile visibility 33670: THOR-1020/THOR-1014: people/profile visibility 33674: THOR-1047: Privacy REST - subscriptions (follower) API 33688: THOR-1047: Privacy REST - subscriptions (follower) API 33696: Fix for THOR-785 "F272: API call to get the number of accounts" 33698: THOR-1033: Fixed free accounts showing console settings (updated Spring Surf libs) 33700: Committed other Surf updates missing from r33698 (WebScript JARs) to ensure that manifest meta-data isn't misleading 33705: THOR-1052: VersionService: ensureVersioningEnabled 33706: Merged BRANCHES/DEV/THOR1_UPLOADLIMITS to BRANCHES/DEV/THOR1_SPRINTS: 33510: (RECORD ONLY) File Upload Limit enhancements 33656: Account Quotas / File Upload limit pt1 33686: Modified dnd-upload and html-upload WebScripts to retrieve maximum upload size from internal service (and refactored core WebScripts to support override) 33710: THOR-1020: Privacy (People REST API) 33713: THOR-1020: Privacy (People REST API) 33718: THOR-1020: Privacy (People REST API) 33722: Thor JMeter test script 33742: Latest SpringSurf libs - performance improvements and concurrency fixes 35930: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33764: THOR-1021: F287: Account Types can define file size upload limits for the Account which are set when the account is upgraded/downgraded between Account types 33767: Tweaked HTML5 upload dialog so that error messages are handled gracefully 33785: Resolve THOR-457: Already have an account email template needs updating/fixing 33786: Fix to issue spotted by DaveC where switching networks would not correctly refresh user metadata. Also fixed minor encoding issues in related Networks WebScripts. 33792: Merged BRANCHES/DEV/THOR1_PUBLIC_EMAIL to BRANCHES/DEV/THOR1_SPRINTS: 33490: Initial feature branch 33535: Fix to unreported issue whereby DirectoryService.getDefaultAccount returns the home account. 33547: THOR-176. Invite new user (public domain email address) into site. Part 1. 33592: Further work for THOR-176. user metadata REST API now does not return homeTenant if the user is from a public email domain. This conditional removal of the 'homeTenant' JSON property is needed by Share. 33593: THOR-176. Refactor of UserTenant to use AccountClass to check isPublicEmailDomain. 33620: Fix for NPE in UserTenant.isPublicDomainUser(). The admin user has no account-type. 33627: Share updates for public e-mail 33797: THOR-176: consolidate public domain check 33802: Remove temporary option to use double @ login (as per THOR-156) - no longer required 33804: Removed locale from the cachekey used for tenant page view cache. 33810: Merged BRANCHES/DEV/THOR1_BLACKLIST to BRANCHES/DEV/THOR1_SPRINTS: 33709: Blacklist CRUD: THOR-974, THOR-975, THOR-976, THOR-977, THOR-978 (Part 1 - DAO layer) 33711: Blacklist CRUD: THOR-974, THOR-975, THOR-976, THOR-977, THOR-978 (Part 2 - Foundation Service layer) 33747: THOR-974, THOR-975, THOR-976, THOR-977 and THOR-978. REST API for CRUD of blacklisted email domains. 33809: THOR-974, THOR-975, THOR-976, THOR-977, THOR-978 Adding REST-client rcq files for blacklist CRUD. 35933: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33814: Merged BRANCHES/DEV/THOR1_ACCOUNT_SETTINGS to BRANCHES/DEV/THOR1_SPRINTS: 33411: (RECORD ONLY) Thor account settings branch from Thor1_Sprints. 33607: Initial checkin for THOR-972, THOR-971, THOR-410 33621: THOR-972, THOR-971, THOR-410 - added missing files 33639: THOR-971, THOR-972, THOR-410: - add paging properties to the list people web script - default network admin to true and internal to null 33641: Second cut of THOR-964 "F173: Network admin can list users of network (with paging support)" 33642: THOR-972, THOR-971, THOR-410 - changed paging properties in list users 33652: Implemented THOR-964,THOR-965,THOR-965,THOR-966,THOR-967,THOR-968,THOR-969 33653: User action click event now stopped so it doesn't modify the url 33671: THOR-971: webscript implementation 33687: Making premote/demote available for network-admins and not only for admins. check for network admin role already exists in the service code. 33694: THOR-971: - Added analytics handling - Fixed invite share url to include tenant 33699: THOR-963 "F17: Network admin can add a one or more internal users to their network" 33702: THOR-971: - changed the bulk create url - changed the activate email template 33703: Make sure Java-based network admin scripts set the response status to 401 if the authenticated user is not a network admin 33737: THOR-410: - more unit tests - tidy up 33744: (RECORD ONLY) Merged BRANCHES/DEV/THOR1_USER_MANAGEMENT to BRANCHES/DEV/THOR1_ACCOUNT_SETTINGS: 33417: Thor JMeter test script tweaks 33420: THOR-1000: Solr tracking: NodeContentGet should not create (empty) temp file if there is no transformer (eg. for image node) 33434: The <#if> statement & <script> element for google analytics wasn't in sync, causing a closing </script> element always being printed. 33440: Latest SpringSurf libs - performance and thread safety improvements. 33458: ThorTest-preReg (JMeter) test update 33460: Latest SpringSurf libs: 33466: THOR-1002: Updated enterprise overlay 33480: Latest SpringSurf libs - Surf performance improvements from Thor high load profiling in Jmeter/Jprofiler 33493: THOR-979: HTML5 upload support 33505: THOR-983: Preload images, JS and CSS for basic dashboards, document library and document details (from login page) 33518: THOR-979: HTML5 upload tweaks (upload doesn't start automatically when updating to give opportunity to set version type and add comment 33520: THOR-900: Modified header.get.html.ftl to ensure that user name is URL encoded (so that the "@" symbol in the user name becomes "%40" to ensure that timeout redirects work) 33527: THOR-1027: Header Alfresco image now links back to application context and about dialog is now linked from footer Alfresco image 33551: THOR-1007: Fixed upload hang on FireFox when uploading folders 33556: Merged BRANCHES/DEV/THOR1_INVITATION to BRANCHES/DEV/THOR1_SPRINTS: 33386: Branch for Invitation enhancements 33474: THOR-1006. Part 1. Services-level changes to support invitation enhancements. 33475: THOR-1006. Documentation on the desc.xml. 33476: THOR-1006. Commenting out some unfinished code to avoid any unwanted side-effects. Still to do: get the authentication check working and ensure no unexpected side-effects. 33483: THOR-1006. Completion of basic services changes to support 'accept invitation on alternate email'. Note! The authentication of the alternative email's password is NOT YET IMPLEMENTED due to a repo dependency. This MUST be implemented before merge to THOR1_SPRINTS. I'll create a new JIRA. 33511: Fix for THOR-1017. 33525: THOR-1017. Slight improvement to desc.xml doc. HTTP status codes in response. 33529: Fixed THOR-980 & THOR-1024 & THOR-1025 33553: Fixed THOR-980 "F14: Allow users to login using existing email address if invite is sent to wrong email address and they already have an account" 33559: Initial feature branch 33669: First cut of THOR-994 and THOR-995 - remove internal and external user from network. 33685: Making remove-external-user.delete.desc.xml accept a domainName templateArg as well as the existing accountId. 33716: THOR-994 and THOR-995. Remove user from network. Addressing some review comments from DaveC. Refactoring mostly. Also added protection on RegistrationService.deleteUser() to prevent deletion of last NetworkAdmin in network. 33745: Adding NetworkdAdmin protection to the remove-external-user.delete webscript. 33752: Additional fixes for THOR-966 & THOR-969 * Improved messages/dialogs: demoting yourself, demoting last admin, removing last admin * New User Button align layout fix as requested by Imran 33754: Documentation for the remove-external-user.delete webscript. 33756: THOR-410: - unit test tidy 33766: Fix for signup link when already logged in as another user 33769: THOR-963 "F17: Network admin can add a one or more internal users to their network" 33770: Ensure a 403 is returned (rather than 500) when attempt is made to remove last NetworkAdmin in a tenant. 33774: Added padding on top of name for the Manage Users screen as requested by ux 33790: Promote/demote icons from Imran 33815: Fix issue with removal of public email user from last invited network 33817: THOR-1060: Activities Feed - perf tweak to halve the number of generated feed entries 33819: THOR-1060: fix ActivitiesFeed subsystem (re-)name 33820: Merged BRANCHES/DEV/THOR1_ACCOUNT_SETTINGS to BRANCHES/DEV/THOR1_SPRINTS: 33756: THOR-410: - unit test tidy 33766: Fix for signup link when already logged in as another user 33769: THOR-963 "F17: Network admin can add a one or more internal users to their network" 33770: Ensure a 403 is returned (rather than 500) when attempt is made to remove last NetworkAdmin in a tenant. 33774: Added padding on top of name for the Manage Users screen as requested by ux 33790: Promote/demote icons from Imran 33825: People REST API 35934: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 33850: (RECORD ONLY) Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/THOR1_SPRINTS 33843: Fix for ALF-12775 33866: Fix for THOR-1071 33878: Fix the build 33881: THOR-1069: Ensure that invitations can be accepted when a user is already logged in 33882: Resolve THOR-1082: Possible to register email address with invalid domain (according to our tenant id rules) 33883: Resolve THOR-1070: External user's avatar not displayed on the People Finder page. 33884: Fix solrcore.properties 33899: Resolve THOR-1077: Incorrect free space displayed when uploading files which exceeds quota 33922: Resolve THOR-1079: Incorrect behavior of the button "Save and close" to "Send Document (s) For Review" tasks. 33933: Resolve THOR-1088: Hide Account Id from Account Summary Screen 33934: Resolve THOR-1089: Review Account Quota text on Account Summary Screen 33942: Fix for THOR-1094. InvalidDomains FTL couldn't handle NULL notes field. This shouldn't arise in the field as we don't put NULL-valued notes in the DB, but it might matter in some test envs. 33949: Resolve THOR-1093: Incorrect notification title displayed when trying to invite user from another network from Manage Users page 33953: Fix for THOR issue where public users should not be able to see Following and Following Me tabs in their own profile. 35954: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 34140: THOR-1098: Prevent resources being requested twice (latest Surf libs) 34153: Minor: THOR-5: MT-aware immutable singletons (spp/vti) 34161: Prevent session timeout redirect problem resulting from clicking user link in activities feed 35960: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 34224: Fix for THOR-789 - Mix of languages. The original bug was not never actually completely fixed, added some additional handling in SpringSurf WebScripts View to not override the locale from the original request parameters if it has already been set elsewhere. 34301: (RECORD ONLY) Merged BRANCHES/DEV/V4.0-BUG-FIX to BRANCHES/DEV/THOR1_SPRINTS: 34279: NodeDAO: re-parent "lost & found" orphan child nodes (see ALF-12358 & ALF-13066 / SYS-301) 34343: (RECORD ONLY) Merged BRANCHES/DEV/V4.0-BUG-FIX to BRANCHES/DEV/THOR1_SPRINTS: 34338: NodeDAO: re-parent "lost & found" orphan child nodes (see ALF-12358 & ALF-13066 / SYS-301) - test fix 34341: NodeDAO: re-parent "lost & found" orphan child nodes (see ALF-12358 & ALF-13066 / SYS-301) - test fix 34388: THOR-953/SYS-294: add db.pool.evict.num.tests option (=> numTestsPerEvictionRun) 34729: (RECORD ONLY) Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/THOR1_SPRINTS: 31867: Merged DEV/TEMPORARY to V3.4-BUG-FIX 31400: ALF-10764: PDF vs 1.5 cause crash jvm - PDFRenderer library has been updated from 2009-09-27 to 0.9.1 version to support PDF documents of 1.5 version 32061: ALF-11376 Requesting PDFBox 1.6 be included in future service pack release. Upgrading pdfbox,fontbox,jempbox from 1.5.0 to 1.6.0 34731: THOR-1261: repo cluster fix (propertyUniqueContextCache) 34734: THOR-1261: repo cluster fix (propertyUniqueContextCache) 34435: Merged BRANCHES/DEV/V4.0-BUG-FIX to BRANCHES/DEV/THOR1_SPRINTS: 34434: ALF-13066: Fix for intermittent failure (testConcurrentLinkToDeletedNode) 35961: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 34558: THOR-1216: tenant context mismatch (Solr tracking) 34606: THOR-1216: tenant context mismatch 34441: (RECORD ONLY) Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/THOR1_SPRINTS 33285: Fix for ALF-12336 - Share loses performance if noncachableObjectTypes are defined (page & component) 34489: Fix to remove hazelcast subdir from build.xml for -exploded build - Thor specific merge issue. 34722: Added hazelcast-cloud jar to allow AWS Hazelcast config options for Share clustering on Thor 34848: THOR - specific version of ClusterAwarePathStoreObjectPersister. 34931: Thor specific lookup of Share custom app context files to include the custom-slingshot-cloud-context and custom-slingshot-application-context only and in the order we want. Also updated Hazelcast example config to include AWS by default 35962: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 34940: THOR-1288: Extra diagnostics for tracking leaked tenant context on thread: 34187: Updated Surf libs (down grades duplicate dependency warnings to debug info) 34410: THOR-1169: Latest Spring Surf libs to fix missing template CSS probs 34418: (RECORD ONLY) Merged BRANCHES/DEV/BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/BRANCHES/DEV/THOR1_SPRINTS 34316: Method signature change to ConfigService fixes for RepoXMLConfigService 34471: (RECORD ONLY) Merged BRANCHES/V4.0 to BRANCHES/DEV/THOR1_SPRINTS 34468: Fix for ALF-13172 Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/V4.0 34467: Fix for ALF-13237 - Change dashboard Layout is not working correctly, original layout is still used after saving changes. 34891: Added missing jug-asl-2.0.0.jar to slingshot deps for Thor 35963: Merged BRANCHES/DEV/THOR1_SPRINTS to BRANCHES/DEV/CLOUD1: 35087: Minor: remove NOOP (introduced in r30776) 35123: THOR-1288: update leak logger 35124: THOR-1288: prod login failure when using cloud console for (bulk) signups 35132: THOR-1288: build/test fix 35133: THOR-1288: build/test fix 35395: Resolve THOR-1340: Alberto.Vazquez@w.illi.am cannot sign up 35964: Spring Surf library refresh 35995: Fix merge issue 35999: Fix merge issue 36053: 1st pass at upgrading to latest Spring Surf 36059: Fix CloudInvitationService tests for cloud1 48191: Merged BRANCHES/DEV/CONV_V413 to BRANCHES/DEV/CONV_HEAD: 47185: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: Merged BRANCHES/DEV/CLOUD2 to BRANCHES/DEV/CONV_V413: MT - enable ability to get call context if overriding of beginCall/afterCall - eg. for cloud use-case (x-network switching) 48192: Temporarily disable generation of installers, to speed up build git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@48255 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
4891 lines
190 KiB
Java
4891 lines
190 KiB
Java
/*
|
|
* Copyright (C) 2005-2013 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 java.util.concurrent.locks.ReadWriteLock;
|
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
|
|
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.node.index.NodeIndexer;
|
|
import org.alfresco.repo.policy.BehaviourFilter;
|
|
import org.alfresco.repo.security.permissions.AccessControlListProperties;
|
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
|
|
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
|
|
import org.alfresco.repo.transaction.TransactionAwareSingleton;
|
|
import org.alfresco.repo.transaction.TransactionListenerAdapter;
|
|
import org.alfresco.repo.transaction.TransactionalDao;
|
|
import org.alfresco.repo.transaction.TransactionalResourceHelper;
|
|
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.NodeRef.Status;
|
|
import org.alfresco.service.cmr.repository.Path;
|
|
import org.alfresco.service.cmr.repository.StoreRef;
|
|
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.EqualsHelper.MapValueComparison;
|
|
import org.alfresco.util.GUID;
|
|
import org.alfresco.util.Pair;
|
|
import org.alfresco.util.PropertyCheck;
|
|
import org.alfresco.util.ReadWriteLockExecuter;
|
|
import org.alfresco.util.ValueProtectingMap;
|
|
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 KEY_LOST_NODE_PAIRS = AbstractNodeDAOImpl.class.getName() + ".lostNodePairs";
|
|
private static final String KEY_DELETED_ASSOCS = AbstractNodeDAOImpl.class.getName() + ".deletedAssocs";
|
|
|
|
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;
|
|
private NodeIndexer nodeIndexer;
|
|
|
|
/**
|
|
* 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<QName><br/>
|
|
* VALUE KEY: None<br/>
|
|
*/
|
|
private EntityLookupCache<NodeVersionKey, Set<QName>, Serializable> aspectsCache;
|
|
/**
|
|
* Cache for the Node properties:<br/>
|
|
* KEY: NodeVersionKey<br/>
|
|
* VALUE: Map<QName, Serializable><br/>
|
|
* VALUE KEY: None<br/>
|
|
*/
|
|
private EntityLookupCache<NodeVersionKey, Map<QName, Serializable>, Serializable> propertiesCache;
|
|
/**
|
|
* Non-clustered cache for the Node parent assocs:<br/>
|
|
* KEY: (nodeId, txnId) pair <br/>
|
|
* VALUE: ParentAssocs
|
|
*/
|
|
private ParentAssocsCache parentAssocsCache;
|
|
private int parentAssocsCacheSize;
|
|
private int parentAssocsCacheLimitFactor = 8;
|
|
|
|
/**
|
|
* 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());
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param nodeIndexer used when making changes that affect indexes
|
|
*/
|
|
public void setNodeIndexer(NodeIndexer nodeIndexer)
|
|
{
|
|
this.nodeIndexer = nodeIndexer;
|
|
}
|
|
|
|
/**
|
|
* 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());
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum capacity of the parent assocs cache
|
|
*
|
|
* @param parentAssocsCacheSize the cache size
|
|
*/
|
|
public void setParentAssocsCacheSize(int parentAssocsCacheSize)
|
|
{
|
|
this.parentAssocsCacheSize = parentAssocsCacheSize;
|
|
}
|
|
|
|
/**
|
|
* Sets the average number of parents expected per cache entry. This parameter is multiplied by the
|
|
* {@link #setParentAssocsCacheSize(int)} parameter to compute a limit on the total number of cached parents, which
|
|
* will be proportional to the cache's memory usage. The cache will be pruned when this limit is exceeded to avoid
|
|
* excessive memory usage.
|
|
*
|
|
* @param parentAssocsCacheLimitFactor
|
|
* the parentAssocsCacheLimitFactor to set
|
|
*/
|
|
public void setParentAssocsCacheLimitFactor(int parentAssocsCacheLimitFactor)
|
|
{
|
|
this.parentAssocsCacheLimitFactor = parentAssocsCacheLimitFactor;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
PropertyCheck.mandatory(this, "nodeIndexer", nodeIndexer);
|
|
|
|
this.nodePropertyHelper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO);
|
|
this.parentAssocsCache = new ParentAssocsCache(this.parentAssocsCacheSize, this.parentAssocsCacheLimitFactor);
|
|
}
|
|
|
|
/*
|
|
* 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
|
|
*/
|
|
protected 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)
|
|
{
|
|
invalidateNodeCaches(node, true, true, true);
|
|
}
|
|
// Finally remove the node reference
|
|
nodesCache.removeByKey(nodeId);
|
|
}
|
|
|
|
/**
|
|
* Invalidate specific node caches using an exact key
|
|
*
|
|
* @param node the node in question
|
|
*/
|
|
private void invalidateNodeCaches(Node node, boolean invalidateNodeAspectsCache,
|
|
boolean invalidateNodePropertiesCache, boolean invalidateParentAssocsCache)
|
|
{
|
|
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
|
|
if (invalidateNodeAspectsCache)
|
|
{
|
|
aspectsCache.removeByKey(nodeVersionKey);
|
|
}
|
|
if (invalidateNodePropertiesCache)
|
|
{
|
|
propertiesCache.removeByKey(nodeVersionKey);
|
|
}
|
|
if (invalidateParentAssocsCache)
|
|
{
|
|
invalidateParentAssocsCached(node);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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 implements TransactionalDao
|
|
{
|
|
/**
|
|
* Checks for the presence of a written DB transaction entry
|
|
*/
|
|
@Override
|
|
public boolean isDirty()
|
|
{
|
|
Long txnId = AbstractNodeDAOImpl.this.getCurrentTransactionId(false);
|
|
return txnId != null;
|
|
}
|
|
|
|
@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.bindDaoService(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
|
|
*/
|
|
|
|
@Override
|
|
public Pair<Long, StoreRef> getStore(StoreRef storeRef)
|
|
{
|
|
Pair<StoreRef, Node> rootNodePair = rootNodesCache.getByKey(storeRef);
|
|
if (rootNodePair == null)
|
|
{
|
|
throw new InvalidStoreRefException(storeRef);
|
|
}
|
|
else
|
|
{
|
|
return new Pair<Long, StoreRef>(rootNodePair.getSecond().getStore().getId(), rootNodePair.getFirst());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
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, 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);
|
|
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);
|
|
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(qnameDAO);
|
|
}
|
|
|
|
public boolean exists(NodeRef nodeRef)
|
|
{
|
|
NodeEntity node = new NodeEntity(nodeRef);
|
|
Pair<Long, Node> pair = nodesCache.getByValue(node);
|
|
return pair != null && !pair.getSecond().getDeleted(qnameDAO);
|
|
}
|
|
|
|
@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, false);
|
|
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(qnameDAO);
|
|
}
|
|
}
|
|
|
|
public Status getNodeIdStatus(Long nodeId)
|
|
{
|
|
Pair<Long, Node> nodePair = nodesCache.getByKey(nodeId);
|
|
// The nodesCache gets both live and deleted nodes.
|
|
if (nodePair == null)
|
|
{
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
return nodePair.getSecond().getNodeStatus(qnameDAO);
|
|
}
|
|
}
|
|
|
|
public Pair<Long, NodeRef> getNodePair(NodeRef nodeRef)
|
|
{
|
|
NodeEntity node = new NodeEntity(nodeRef);
|
|
Pair<Long, Node> pair = nodesCache.getByValue(node);
|
|
// Check it
|
|
if (pair == null || pair.getSecond().getDeleted(qnameDAO))
|
|
{
|
|
// The cache says that the node is not there or is deleted.
|
|
// We double check by going to the DB
|
|
Node dbNode = selectNodeByNodeRef(nodeRef);
|
|
if (dbNode == null || dbNode.getDeleted(qnameDAO))
|
|
{
|
|
// The DB agrees. This is an invalid noderef.
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
// The cache was wrong, possibly due to it caching negative results earlier.
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Repairing stale cache entry for node: " + nodeRef);
|
|
}
|
|
Long nodeId = dbNode.getId();
|
|
invalidateNodeCaches(nodeId);
|
|
nodesCache.setValue(nodeId, dbNode);
|
|
return dbNode.getNodePair();
|
|
}
|
|
}
|
|
return pair.getSecond().getNodePair();
|
|
}
|
|
|
|
/**
|
|
* Trigger a post transaction prune of any associations that point to this deleted one.
|
|
* @param nodeId
|
|
*/
|
|
private void pruneDanglingAssocs(Long nodeId)
|
|
{
|
|
selectChildAssocs(nodeId, null, null, null, null, null, new ChildAssocRefQueryCallback()
|
|
{
|
|
@Override
|
|
public boolean preLoadNodes()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair, Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
bindFixAssocAndCollectLostAndFound(childNodePair, "childNodeWithDeletedParent", childAssocPair.getFirst(), childAssocPair.getSecond().isPrimary());
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void done()
|
|
{
|
|
}
|
|
});
|
|
selectParentAssocs(nodeId, null, null, null, new ChildAssocRefQueryCallback()
|
|
{
|
|
@Override
|
|
public boolean preLoadNodes()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair, Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
bindFixAssocAndCollectLostAndFound(childNodePair, "deletedChildWithParents", childAssocPair.getFirst(), false);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void done()
|
|
{
|
|
}
|
|
});
|
|
}
|
|
|
|
public Pair<Long, NodeRef> getNodePair(Long nodeId)
|
|
{
|
|
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
|
|
// Check it
|
|
if (pair == null || pair.getSecond().getDeleted(qnameDAO))
|
|
{
|
|
// The cache says that the node is not there or is deleted.
|
|
// We double check by going to the DB
|
|
Node dbNode = selectNodeById(nodeId);
|
|
if (dbNode == null || dbNode.getDeleted(qnameDAO))
|
|
{
|
|
// The DB agrees. This is an invalid noderef.
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
// The cache was wrong, possibly due to it caching negative results earlier.
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Repairing stale cache entry for node: " + nodeId);
|
|
}
|
|
invalidateNodeCaches(nodeId);
|
|
nodesCache.setValue(nodeId, dbNode);
|
|
return dbNode.getNodePair();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return pair.getSecond().getNodePair();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a node instance regardless of whether it is considered <b>live</b> or <b>deleted</b>
|
|
*
|
|
* @param nodeId the node ID to look for
|
|
* @param liveOnly <tt>true</tt> to ensure that only <b>live</b> nodes are retrieved
|
|
* @return a node that will be <b>live</b> if requested
|
|
* @throws ConcurrencyFailureException if a valid node is not found
|
|
*/
|
|
private Node getNodeNotNull(Long nodeId, boolean liveOnly)
|
|
{
|
|
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
|
|
|
|
if (pair == null)
|
|
{
|
|
// The node has no entry in the database
|
|
NodeEntity dbNode = selectNodeById(nodeId);
|
|
nodesCache.removeByKey(nodeId);
|
|
throw new ConcurrencyFailureException(
|
|
"No node row exists: \n" +
|
|
" ID: " + nodeId + "\n" +
|
|
" DB row: " + dbNode);
|
|
}
|
|
else if (pair.getSecond().getDeleted(qnameDAO) && liveOnly)
|
|
{
|
|
// The node is not 'live' as was requested
|
|
NodeEntity dbNode = selectNodeById(nodeId);
|
|
nodesCache.removeByKey(nodeId);
|
|
// Make absolutely sure that the node is not referenced by any associations
|
|
pruneDanglingAssocs(nodeId);
|
|
// Force a retry on the transaction
|
|
throw new ConcurrencyFailureException(
|
|
"No live node exists: \n" +
|
|
" ID: " + nodeId + "\n" +
|
|
" DB row: " + dbNode);
|
|
}
|
|
else
|
|
{
|
|
return pair.getSecond();
|
|
}
|
|
}
|
|
|
|
public QName getNodeType(Long nodeId)
|
|
{
|
|
Node node = getNodeNotNull(nodeId, false);
|
|
Long nodeTypeQNameId = node.getTypeQNameId();
|
|
return qnameDAO.getQName(nodeTypeQNameId).getSecond();
|
|
}
|
|
|
|
public Long getNodeAclId(Long nodeId)
|
|
{
|
|
Node node = getNodeNotNull(nodeId, true);
|
|
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, true);
|
|
// Find an initial ACL for the node
|
|
Long parentAclId = parentNode.getAclId();
|
|
AccessControlListProperties inheritedAcl = null;
|
|
Long childAclId = null;
|
|
if (parentAclId != null)
|
|
{
|
|
try
|
|
{
|
|
Long inheritedACL = aclDAO.getInheritedAccessControlList(parentAclId);
|
|
inheritedAcl = aclDAO.getAccessControlListProperties(inheritedACL);
|
|
if (inheritedAcl != null)
|
|
{
|
|
childAclId = inheritedAcl.getId();
|
|
}
|
|
}
|
|
catch (RuntimeException 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);
|
|
// Rethrow for a retry (ALF-17286)
|
|
throw new RuntimeException(
|
|
"Failure while 'getting' inherited ACL or ACL properties: \n" +
|
|
" parent ACL ID: " + parentAclId + "\n" +
|
|
" inheritied ACL: " + inheritedAcl,
|
|
e);
|
|
}
|
|
}
|
|
// 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, 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, false);
|
|
|
|
// 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
|
|
* @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,
|
|
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);
|
|
// 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();
|
|
Node dbTargetNode = selectNodeByNodeRef(targetNodeRef);
|
|
if (dbTargetNode == null)
|
|
{
|
|
// There does not appear to be any row that could prevent an insert
|
|
throw new AlfrescoRuntimeException("Failed to insert new node: " + node, e);
|
|
}
|
|
else if (dbTargetNode.getDeleted(qnameDAO))
|
|
{
|
|
Long dbTargetNodeId = dbTargetNode.getId();
|
|
// This is OK. It happens when we create a node that existed in the past.
|
|
// Remove the row completely
|
|
deleteNodeProperties(dbTargetNodeId, (Set<Long>) null);
|
|
deleteNodeById(dbTargetNodeId);
|
|
// Now repeat the insert but let any further problems just be thrown out
|
|
id = insertNode(node);
|
|
}
|
|
else
|
|
{
|
|
// A live node exists.
|
|
throw new NodeExistsException(dbTargetNode.getNodePair(), e);
|
|
}
|
|
}
|
|
node.setId(id);
|
|
|
|
Set<QName> nodeAspects = null;
|
|
if (addAuditableAspect)
|
|
{
|
|
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, true);
|
|
final StoreEntity newParentStore = newParentNode.getStore();
|
|
final Node childNode = getNodeNotNull(childNodeId, true);
|
|
final StoreEntity childStore = childNode.getStore();
|
|
final ChildAssocEntity primaryParentAssoc = getPrimaryParentAssocImpl(childNodeId);
|
|
final Long oldParentAclId;
|
|
final Long oldParentNodeId;
|
|
if (primaryParentAssoc == null)
|
|
{
|
|
oldParentAclId = null;
|
|
oldParentNodeId = null;
|
|
}
|
|
else
|
|
{
|
|
if (primaryParentAssoc.getParentNode() == null)
|
|
{
|
|
oldParentAclId = null;
|
|
oldParentNodeId = null;
|
|
}
|
|
else
|
|
{
|
|
oldParentNodeId = primaryParentAssoc.getParentNode().getId();
|
|
oldParentAclId = getNodeNotNull(oldParentNodeId, true).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(),
|
|
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);
|
|
// Completely delete the original node but keep the ACL as it's reused
|
|
deleteNodeImpl(childNodeId, false);
|
|
}
|
|
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);
|
|
// Ensure we invalidate the name cache (the child version key might not have been 'bumped' by the last
|
|
// 'touch')
|
|
if (updated > 0 && primaryParentAssoc != null)
|
|
{
|
|
Pair<Long, QName> oldTypeQnamePair = qnameDAO.getQName(
|
|
primaryParentAssoc.getTypeQNameId());
|
|
if (oldTypeQnamePair != null)
|
|
{
|
|
childByNameCache.remove(new ChildByNameKey(oldParentNodeId, oldTypeQnamePair.getSecond(),
|
|
primaryParentAssoc.getChildNodeName()));
|
|
}
|
|
}
|
|
return updated;
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
controlDAO.rollbackToSavepoint(savepoint);
|
|
// DuplicateChildNodeNameException implements DoNotRetryException.
|
|
// There are some cases - FK violations, specifically - where we DO actually want to retry.
|
|
// Detecting this is done by looking for the related FK names, 'fk_alf_cass_*' in the error message
|
|
String lowerMsg = e.getMessage().toLowerCase();
|
|
if (lowerMsg.contains("fk_alf_cass_"))
|
|
{
|
|
throw new ConcurrencyFailureException("FK violation updating primary parent association for " + childNodeId, e);
|
|
}
|
|
// We assume that this is from the child cm:name constraint violation
|
|
throw new DuplicateChildNodeNameException(
|
|
newParentNode.getNodeRef(),
|
|
assocTypeQName,
|
|
childNodeName,
|
|
e);
|
|
}
|
|
}
|
|
};
|
|
childAssocRetryingHelper.doWithRetry(callback);
|
|
|
|
// Optimize for rename case
|
|
if (!EqualsHelper.nullSafeEquals(newParentNodeId, oldParentNodeId))
|
|
{
|
|
// 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, true);
|
|
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);
|
|
}
|
|
|
|
|
|
@Override
|
|
public int touchNodes(Long txnId, List<Long> nodeIds)
|
|
{
|
|
// limit in clause to 1000 node ids
|
|
int batchSize = 1000;
|
|
|
|
int touched = 0;
|
|
ArrayList<Long> batch = new ArrayList<Long>(batchSize);
|
|
for(Long nodeId : nodeIds)
|
|
{
|
|
invalidateNodeCaches(nodeId);
|
|
batch.add(nodeId);
|
|
if(batch.size() % batchSize == 0)
|
|
{
|
|
touched += updateNodes(txnId, batch);
|
|
batch.clear();
|
|
}
|
|
}
|
|
if(batch.size() > 0)
|
|
{
|
|
touched += updateNodes(txnId, batch);
|
|
}
|
|
return touched;
|
|
}
|
|
|
|
/**
|
|
* 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, false);
|
|
}
|
|
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, false);
|
|
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)
|
|
{
|
|
// Because we cache parent assocs by transaction, we must manually invalidate on this version change
|
|
invalidateParentAssocsCached(node);
|
|
}
|
|
else
|
|
{
|
|
copyParentAssocsCached(node);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The node was not touched. By definition it MUST be in the current transaction.
|
|
// We invalidate the caches as specifically requested
|
|
invalidateNodeCaches(
|
|
node,
|
|
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());
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// ALF-16366: Ensure index impact is accounted for. If the node is being deleted we would expect the
|
|
// appropriate events to be fired manually
|
|
if (!nodeUpdate.isUpdateTypeQNameId() || !getNodeNotNull(nodeId, false).getDeleted(qnameDAO))
|
|
{
|
|
nodeIndexer.indexUpdateNode(oldNode.getNodeRef());
|
|
}
|
|
|
|
// 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, true);
|
|
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);
|
|
}
|
|
|
|
@Override
|
|
public void deleteNode(Long nodeId)
|
|
{
|
|
// Delete and take the ACLs to the grave
|
|
deleteNodeImpl(nodeId, true);
|
|
}
|
|
|
|
/**
|
|
* Physical deletion of the node
|
|
*
|
|
* @param nodeId the node to delete
|
|
* @param deleteAcl <tt>true</tt> to delete any associated ACLs otherwise
|
|
* <tt>false</tt> if the ACLs get reused elsewhere
|
|
*/
|
|
private void deleteNodeImpl(Long nodeId, boolean deleteAcl)
|
|
{
|
|
Node node = getNodeNotNull(nodeId, true);
|
|
// Gather data for later
|
|
Long aclId = node.getAclId();
|
|
Set<QName> nodeAspects = getNodeAspects(nodeId);
|
|
|
|
// 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 child associations (invalidate children)
|
|
invalidateNodeChildrenCaches(nodeId, true, true);
|
|
invalidateNodeChildrenCaches(nodeId, false, true);
|
|
|
|
// Remove aspects
|
|
deleteNodeAspects(nodeId, null);
|
|
|
|
// Remove properties
|
|
deleteNodeProperties(nodeId, (Set<Long>) null);
|
|
|
|
// Remove subscriptions
|
|
deleteSubscriptions(nodeId);
|
|
|
|
// Delete the row completely:
|
|
// ALF-12358: Concurrency: Possible to create association references to deleted nodes
|
|
// There will be no way that any references can be made to a deleted node because we
|
|
// are really going to delete it. However, for tracking purposes we need to maintain
|
|
// a list of nodes deleted in the transaction. We store that information against a
|
|
// new node of type 'sys:deleted'. This means that 'deleted' nodes are really just
|
|
// orphaned (read standalone) nodes that remain invisible outside of the DAO.
|
|
int deleted = deleteNodeById(nodeId);
|
|
// We will always have to invalidate the cache for the node
|
|
invalidateNodeCaches(nodeId);
|
|
// Concurrency check
|
|
if (deleted != 1)
|
|
{
|
|
// We thought that the row existed
|
|
throw new ConcurrencyFailureException(
|
|
"Failed to delete node: \n" +
|
|
" Node: " + node);
|
|
}
|
|
|
|
// Remove ACLs
|
|
if (deleteAcl && aclId != null)
|
|
{
|
|
aclDAO.deleteAclForNode(aclId, false);
|
|
}
|
|
|
|
// The node has been cleaned up. Now we recreate the node for index tracking purposes.
|
|
// Use a 'deleted' type QName
|
|
StoreEntity store = node.getStore();
|
|
String uuid = node.getUuid();
|
|
Long deletedQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_DELETED).getFirst();
|
|
Long defaultLocaleId = localeDAO.getOrCreateDefaultLocalePair().getFirst();
|
|
Node deletedNode = newNodeImpl(store, uuid, deletedQNameId, defaultLocaleId, null, null);
|
|
Long deletedNodeId = deletedNode.getId();
|
|
// Store the original ID as a property
|
|
Map<QName, Serializable> trackingProps = Collections.singletonMap(ContentModel.PROP_ORIGINAL_ID, (Serializable) nodeId);
|
|
setNodePropertiesImpl(deletedNodeId, trackingProps, true);
|
|
}
|
|
|
|
@Override
|
|
public int purgeNodes(long maxTxnCommitTimeMs)
|
|
{
|
|
return deleteNodesByCommitTime(maxTxnCommitTimeMs);
|
|
}
|
|
|
|
/*
|
|
* Node Properties
|
|
*/
|
|
|
|
public Map<QName, Serializable> getNodeProperties(Long nodeId)
|
|
{
|
|
Map<QName, Serializable> props = getNodePropertiesCached(nodeId);
|
|
// Create a shallow copy to allow additions
|
|
props = new HashMap<QName, Serializable>(props);
|
|
|
|
Node node = getNodeNotNull(nodeId, false);
|
|
// 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());
|
|
}
|
|
|
|
// Wrap to ensure that we only clone values if the client attempts to modify
|
|
// the map or retrieve values that might, themselves, be mutable
|
|
props = new ValueProtectingMap<QName, Serializable>(props, NodePropertyValue.IMMUTABLE_CLASSES);
|
|
|
|
// 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, false);
|
|
AuditablePropertiesEntity auditableProperties = node.getAuditableProperties();
|
|
if (auditableProperties != null)
|
|
{
|
|
value = auditableProperties.getAuditableProperty(propertyQName);
|
|
}
|
|
}
|
|
else if (ReferenceablePropertiesEntity.isReferenceableProperty(propertyQName)) // sys:referenceable
|
|
{
|
|
Node node = getNodeNotNull(nodeId, false);
|
|
value = ReferenceablePropertiesEntity.getReferenceableProperty(node, propertyQName);
|
|
}
|
|
else if (LocalizedPropertiesEntity.isLocalizedProperty(propertyQName)) // sys:localized
|
|
{
|
|
Node node = getNodeNotNull(nodeId, false);
|
|
value = LocalizedPropertiesEntity.getLocalizedProperty(localeDAO, node, propertyQName);
|
|
}
|
|
else
|
|
{
|
|
Map<QName, Serializable> props = getNodePropertiesCached(nodeId);
|
|
// Wrap to ensure that we only clone values if the client attempts to modify
|
|
// the map or retrieve values that might, themselves, be mutable
|
|
props = new ValueProtectingMap<QName, Serializable>(props, NodePropertyValue.IMMUTABLE_CLASSES);
|
|
// The 'get' here will clone the value if it is mutable
|
|
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.
|
|
* <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, false);
|
|
// 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, false).getNodeVersionKey();
|
|
copyNodeAspectsCached(nodeVersionKey, newNodeVersionKey);
|
|
copyNodePropertiesCached(nodeVersionKey, newNodeVersionKey);
|
|
copyParentAssocsCached(node);
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
// Copy cache properties for additions
|
|
propsToCache = new HashMap<QName, Serializable>(oldPropsCached);
|
|
// Combine the old and new properties
|
|
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);
|
|
// Get cache props
|
|
Map<QName, Serializable> cachedProps = getNodePropertiesCached(nodeId);
|
|
// Remove deleted properties
|
|
Map<QName, Serializable> props = new HashMap<QName, Serializable>(cachedProps);
|
|
props.keySet().removeAll(propertyQNames);
|
|
// Update cache
|
|
setNodePropertiesCached(nodeId, props);
|
|
}
|
|
// 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, false);
|
|
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 the read-only cached property map
|
|
*/
|
|
private Map<QName, Serializable> getNodePropertiesCached(Long nodeId)
|
|
{
|
|
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId, false).getNodeVersionKey();
|
|
Pair<NodeVersionKey, Map<QName, Serializable>> cacheEntry = propertiesCache.getByKey(nodeVersionKey);
|
|
if (cacheEntry == null)
|
|
{
|
|
invalidateNodeCaches(nodeId);
|
|
throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
|
|
}
|
|
// We have the properties from the cache
|
|
Map<QName, Serializable> cachedProperties = cacheEntry.getSecond();
|
|
return cachedProperties;
|
|
}
|
|
|
|
/**
|
|
* 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, false).getNodeVersionKey();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, false).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, false).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, false).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, false).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);
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug(
|
|
"Failed to insert node association: \n" +
|
|
" sourceNodeId: " + sourceNodeId + "\n" +
|
|
" targetNodeId: " + targetNodeId + "\n" +
|
|
" assocTypeQName: " + assocTypeQName + "\n" +
|
|
" assocIndex: " + assocIndex,
|
|
e);
|
|
}
|
|
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;
|
|
}
|
|
|
|
@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>> getNodeAssocsToAndFrom(Long nodeId)
|
|
{
|
|
List<NodeAssocEntity> nodeAssocEntities = selectNodeAssocs(nodeId);
|
|
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>> 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,
|
|
boolean allowDeletedChild)
|
|
{
|
|
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, true);
|
|
final Node childNode = getNodeNotNull(childNodeId, !allowDeletedChild);
|
|
|
|
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);
|
|
// DuplicateChildNodeNameException implements DoNotRetryException.
|
|
|
|
// Allow real DB concurrency issues (e.g. DeadlockLoserDataAccessException) straight through for a retry
|
|
if (e instanceof ConcurrencyFailureException)
|
|
{
|
|
throw e;
|
|
}
|
|
|
|
// There are some cases - FK violations, specifically - where we DO actually want to retry.
|
|
// Detecting this is done by looking for the related FK names, 'fk_alf_cass_*' in the error message
|
|
String lowerMsg = e.getMessage().toLowerCase();
|
|
if (lowerMsg.contains("fk_alf_cass_"))
|
|
{
|
|
throw new ConcurrencyFailureException("FK violation updating primary parent association:" + assoc, 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, false);
|
|
Long assocId = assoc.getId();
|
|
// Touch the node; parent assocs have been updated
|
|
touchNode(childNodeId, null, null, false, false, true);
|
|
// 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 + ". A concurrency violation is likely.\n" +
|
|
"This can also occur if code reacts to 'beforeDelete' callbacks and pre-emptively deletes associations \n" +
|
|
"that are about to be cascade-deleted. The 'onDelete' phase then fails to delete the association.\n" +
|
|
"See links on issue ALF-12358."); // TODO: Get docs URL
|
|
}
|
|
// Update cache
|
|
Long childNodeId = assoc.getChildNode().getId();
|
|
ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId);
|
|
// Delete it
|
|
List<Long> assocIds = Collections.singletonList(assocId);
|
|
int count = deleteChildAssocs(assocIds);
|
|
if (count != 1)
|
|
{
|
|
throw new ConcurrencyFailureException("Child association not deleted: " + assocId);
|
|
}
|
|
// Touch the node; parent assocs have been updated
|
|
touchNode(childNodeId, null, null, false, false, true);
|
|
// 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
|
|
{
|
|
int total = 0;
|
|
Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
|
|
try
|
|
{
|
|
for (ChildAssocEntity parentAssoc : getParentAssocsCached(childNodeId).getParentAssocs().values())
|
|
{
|
|
// Subtlety: We only update those associations for which name uniqueness checking is enforced.
|
|
// Such associations have a positive CRC
|
|
if (parentAssoc.getChildNodeNameCrc() <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
Pair<Long, QName> oldTypeQnamePair = qnameDAO.getQName(parentAssoc.getTypeQNameId());
|
|
// Ensure we invalidate the name cache (the child version key might not be 'bumped' by the next
|
|
// 'touch')
|
|
if (oldTypeQnamePair != null)
|
|
{
|
|
childByNameCache.remove(new ChildByNameKey(parentAssoc.getParentNode().getId(),
|
|
oldTypeQnamePair.getSecond(), parentAssoc.getChildNodeName()));
|
|
}
|
|
int count = updateChildAssocUniqueName(parentAssoc.getId(), childName);
|
|
if (count <= 0)
|
|
{
|
|
// Should not be attempting to delete a deleted node
|
|
throw new ConcurrencyFailureException("Failed to update an existing parent association "
|
|
+ parentAssoc.getId());
|
|
}
|
|
total += count;
|
|
}
|
|
controlDAO.releaseSavepoint(savepoint);
|
|
return total;
|
|
}
|
|
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;
|
|
}
|
|
|
|
private void bindFixAssocAndCollectLostAndFound(final Pair<Long, NodeRef> lostNodePair, final String lostName, final Long assocId, final boolean orphanChild)
|
|
{
|
|
// Remember the items already deleted in inner transactions
|
|
final Set<Pair<Long, NodeRef>> lostNodePairs = TransactionalResourceHelper.getSet(KEY_LOST_NODE_PAIRS);
|
|
final Set<Long> deletedAssocs = TransactionalResourceHelper.getSet(KEY_DELETED_ASSOCS);
|
|
AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter()
|
|
{
|
|
@Override
|
|
public void afterRollback()
|
|
{
|
|
if (transactionService.getAllowWrite())
|
|
{
|
|
// New transaction
|
|
RetryingTransactionCallback<Void> callback = new RetryingTransactionCallback<Void>()
|
|
{
|
|
public Void execute() throws Throwable
|
|
{
|
|
if (assocId == null)
|
|
{
|
|
// 'child' with missing parent assoc => collect lost+found orphan child
|
|
if (lostNodePairs.add(lostNodePair))
|
|
{
|
|
collectLostAndFoundNode(lostNodePair, lostName);
|
|
logger.error("ALF-13066: Orphan child node has been re-homed under lost_found: "
|
|
+ lostNodePair);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 'child' with deleted parent assoc => delete invalid parent assoc and if primary then
|
|
// collect lost+found orphan child
|
|
if (deletedAssocs.add(assocId))
|
|
{
|
|
deleteChildAssoc(assocId); // Can't use caching version or may hit infinite loop
|
|
logger.error("ALF-12358: Deleted node - removed child assoc: " + assocId);
|
|
}
|
|
|
|
if (orphanChild && lostNodePairs.add(lostNodePair))
|
|
{
|
|
collectLostAndFoundNode(lostNodePair, lostName);
|
|
logger.error("ALF-12358: Orphan child node has been re-homed under lost_found: "
|
|
+ lostNodePair);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* TODO: Remove once ALF-12358 has been proven to be fixed i.e. no more orphans are created ... ever.
|
|
*/
|
|
private void collectLostAndFoundNode(Pair<Long, NodeRef> lostNodePair, String lostName)
|
|
{
|
|
Long childNodeId = lostNodePair.getFirst();
|
|
NodeRef lostNodeRef = lostNodePair.getSecond();
|
|
|
|
Long newParentNodeId = getOrCreateLostAndFoundContainer(lostNodeRef.getStoreRef()).getId();
|
|
|
|
String assocName = lostName+"-"+System.currentTimeMillis();
|
|
// Create new primary assoc (re-home the orphan node under lost_found)
|
|
ChildAssocEntity assoc = newChildAssocImpl(newParentNodeId,
|
|
childNodeId,
|
|
true,
|
|
ContentModel.ASSOC_CHILDREN,
|
|
QName.createQName(assocName),
|
|
assocName,
|
|
true);
|
|
|
|
// Touch the node; all caches are fine
|
|
touchNode(childNodeId, null, null, false, false, false);
|
|
|
|
// update cache
|
|
boolean isRoot = false;
|
|
boolean isStoreRoot = false;
|
|
ParentAssocsInfo parentAssocInfo = new ParentAssocsInfo(isRoot, isStoreRoot, assoc);
|
|
setParentAssocsCached(childNodeId, parentAssocInfo);
|
|
|
|
// Account for index impact; remove the orphan committed to the index
|
|
nodeIndexer.indexUpdateChildAssociation(
|
|
new ChildAssociationRef(null, null, null, lostNodeRef),
|
|
assoc.getRef(qnameDAO));
|
|
|
|
/*
|
|
// Update ACLs for moved tree - note: actually a NOOP if oldParentAclId is null
|
|
Long newParentAclId = newParentNode.getAclId();
|
|
Long oldParentAclId = null; // unknown
|
|
accessControlListDAO.updateInheritance(childNodeId, oldParentAclId, newParentAclId);
|
|
*/
|
|
}
|
|
|
|
private Node getOrCreateLostAndFoundContainer(StoreRef storeRef)
|
|
{
|
|
Pair<Long, NodeRef> rootNodePair = getRootNode(storeRef);
|
|
Long rootParentNodeId = rootNodePair.getFirst();
|
|
|
|
final List<Pair<Long, NodeRef>> nodes = new ArrayList<Pair<Long, NodeRef>>(1);
|
|
NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
nodes.add(childNodePair);
|
|
// More results
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean preLoadNodes()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void done()
|
|
{
|
|
}
|
|
};
|
|
Set<QName> assocTypeQNames = new HashSet<QName>(1);
|
|
assocTypeQNames.add(ContentModel.ASSOC_LOST_AND_FOUND);
|
|
getChildAssocs(rootParentNodeId, assocTypeQNames, callback);
|
|
|
|
Node lostFoundNode = null;
|
|
if (nodes.size() > 0)
|
|
{
|
|
Long lostFoundNodeId = nodes.get(0).getFirst();
|
|
lostFoundNode = getNodeNotNull(lostFoundNodeId, true);
|
|
if (nodes.size() > 1)
|
|
{
|
|
logger.warn("More than one lost_found, using first: " + lostFoundNode.getNodeRef());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Locale locale = localeDAO.getOrCreateDefaultLocalePair().getSecond();
|
|
|
|
lostFoundNode = newNode(
|
|
rootParentNodeId,
|
|
ContentModel.ASSOC_LOST_AND_FOUND,
|
|
ContentModel.ASSOC_LOST_AND_FOUND,
|
|
storeRef,
|
|
null,
|
|
ContentModel.TYPE_LOST_AND_FOUND,
|
|
locale,
|
|
ContentModel.ASSOC_LOST_AND_FOUND.getLocalName(),
|
|
null).getChildNode();
|
|
|
|
logger.info("Created lost_found: " + lostFoundNode.getNodeRef());
|
|
}
|
|
|
|
return lostFoundNode;
|
|
}
|
|
|
|
/**
|
|
* 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); // note: currently may throw NotLiveNodeException
|
|
// bulk load parents as we are certain to hit them in the next call
|
|
ArrayList<Long> toLoad = new ArrayList<Long>(parentAssocInfo.getParentAssocs().size());
|
|
for(Map.Entry<Long, ChildAssocEntity> entry : parentAssocInfo.getParentAssocs().entrySet())
|
|
{
|
|
toLoad.add(entry.getValue().getParentNode().getId());
|
|
}
|
|
cacheNodesById(toLoad);
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
/**
|
|
* A Map-like class for storing ParentAssocsInfos. It prunes its oldest ParentAssocsInfo entries not only when a
|
|
* capacity is reached, but also when a total number of cached parents is reached, as this is what dictates the
|
|
* overall memory usage.
|
|
*/
|
|
private static class ParentAssocsCache
|
|
{
|
|
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
|
private final int size;
|
|
private final int maxParentCount;
|
|
private final Map<Pair <Long, String>, ParentAssocsInfo> cache;
|
|
private final Map<Pair <Long, String>, Pair <Long, String>> nextKeys;
|
|
private final Map<Pair <Long, String>, Pair <Long, String>> previousKeys;
|
|
private Pair <Long, String> firstKey;
|
|
private Pair <Long, String> lastKey;
|
|
private int parentCount;
|
|
|
|
/**
|
|
* @param size
|
|
* @param limitFactor
|
|
*/
|
|
public ParentAssocsCache(int size, int limitFactor)
|
|
{
|
|
this.size = size;
|
|
this.maxParentCount = size * limitFactor;
|
|
final int mapSize = size * 2;
|
|
this.cache = new HashMap<Pair <Long, String>, ParentAssocsInfo>(mapSize);
|
|
this.nextKeys = new HashMap<Pair <Long, String>, Pair <Long, String>>(mapSize);
|
|
this.previousKeys = new HashMap<Pair <Long, String>, Pair <Long, String>>(mapSize);
|
|
}
|
|
|
|
private ParentAssocsInfo get(Pair <Long, String> cacheKey)
|
|
{
|
|
lock.readLock().lock();
|
|
try
|
|
{
|
|
return cache.get(cacheKey);
|
|
}
|
|
finally
|
|
{
|
|
lock.readLock().unlock();
|
|
}
|
|
}
|
|
|
|
private void put(Pair <Long, String> cacheKey, ParentAssocsInfo parentAssocs)
|
|
{
|
|
lock.writeLock().lock();
|
|
try
|
|
{
|
|
// If an entry already exists, remove it and do the necessary housekeeping
|
|
if (cache.containsKey(cacheKey))
|
|
{
|
|
remove(cacheKey);
|
|
}
|
|
|
|
// Add the value and prepend the key
|
|
cache.put(cacheKey, parentAssocs);
|
|
if (firstKey == null)
|
|
{
|
|
lastKey = cacheKey;
|
|
}
|
|
else
|
|
{
|
|
nextKeys.put(cacheKey, firstKey);
|
|
previousKeys.put(firstKey, cacheKey);
|
|
}
|
|
firstKey = cacheKey;
|
|
parentCount += parentAssocs.getParentAssocs().size();
|
|
|
|
// Now prune the oldest entries whilst we have more cache entries or cached parents than desired
|
|
int currentSize = cache.size();
|
|
while (currentSize > size || parentCount > maxParentCount)
|
|
{
|
|
remove(lastKey);
|
|
currentSize--;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock.writeLock().unlock();
|
|
}
|
|
}
|
|
|
|
private ParentAssocsInfo remove(Pair <Long, String> cacheKey)
|
|
{
|
|
lock.writeLock().lock();
|
|
try
|
|
{
|
|
// Remove from the map
|
|
ParentAssocsInfo oldParentAssocs = cache.remove(cacheKey);
|
|
|
|
// If the object didn't exist, we are done
|
|
if (oldParentAssocs == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Re-link the list
|
|
Pair <Long, String> previousCacheKey = previousKeys.remove(cacheKey);
|
|
Pair <Long, String> nextCacheKey = nextKeys.remove(cacheKey);
|
|
if (nextCacheKey == null)
|
|
{
|
|
if (previousCacheKey == null)
|
|
{
|
|
firstKey = lastKey = null;
|
|
}
|
|
else
|
|
{
|
|
lastKey = previousCacheKey;
|
|
nextKeys.remove(previousCacheKey);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (previousCacheKey == null)
|
|
{
|
|
firstKey = nextCacheKey;
|
|
previousKeys.remove(nextCacheKey);
|
|
}
|
|
else
|
|
{
|
|
nextKeys.put(previousCacheKey, nextCacheKey);
|
|
previousKeys.put(nextCacheKey, previousCacheKey);
|
|
}
|
|
}
|
|
// Update the parent count
|
|
parentCount -= oldParentAssocs.getParentAssocs().size();
|
|
return oldParentAssocs;
|
|
}
|
|
finally
|
|
{
|
|
lock.writeLock().unlock();
|
|
}
|
|
}
|
|
|
|
private void clear()
|
|
{
|
|
lock.writeLock().lock();
|
|
try
|
|
{
|
|
cache.clear();
|
|
nextKeys.clear();
|
|
previousKeys.clear();
|
|
firstKey = lastKey = null;
|
|
parentCount = 0;
|
|
}
|
|
finally
|
|
{
|
|
lock.writeLock().unlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Returns a node's parent associations
|
|
*/
|
|
private ParentAssocsInfo getParentAssocsCached(Long nodeId)
|
|
{
|
|
Node node = getNodeNotNull(nodeId, false);
|
|
Pair<Long, String> cacheKey = new Pair<Long, String>(nodeId, node.getTransaction().getChangeTxnId());
|
|
ParentAssocsInfo value = parentAssocsCache.get(cacheKey);
|
|
if (value == null)
|
|
{
|
|
value = loadParentAssocs(node.getNodeVersionKey());
|
|
parentAssocsCache.put(cacheKey, value);
|
|
}
|
|
|
|
// We have already validated on loading that we have a list in sync with the child node, so if the list is still
|
|
// empty we have an integrity problem
|
|
if (value.getPrimaryParentAssoc() == null && !value.isStoreRoot())
|
|
{
|
|
Pair<Long, NodeRef> currentNodePair = node.getNodePair();
|
|
// We have a corrupt repository - non-root node has a missing parent ?!
|
|
bindFixAssocAndCollectLostAndFound(currentNodePair, "nonRootNodeWithoutParents", null, false);
|
|
|
|
// throw - error will be logged and then bound txn listener (afterRollback) will be called
|
|
throw new NonRootNodeWithoutParentsException(currentNodePair);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Update a node's parent associations.
|
|
*/
|
|
private void setParentAssocsCached(Long nodeId, ParentAssocsInfo parentAssocs)
|
|
{
|
|
Node node = getNodeNotNull(nodeId, false);
|
|
Pair<Long, String> cacheKey = new Pair<Long, String>(nodeId, node.getTransaction().getChangeTxnId());
|
|
parentAssocsCache.put(cacheKey, parentAssocs);
|
|
}
|
|
|
|
/**
|
|
* Helper method to copy cache values from one key to another
|
|
*/
|
|
private void copyParentAssocsCached(Node from)
|
|
{
|
|
String fromTransactionId = from.getTransaction().getChangeTxnId();
|
|
String toTransactionId = getCurrentTransaction().getChangeTxnId();
|
|
// If the node is already in this transaction, there's nothing to do
|
|
if (fromTransactionId.equals(toTransactionId))
|
|
{
|
|
return;
|
|
}
|
|
Pair<Long, String> cacheKey = new Pair<Long, String>(from.getId(), fromTransactionId);
|
|
ParentAssocsInfo cacheEntry = parentAssocsCache.get(cacheKey);
|
|
if (cacheEntry != null)
|
|
{
|
|
parentAssocsCache.put(new Pair<Long, String>(from.getId(), toTransactionId), cacheEntry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper method to remove associations relating to a cached node
|
|
*/
|
|
private void invalidateParentAssocsCached(Node node)
|
|
{
|
|
// Invalidate both the node and current transaction ID, just in case
|
|
Long nodeId = node.getId();
|
|
String nodeTransactionId = node.getTransaction().getChangeTxnId();
|
|
parentAssocsCache.remove(new Pair<Long, String>(nodeId, nodeTransactionId));
|
|
if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_WRITE)
|
|
{
|
|
String currentTransactionId = getCurrentTransaction().getChangeTxnId();
|
|
if (!currentTransactionId.equals(nodeTransactionId))
|
|
{
|
|
parentAssocsCache.remove(new Pair<Long, String>(nodeId, currentTransactionId));
|
|
}
|
|
}
|
|
}
|
|
|
|
private ParentAssocsInfo loadParentAssocs(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);
|
|
if (nodeCheckFromDb == null || nodeCheckFromDb.getDeleted(qnameDAO) || !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 + ")");
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/*
|
|
* Bulk caching
|
|
*/
|
|
|
|
@Override
|
|
public void setCheckNodeConsistency()
|
|
{
|
|
if (nodesTransactionalCache != null)
|
|
{
|
|
nodesTransactionalCache.setDisableSharedCacheReadForTransaction(true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Set<Long> getCachedAncestors(List<Long> nodeIds)
|
|
{
|
|
// First, make sure 'level 1' nodes and their parents are in the cache
|
|
cacheNodesById(nodeIds);
|
|
for (Long nodeId : nodeIds)
|
|
{
|
|
// Filter out deleted nodes
|
|
if (exists(nodeId))
|
|
{
|
|
getParentAssocsCached(nodeId);
|
|
}
|
|
}
|
|
// Now recurse on all ancestors in the cache
|
|
Set<Long> ancestors = new TreeSet<Long>();
|
|
for (Long nodeId : nodeIds)
|
|
{
|
|
findCachedAncestors(nodeId, ancestors);
|
|
}
|
|
return ancestors;
|
|
}
|
|
|
|
/**
|
|
* Uses the node and parent assocs cache content to recursively find the set of currently cached ancestor node IDs
|
|
*/
|
|
private void findCachedAncestors(Long nodeId, Set<Long> ancestors)
|
|
{
|
|
if (!ancestors.add(nodeId))
|
|
{
|
|
return; // Already visited
|
|
}
|
|
Node node = nodesCache.getValue(nodeId);
|
|
if (node == null)
|
|
{
|
|
return; // Not in cache yet - will load in due course
|
|
}
|
|
Pair<Long, String> cacheKey = new Pair<Long, String>(nodeId, node.getTransaction().getChangeTxnId());
|
|
ParentAssocsInfo value = parentAssocsCache.get(cacheKey);
|
|
if (value == null)
|
|
{
|
|
return; // Not in cache yet - will load in due course
|
|
}
|
|
for (ChildAssocEntity childAssoc : value.getParentAssocs().values())
|
|
{
|
|
findCachedAncestors(childAssoc.getParentNode().getId(), ancestors);
|
|
}
|
|
}
|
|
|
|
@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.
|
|
*/
|
|
|
|
boolean disableSharedCacheReadForTransaction = false;
|
|
if (nodesTransactionalCache != null)
|
|
{
|
|
disableSharedCacheReadForTransaction = nodesTransactionalCache.getDisableSharedCacheReadForTransaction();
|
|
}
|
|
|
|
if ((disableSharedCacheReadForTransaction == false) && 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
|
|
List<Node> nodes = selectNodesByUuids(storeId, batch);
|
|
cacheNodesNoBatch(nodes);
|
|
batch.clear();
|
|
}
|
|
}
|
|
// Load any remaining nodes
|
|
if (batch.size() > 0)
|
|
{
|
|
List<Node> nodes = selectNodesByUuids(storeId, batch);
|
|
cacheNodesNoBatch(nodes);
|
|
}
|
|
}
|
|
|
|
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
|
|
List<Node> nodes = selectNodesByIds(batch);
|
|
cacheNodesNoBatch(nodes);
|
|
batch.clear();
|
|
}
|
|
}
|
|
// Load any remaining nodes
|
|
if (batch.size() > 0)
|
|
{
|
|
List<Node> nodes = selectNodesByIds(batch);
|
|
cacheNodesNoBatch(nodes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(qnameDAO));
|
|
}
|
|
|
|
// Done
|
|
return nodeStatuses;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
public Long getMinTxnId()
|
|
{
|
|
Long id = selectMinTxnId();
|
|
return (id == null ? LONG_ZERO : id);
|
|
}
|
|
|
|
public Long getMinUnusedTxnCommitTime()
|
|
{
|
|
Long id = selectMinUnusedTxnCommitTime();
|
|
return (id == null ? LONG_ZERO : id);
|
|
}
|
|
|
|
public Long getMaxTxnId()
|
|
{
|
|
Long id = selectMaxTxnId();
|
|
return (id == null ? LONG_ZERO : id);
|
|
}
|
|
|
|
/*
|
|
* 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 StoreEntity selectStore(StoreRef storeRef);
|
|
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);
|
|
protected abstract int deleteNodesByCommitTime(long maxTxnCommitTimeMs);
|
|
protected abstract NodeEntity selectNodeById(Long id);
|
|
protected abstract NodeEntity selectNodeByNodeRef(NodeRef nodeRef);
|
|
protected abstract List<Node> selectNodesByUuids(Long storeId, SortedSet<String> uuids);
|
|
protected abstract List<Node> selectNodesByIds(SortedSet<Long> ids);
|
|
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 deleteNodeAssocs(List<Long> ids);
|
|
protected abstract List<NodeAssocEntity> selectNodeAssocs(Long nodeId);
|
|
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 deleteChildAssocs(List<Long> ids);
|
|
protected abstract int updateChildAssocIndex(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
int index);
|
|
protected abstract int updateChildAssocUniqueName(Long assocId, 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);
|
|
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();
|
|
protected abstract Long selectMinTxnId();
|
|
protected abstract Long selectMaxTxnId();
|
|
protected abstract Long selectMinUnusedTxnCommitTime();
|
|
}
|