mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-06-30 18:15:39 +00:00
34474: ALF-13169 Tomcat fails to shutdown - fix non daemon Timer's 34475: Part 1: Fix for ALF-13244 SOLR Multi-threaded tracking is required for performance - simultaneous document transformations - multi-threaded transaction and node tracking (off by default at the moment) - fix index/repo sync check failure if ACLs have been indexed but no transactions - minimise data sent back from query responses (not all stored fields) - added SOLR side config for HTTPClient pooling, cache sizing and tracker configuration - fixed SOLR incremental cache update for merges that end with all deletions in the old index - fixed unclosed stream in SolrKeyResourceLoader 34478: ALF-13050 - CIFS: Disabling account is not respected Also contains major rework of logging and exception handling. 34499: Fix for ALF-13150 34526: Fix for ALF-13288 34530: Minor CSS tweaks after changes for ALF-11991 34539: ALF-13176 - Implement Word for Mac 2011 Cifs Shuffle. 34541: ALF-13244 SOLR Multi-threaded tracking is required for performance - simultaneous document transformations - multi-threaded ACL tracking - multi-threaded statistics and reporting control - nodes that can not be indexed have an error record added to the index and do not block indexing the transaction (nodes unindexed due to exceptions can be found by ID query and the exception stored in the index) - nodes that are not-indexed have a minimal record added to the index for index consistency checking (unindexed nodes can be found by ID query) 34544: Add support for CIFS Level II shared oplocks. ALF-13138, ALF-13110. Fixed CIFS open for attributes only access preventing oplock on the following file open. Fixed reporting serialized copies of file access tokens as leaked. 34576: ALF-12767 - CIFS TextEdit - File has been modified outside TextEdit 34577: incorrectly checked in copy of network-protocol-context.xml 34580: ALF-13215: Ensure that permissions for everyone cannot be upgraded on moderated or private site. Fixed inconsistency between permissions shown in properties and in dialog 34582: ALF-13332: Updated modifier link for correct profile 34609: ALF-12740: Update to previous fix (only apply to IE8 and below) 34623: ALF-12767 - CIFS TextEdit - File has been modified outside TextEdit 34636: Fix for ALF-13365 SOLR: Recently modified docs dashlet sorts incorrectly - respect short property names on sort requests @cm:created and not require the full @{uri...}created 34659: ALF-2550 - added enterprise repo config files. 34715: Fix for __ShowDetails desktop action returned URL is truncated if hostname too long. ALF-13202. 34726: ALF-13293: Webdav: Version history lost after editing content in Finder 34738: ALF-7883: WebDAV: support HEAD method for folder - Fix by Pavel 34743: Fix for ALF-13244 SOLR Multi-threaded tracking is required for performance - simultaneous document transformations - batch fetch for nodes in transaction, acls in sets, and acls and readers - config for batch fetching - Better reporting for ACL set indexing 34747: ALF-13262: adding missing indexes for new schema's (activiti-schema create) + schema patch for existing schema 34817: Merged V4.0 to V4.0-BUG-FIX 34493: SPANISH: translation updates based on EN r34103 34498: Fixed ALF-12031: WCM: Content cannot be expired: avmExpiredContentTrigger is missing - Side-effect of ALF-11644: AVM cleanup jobs run when WCM is not installed - WORKAROUND: Get file 'root\projects\installer\wcm-bootstrap-context.xml' and use that 34525: Fix for ALF-13210: - removed "unsupported" from bulk filesystem import web pages 34531: Fix for ALF-13117 and ALF-13273 34549: Merged BRANCHES/DEV/BELARUS/HEAD-2012_03_15 to BRANCHES/V4.0: 34528: ALF-12874: 34552: ALF-13322: Fixed doc lib reload loop caused by "#" in folder name 34553: ALF-13311: Ensure images can be linked in TinyMCE create HTML content editor 34556: Minor: removed unused code 34557: Merged DEV to V4.0 34537: ALF-13035: Add "START WITH" parameter to IDENTITY field. ALF-13034: Add "optional" parameter for statement that drops index that was generated automatically. 34567: ALF-11047: Ensure that Explorer linked files and folders (from outside of sites) display correctly 34578: Fixes: ALF-11744: Dates rendered with the form service date control are rendered on the server, so show server time. - I've added the timezone to the display format and the ISO8601 date to the as an attribute on the HTML element to allow client side parsing - Adds client side parsing on the Doc Details page, so times are shown in the timezone of the user's browser. 34583: GERMAN: Translation update, based on EN r34103, Fixes: ALF-13075, 34584: FRENCH: Translation update based on EN r34103, Fixes: ALF-13002, ALF-13003, ALF-13020 34585: ITALIAN: Translation update based on EN r34103 34586: JAPANESE: Translation update based on EN r34103 34587: DUTCH: Translation update based on EN r34103, Fixes: ALF-12575. 34626: Fixes: ALF-13375 - Date rendering bug in search results 34630: Further fix for ALF-13375 that modifies Alfresco.util.formatDate's ISO8601 support for backward compatibility (e.g. passing in non ISO strings). 34635: ALF-12061: Mac support: Document Connection always throws an error - Case sensitivity fix by Pavel 34653: ALF-12308, ALF-12309, ALF-12554: Stack specific script errors 34655: Fix for ALF-12723 CMIS: Over-riding cm:autoVersionOnUpdateProps in custom model prevents startup 34656: Merged HEAD to BRANCHES/V4.0: 34654: Fixes: ALF-13389: Old element id used when setting event end date. 34657: Translation updates for all languages except JA. 34660: Fix to license driven config files to remove erroneous characters 34669: Merged DEV to V4.0 34663: ALF-12242: User activation issue InviteHelper.acceptNominatedInvitation() method was changed to enable user account in any case(no matter was it enabled/disabled before) 34681: Merged DEV/THEMIS2 to V4.0 34472: Document List Customization Refactor - SLingshotSiteModuleEvalutaor now has new <applyForNonSite> param that defaults to false for backward compability - Slingshot extension points, surf-doclist.get now uses 2 spring beans: * "resolver.doclib.doclistDataUrl" to get the repo doclist data url * "resolver.doclib.actionGroup" to get each item/nodes action group id 34692: Fix for ALF-12715 - Incorrect SPP working (mimetype not set on document stored via ADM Remote Store API) 34708: ALF-13239: Merged V3.4-BUG-FIX (3.4.9) to V4.0 (4.0.1) 34707: ALF-13239 Share rule to convert to PNG fails on JPG images - Issue was showing up in 4.0.1 as a change was made for iPad that introduced an imageOptions.isAutoOrient() setting. This forced a concatenation of null with " -auto-orient". However there are also crop and resize options that could also do this even in 3.4 Setting the commandOptions String to "" when null, is fine as this is how property value nulls are handled later anyway. 34718: JAPANESE: Localisation of Company specific contact information & addition of timezone to form control. 34719: FRENCH: File consistency tweak. 34746: ALF-12903: Create HTML content fix 34754: Merged PATCHES/V4.0.0 to V4.0 34750: Reinstate ${version.label} into version.number property 34810: Merged DEV to V4.0 (with corrections) 34807: ALF-13290 : Mac Support: Error appears after collaborator saves changes to the document deleteFailedThumbnailChildren method should be run as system user as it may fails with AccesssDenied if collaborator updates document 34876: Fix fo ALF-13503 Add SOLR client API tests to the SystemBuildTest project - SOLR API tests run embedded with SSL 34984: ALF-13109 - Correction to NTIOCtl.FsCtlCreateOrGetObjectId 35009: Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/DEV/V4.0-BUG-FIX: 35008: Fix for ALF-12817. Fixed as suggested - new method remove(). 35031: Fix for ALF-12309 35032: Fix fo ALF-13535 using CMIS, on-disk tickets cache can grow unbounded - expire tickets based on inactivity by default - added job to clean up expired tickets - all are configurable 35033: Fix fo ALF-13535 using CMIS, on-disk tickets cache can grow unbounded - avoid NPE for null tickets 35037: Fix for ALF-13505 SOLR tracking readers does not encode all uids correctly - fixed reader encoding 35049: ALF-13384 - Saving large Word (mac 2011) document via CIFS fails in Mac OS X Lion 35053: Merged V4.0 (V4.0.1) to V4.0-BUG-FIX (4.0.2) 34844: Merged V3.4-BUG-FIX (3.4.9) to V4.0 (4.0.1) 34843: ALF-5830 show_audit.ftl template doesn't work anymore - Removed L10n messages that are no longer used (should have been removed in 3.4.6 when this issue was fixed) 34847: Merged HEAD to BRANCHES/V4.0: 34804: Fixes: ALF-13309: Issue with over zealous HTML escaping with truncated descriptions in the Calendar Agenda view. 34861: ALF-13497: Merged PATCHES/V4.0.0 to V4.0 34813: ALF-13115: No feedback is given to the user when Approve/Reject is clicked for a task when they followed a link to the task in an email. - Fix by Pavel, reviewed by Kev - Now they get a confirmation message followed by a redirect to their dashboard 34862: Fix for ALF-10823 "allowGuestLogin=false" and Share then fills the alfresco error log with "Guest authentication not supported" Fix for ALF-12678 Errors in log on startup (ts.alfresco.com 4.0) - improved handling of 500 errors relating to GuestAuthNotSupported when alfresco.authentication.allowGuestLogin=false 34867: Merged DEV to V4.0 34565: ALF-13074: JBPM workflow definitions are not resilient to missing model definitions WARN messages have been added if JBPM workflow definitions cannot be loaded in the model definitions. 34855: ALF-13074: JBPM workflow definitions are not resilient to missing model definitions Reimplemented to handle all exceptions during constructing WorkflowInstances WorkflowTasks and WorkflowDefinitions. 34859: ALF-13074: JBPM workflow definitions are not resilient to missing model definitions Logger messages was changed to correspond the logger pattern. 34893: Translation updates for DE and ES. 34894: Fixes: ALF-13518; Updates Calendar event object's URL to work out of context. 34896: FRENCH: Translates new strings. 34915: Merged DEV to V4.0 34912: ALF-13267: There should not be a web-client-config-custom.xml in alfresco.war Move "modules\quickr\config\alfresco\extension\web-client-config-custom.xml" to "modules\quickr\config\alfresco\module\org.alfresco.module.quickr\ui\web-client-custom.xml". 34913: ALF-13267: There should not be a web-client-config-custom.xml in alfresco.war Delete "modules\quickr\config\alfresco\extension\web-client-config-custom.xml". 34916: ALF-13267: Merged V3.4 to V4.0 (and reversed previous duplicate fix) 24828: Merged BRANCHES/DEV/BELARUS/V3.4-2011_01_13 to BRANCHES/V3.4: 24824: ALF-6361: web-client-config-custom.xml doesn't work in /alfresco/tomcat/shared/classes/alfresco/extension 34929: ALF-12242: Issues activating users when more than one member in the authentication chain - Correction to fix that caused regressions ALF-13494, ALF-13498 - Need to check for the mutability of a user's authentication before trying to enable it - Also chaining of the authentication enabled attribute should assume true until false found, not the other way around 34930: ALF-12242: Reverted change to this class as it wasn't necessary and wouldn't work! 34932: ALF-13453: Enable XMLConstants.FEATURE_SECURE_PROCESSING feature on Transformer Factory to prevent remote code execution - Now SecureTransformerFactory should be used as a standard 34965: Merged PATCHES/V4.0.0 to V4.0 34959: ALF-13550: Fix for ALF-13546 SOLR tracking fails for nodes with content and no auditable aspect - NPE as there is no last modification date to use 34960: ALF-13551: Merged BRANCHES/DEV/V4.0-BUG-FIX to PATCHES\V4.0.0 - fix for ALF-13544 When SOLR encounters an error indexing a document, subsequent indexing does not occur 34541: ALF-13244 SOLR Multi-threaded tracking is required for performance - simultaneous document transformations - nodes that can not be indexed have an error record added to the index and do not block indexing the transaction (nodes unindexed due to exceptions can be found by ID query and the exception stored in the index) - nodes that are not-indexed have a minimal record added to the index for index consistency checking (unindexed nodes can be found by ID query) 34968: ALF-13453: Reversed XSLTProcessor and XSLTRenderingEngine changes for now as they break http://wiki.alfresco.com/wiki/WCM_Forms_Rendering and model handling via bsf extensions. A more sophisticated approach is required. See bug for more info. 34972: ALF-13340: Upgrade postgres JDBC driver to tested/supported version! 34997: ALF-13453, ALF-13565: Fully reverted revision 34932 as it prevents startup on Weblogic 34998: Merged V4.0-BUG-FIX to V4.0 34992: DUTCH: translation updates based on EN r34861 34993: FRENCH: Translation updates based on r34861 34994: ITALIAN: Translation updates based on r34861 35013: ALF-13561: Not found error after uploading new version - Fix by Pavel 35034: Fixes ALF-13570: Error loading event info panel. 35039: ALF-13573: Merged V3.4-BUG-FIX (3.4.9) to V4.0 (4.0.1) 35022: ALF-13451: Allow modules to configure mimetypes 35041: ALF-13466: Error is displayed by approve or reject wcm workflow - Fixed regression caused by ALF-4098 - Protected calls to new addNewChildrenIfAny() method with isDirectory() checks 35042: GERMAN: Translation updates based on r35029, and fixes ALF-12471. 35043: SPANISH: Translation updates based on r35029, and fixes ALF-12471. 35044: FRENCH: Translation updates based on r35029, and fixes ALF-12471. 35045: ITALIAN: Translation updates based on r35029, and fixes ALF-12471. 35046: JAPANESE: Translation updates based on r35029, and fixes ALF-12471. 35047: DUTCH: Translation updates based on r35029, and fixes ALF-12471. 35090: Remove Kofax. It has been migrated to integrations/kofax 35097: Added new file server cluster tests. Open for attributes only overlapped with open with oplock. Open with oplock with break to level II shared oplock. 35099: JLAN Client updates to support level II oplocks, required by new cluster tests. 35100: Various oplock related fixes, including problems opening file on second cluster node. ALF-13109. 35107: remove errant '>' 35116: ALF-13401 - Mac LION Powerpoint CIFS 35162: Removed spurious attempt to force a concurrency exception for getNodePair after a node had actually been deleted. Code would retry 50 times before failing. Reviewed with Derek, its not the node service's job to second guess that there may be a concurrency problem in a client's cache. 35164: Fix for ALF-13641 - Negative cases for date value in propertyNegative cases for date value in property. Today button 35169: ALF-13401, ALF-12393: Added exception translation to AbstractReindexComponent retrying transactions, following change in r35162 35172: ALF-13626: category.put.json.ftl has wrong bracket 35173: ALF-12749 - CIFS: Editing of ppt/pptx files fails (MacOSx specific) 35174: Fix for ALF-13556 - Sorting for custom model fields doesn't work for search results in Share 35176: Fix for ALF-4281 - Script error at 'Email space users' form 35186: Merged BRANCHES/DEV/DAM/V4.0-BUG-FIX-34847 to BRANCHES/DEV/V4.0-BUG-FIX: 34875: Creating new branch from $FROM 34939: Merged BRANCHES/DEV/DAM/V4.0-BUG-FIX-34397 to BRANCHES/DEV/DAM/V4.0-BUG-FIX-34847: 34400: Creating new branch from $FROM 34422: Merged DEV/DAM-0.1 to DEV/DAM/V4.0-BUG-FIX-34397 34085: Allow for generateThumbnailUrl to accept a rendition name parameter. 34086: Changed simpleView view type switch to integer implementation rather than boolean. 34087: Pulled specific rendering code for simple and detail view into separate view renderer objects. 34092: If simpleView was stored as a boolean convert it to an integer for ALF-12952. 34423: Merged DEV/DAM/HEAD-34276 to DEV/DAM/V4.0-BUG-FIX-34397 34307: ALF-12952: Change DocumentList simpleView Nav Switch to an Int Implementation 34957: ALF-12952: Change DocumentList simpleView Nav Switch to an Int Implementation - Removed ability to specify index on registerViewRenderer - Added firing of setupAdditionalViewRenderers to make it easier for extensions to register themselves at the appropriate time 35021: ALF-12955: Share Document Library and Repository Browser Should Easily Allow for Additional Views - Changed viewRenderers to an object implementation with storage/retrieval via named properties or 'keys' 35050: ALF-12955: Share Document Library and Repository Browser Should Easily Allow for Additional Views - Renamed simpleView preference and option to viewRendererName - Reintroduced simpleView boolean preference and option as deprecated to allow deletion of old preference - Renamed viewRendererOrder to viewRendererNames - Added default viewRendererNames at DocumentList.options level - Renamed widgets.simpleDetailed to widgets.viewRendererSelect but did NOT change HTML id for backwards compatibility - Renamed onSimpleDetailed to onViewRendererSelect - Added deletion of deprecated simpleView preference if it exists 35056: ALF-12955: Share Document Library and Repository Browser Should Easily Allow for Additional Views - Made viewRenderer methods a proper Alfresco.ViewRenderer object which is more easily extended - Added name property to ViewRenderer constructor and changed registerViewRenderer to use that as a key - With more strictly defined ViewRenderers in place, changed select button to iterate over viewRendererNames rather than explicit list 35104: ALF-12955: Share Document Library and Repository Browser Should Easily Allow for Additional Views - Added markup tag around the document list container 35126: ALF-12955: Share Document Library and Repository Browser Should Easily Allow for Additional Views - Added markup tag documentListConstructorSetOptions around setOptions after DocumentList object constructor - Added markup tag documentListViewRendererSelect around view select buttons - Added markup tag documentListShowFolders around show folders button - Added markup tag documentListSortSelect around sort selection buttons - Renamed Alfresco.ViewRenderer to more specific Alfresco.DocumentListViewRenderer and private methods similarly - Added default for viewRendererName if it's undefined in options - Added check for availability of renderer specified in user preference, if not use default, and consolidated renderer index lookup 35179: ALF-12955: Share Document Library and Repository Browser Should Easily Allow for Additional Views - Removed documentListConstructorSetOptions 35194: Temp disable cifs text edit test. 35197: ALF-13097 - IMAP templates have wrong mimetype 35201: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 34462: Merged DEV to V3.4-BUG-FIX 34461: ALF-10759: Advanced search fails for sub-element tags UITagSelector component which allows Advanced Search to add new tag option to search 34479: Merged V3.4 to V3.4-BUG-FIX (RECORD ONLY) 34477: ALF-13237: Yet another 13th hour Spring Surf Regression - Can't afford to pull in all the latest surf goodies so overriding PageImpl.class with one corresponding to Surf revision 1034 in WEB-INF/classes, just for 3.4.8 34515: ALF-9855: Alfresco side to support standard Adobe-Japan1 PDF fonts in swftools - Bitrock binaries provided 34518: ALF-13266: Ubuntu installation fails in non-obvious way when machine lacks sufficient memory - Fix from Bitrock - L10N required 34536: Merged DEV to V3.4-BUG-FIX 34529: ALF-13135: Impossible to Add new member on Workspace using email address NPE fix if AD users don't have e-mail address as a property. 34538: ALF-12812 Saving files with apps on Mac OS X Lion in CIFS doesn't invoke rules (Update rule fires BEFORE, FileFolderInterceptor recalcs HIDDEN and TEMPORARY ) 34542: Add support for Level II shared oplock. ALF-13093, ALF-12328. Fixed CIFS open for attributes only access preventing oplock on the following file open. 34543: Oplock and open for attributes fixes to the repo/AVM filesystems. ALF-13093, ALF-12328. 34579: ALF-13284: Removing obselete files 34603: ALF-10833 Alfresco does not show correct thumbnails for some specific kind of PDFs - Patched PDFRenderer-0.9.1 to return a null page if there was an error. The code structure did not lend itself to simply throwing the exception. - Modified PdfToImageContentTransformer to check for a null page and it then throws an AlfescoRuntimeException which causes the failover transformer to use the next transformer in the list: PDBBox which is able to transform the pdf and the image that was missing. 34617: Add missing source Java folder. 34629: ALF-13188: Content IO Channel not closed 34697: ALF-13149: Start up performance suffers if the alf_transaction table grows too large. 34712: ALF-13063: sample settings for DB2 34803: New installer translations from Gloria 34809: ALF-11956: Merged BELARUS/V3.4-BUG-FIX-2012_01_26 to V3.4-BUG-FIX (V3.4.9) << In addition to the 2 merged revisions, includes the change for ALF-11972 and test all-widgets.xsd >> 33715: ALF-11956: WCM accessibility - sandbox name oriented titles were added almost to all action links at 'Browse Website' page view; - adding titles to image tags functionality was added to ActionLinkRenderer, UIMenu and UISandboxes (this includes arrow icons for 'Web Forms' and 'Modified Items'); - titles were added to XForm Date/Time picker controls (text input and arrow buttons); - 'Click to edit' functionality via keyboard availability was added to XForms TinyMCE editor control (using 'Tab' key, 'Alt' + 'E' in IE or 'Alt' + 'Shift' + 'E' in FireFox); - additional i18n properties for Date/Time picker and action link titles were added 34625: ALF-11956: WCM accessibility Increasing XForms widgets readability by screen reader tools: - Tiny MCE 3.2.7 buttons; - required fields; - inputs labels; - VGroup, HGroup and Repeating widgets folding icons/buttons and others ALF-11972: Title attributes for the WCM form element xs:anyURI not included to allow multiple xs:anyURI file picker "Select" buttons to be distinguished by screen readers - Change defined in JIRA 34846: Translation updates: - FR: Missing Strings - DE: Fixes encoding issue 34881: ALF-13512: Merged PATCHES/V3.4.8 to V3.4-BUG-FIX 34829: ALF-12621: Sort order of folders including hyphens ( - ) are different in folder-tree and view on folders (in Share) - Switched from using JS sort to Java locale-based sort 34845: ALF-12621: Fixed array typing problems in previous checkin 34918: Fix for ALF-13385 Access DENIED api does not seem to work - changed default behaviour to any-deny-denies - config to switch back - needs custom port to 4.0 for SOLR - unit tests added 34919: Fix for ALF-13385 Access DENIED api does not seem to work - added property based configuration and default configuration check 34937: ALF-11956: Merged BELARUS/V3.4-BUG-FIX-2012_01_26 to V3.4-BUG-FIX (V3.4.9) 34886: ALF-11956: WCM accessibility - headings functionality is added. WAI-ARIA markup was used; - alert for XForms validation errors is added. WAI-ARIA markup was used; - previous accessibility changes tested and fixed against the new functionality 35003: Merged HEAD to V3.4-BUG-FIX 34673: Changed from time-based module and component names to GUID-based names. Not likely to affect anything. 35057: Fix for ALF-12590 Share - Document library doesn't return subfolders when parent space contains the character "- " - updated to the latest version of jaxen (which now includes saxpath) - the problem path is now parsed correctly 35074: ALF-13597: Merged PATCHES/V3.4.6 to V3.4-BUG-FIX 34978: ALF-13489: Index tracker now has ability to distinguish create/update/rename/link/unlink - Will prevent unnecessary cascading PATH regeneration on remote cluster nodes - QNames and noderefs of parents in index compared with those in the database - Experimental - needs testing 34983: ALF-13489: Correction to renamed node detection 34985: ALF-13489: Even more foolproof parent assoc cross-referencing - Should handle duplicate QNames, etc. - Renames now just an add and a remove 35075: ALF-13598: Merged PATCHES/V3.4.6 to V3.4-BUG-FIX 34872: Merged DEV (by Pavel) to PATCHES/V3.4.6 (and refactored) 34554: ALF-11777 : Persistent lock is left on document in certain use cases when editing online (spp) 1. From now documents are locked for maximum 24 hours when working through WebDAV/Vti. 2. Session listeners were added for web-client and vti-module to allow handling session expiration event. 3. WebDAVLockService class was implemented. It is used by session listeners to perform session cleaning (forcibly unlock all documents that were persistently locked during http session). 4. LOCK/UNLOCK webdav methods and Get/Checkout/UncheckoutDocumentMethod vti methods where updated to correctly populate session list of locked documents. 34832: ALF-11777 : Persistent lock is left on document in certain use cases when editing online (spp) 1. From now documents are locked for maximum 24 hours when working through WebDAV/Vti. 2. Session listener was added for webdav/vti to allow handling session expiration event. 3. LOCK/UNLOCK webdav methods and Get/Checkout/UncheckoutDocumentMethod vti methods where updated to use shared code to lock/unlock nodes. 34833: ALF-11777 : Persistent lock is left on document in certain use cases when editing online (spp) 1. Remove unnecessary classes after 34554 rev. 34852: ALF-11777 : Persistent lock is left on document in certain use cases when editing online (spp) 1. Some changes after David's review of revisions 34832, 34833. 34874: ALF-11777: Fixed typo 35078: ALF-12785: BaseDownloadContentServlet could co into an infinite loop if asked to seek past the end of a file 35079: ALF-12490 "HTTP Status 500 - 00200935 Exception in Transaction" message error with webform - ALF-9524 fix assumed there were only switch elements in a form 35086: ALF-13563: Upgrade to Bitrock 8.1.0 to fix password validation issue 35095: ALF-12764: New distributable alfresco-enterprise-ear-3.4.9.zip - Like war zip, but contains .ear file instead of .wars and also contains WAS shared library - Means samples and other bits are finally available to non-Tomcat users 35103: Merged DEV to V3.4-BUG-FIX 35098: ALF-12776: if a user requests to join a moderated site, and that request is rejected, the rejection email is sent to the user-id and not the email id. Implemented Correct WorkflowModelModeratedInvitation.WF_PROP_REVIEW_COMMENTS field in configuration for moderatedInvitationReviewTask Person's email into emailAction PARAM_TO 35114: ALF-12766 Creating Web Content several users - different sandboxes - To be consistent with ALF-11440 PM comment 18-Dec-2011 and ALF-8787 A Manager should only be able to create a file in a sandbox if it is NOT locked somewhere else. - Not much can be done about the error message as the locked path is useful in other situations and it is not possible to issue a different message on create only 35121: ALF-11956: Merged BELARUS/V3.4-BUG-FIX-2012_04_05 to V3.4-BUG-FIX (V3.4.9) 35109: ALF-11956: WCM accessibility - Date/Time Pickers are made accessible via the keyboard and readable by JAWS (13, demo version). WAI-ARIA standard is used; - corrected 'expanded' state determination for Date/Time Pickers; - Modified Items and Web Forms arrow buttons are made accessible via the keyboard on the Browse Website page; - some changes per the description of the issue and per the comment of the 23-Feb-12 11:33 AM 35145: ALF-11990: CIFS login with case insensitive username is rejected - User name normalization moved to before MD4 hash retrieval 35151: Port of oplock related changes from v4.x. 35177: Fix for ALF-11936 - RSS feed from the activities dashlet produces invalid XML 35178: ALF-12631: removeChild requires delete permissions on the child node, even when it is a secondary association - now it doesn't (thanks to Andy's solution) - new ACL_PRI_CHILD_ASSOC_ON_CHILD ACL entry only enforces the permission on the child node when it is a primary association 35181: Merged DEV to V3.4-BUG-FIX 35165: ALF-13409: Invite to a site throws an error if an instance of invitation-moderated-workflow is started by a user whose account is subsequently deleted InvitationServiceImpl listens for person node deletions (it already implements beforeDeleteNode) and cancels invitations within beforeDeleteNode 35182: ALF-12567 Unable to create thumbnails for certain PDF files - The supplied PDF contains an invalid offset in the xref table. This turns out to be a quite common error resulting in thousands of Google hits. The offset is set to the string value "4294967295". This number in hex is FFFFFFFF. The value of an 4 byte int in C or Java with this value is -1. Neither PDFRenderer nor PDFBox have workarounds for this although lots of other systems do, which is why it is possible to view or edit it in other systems. Patched both PDFRenderer and PDFBox to handle this common error. 35185: ALF-13033: Friendlier error message when you try to delete non existent content from a sandbox 35191: ALF-13409: Fix build. 35192: Merged V3.4 to V3.4-BUG-FIX 35161: ALF-13624: Merged V4.0-BUG-FIX to V3.4 34474: ALF-13169 Tomcat fails to shut down - fix non daemon Timers (and punctuation!) 35163: ALF-13656: Merged HEAD to V3.4 31375: Fix for ALF-435 - Unfriendly error occurs when trying to delete renamed category from category page 35189: Italian translations from Gloria 35193: Merged V3.4 to V3.4-BUG-FIX (RECORD ONLY) 35125: Merged V3.4-BUG-FIX to V3.4 35156: Correction to merge in revision 35125 (a reintegrate merge rather than a selective merge) 35202: Merged V3.4-BUG-FIX to V4.0-BUG-FIX (RECORD ONLY) 34532: ALF-13233: Merged HEAD to V3.4-BUG-FIX 32960: ALF-11008 - Support the WebDAV DELETE method in SPP/VTI, with the special response required by SPP for locked documents 34559: ALF-13106: Merged HEAD to V3.4-BUG-FIX 28223: Merged DEV/SWIFT to HEAD (Tika and Poi) 30589: Upate Tika and add Ogg Vorbis support + tests 30673: Upgrade POI and Tika for recent fixes 31009: Bump the Tika version for some recent fixes 31010: Update the test audio files to include more metadata 31011: ALF-6170 Add missing audio model (needed in devcon demo) 31013: Update the MP3 extractor to output audio keys (related to ALF-6170), and refactor the audio extractors to share more common code. Also expands the audio extractor tests to share common code, and test more metadata. (Needed for devcon demo) 31022: Tika update for custom mimetypes enhancement 31023: Add @since tags where known, and do a quick coding standards sweep 31274: ALF-10813 follow-on - make it clearer that we're just creating the one detector, and switch to the new style version 31289: ALF-10803 - Upgrade Tika to add the extra WordPerfect mimetype 31553: ALF-10525 ACP mimetype detection fix, unit tests for it, and a NPE fix 31554: Update Tika to get the fix for TIKA-764 32105: ALF-11574 Upgrade Tika for the fix to TIKA-784, and add the DITA types to the Alfresco mimetype map 32138: Bump the Tika version for the updated TIKA-784 fix, and add an Alfresco side unit test for this case 32153: Update the vorbis jar to one that includes the license info more clearly in META-INF (without needing to read the POM) 32320: ALF-11650 Upgrade Tika for TIKA-789 (MPP Detection), and add tests that show it is now being correctly handled 32363: Update POI and Tika for the new code required to solve ALF-10980 (MPP Open/Change detection) 34560: ALF-13106: Merged V4.0-BUG-FIX to V3.4-BUG-FIX 33330: ALF-12487 In Mimetype Detection, if Tika detects a generic type of text/plain or XML, defer to the Alfresco filename based type (as we already do for octet stream) 33379: Add the TIFF mimetype 33380: Improve the stream to Tika conversion code, following review for THOR-952 33385: Upgrade to the latest Tika and POI, for recent bug fixes 33779: Upgrade Tika for ALF-12714 33782: ALF-12714 Add 3GPP/3GPP2 video, and MP4 Audio mimetypes 33783: Update Tika for more MP4/QuickTime support, and enable MP4 audio metadata extraction + "quick" testing 34561: ALF-13106: Fixed merge errors 34562: ALF-13106: Merged SWIFT to V3.4-BUG-FIX 26546: Have one copy of the Tika Config in spring, rather than several places fetching their own copy of the default one (either explicitly or implicitly). 34563: ALF-13106: Merged HEAD to V3.4-BUG-FIX 32264: Adding "quick" test resources for MS project. 34564: ALF-13106: Fix unit test 34752: GERMAN: Translation updates, based on EN: 34612 34753: SPANISH: Translation updates, based on EN: 34612 34755: FRENCH: Translation updates, based on EN: 34612 34756: ITALIAN: Translation updates, based on EN: 34612 34967: ALF-13552: Merged V4.0 to V3.4-BUG-FIX 34932: ALF-13453: Enable XMLConstants.FEATURE_SECURE_PROCESSING feature on Transformer Factory to prevent remote code execution - Now SecureTransformerFactory should be used as a standard 34971: ALF-13552: Merged V4.0 to V3.4-BUG-FIX 34968: ALF-13453: Reversed XSLTProcessor and XSLTRenderingEngine changes for now as they break http://wiki.alfresco.com/wiki/WCM_Forms_Rendering and model handling via bsf extensions. A more sophisticated approach is required. See bug for more info. 34982: ALF-13554: Merged V4.0 to V3.4-BUG-FIX 34972: ALF-13340: Upgrade postgres JDBC driver to tested/supported version! 34999: ALF-13552: Merged V4.0 to V3.4-BUG-FIX 34997: ALF-13453, ALF-13565: Fully reverted revision 34932 as it prevents startup on Weblogic 35000: Translation updates for DE, ES, IT. Based on EN r34846. 35015: ALF-13451: Merged V4.0-BUG-FIX to V3.4-BUG-FIX 33864: ALF-10736: JSF - Adding mimetype does not work on 3.4.x 35020: ALF-13451: Merged V4.0-BUG-FIX to V3.4-BUG-FIX 33863: ConfigSource for XMLConfigService which uses a ResourceFinder for wildcard-compatible lookups (UrlConfigSource does not support them) 35029: JAPANESE: Translation updates based on EN r34846 35212: ALF-13409: Deleting a person can now cancel their invitations. Cancelling invitations can delete inactive persons! So prevent infinite looping with a transaction local resource - Also fix up other invite related unit tests 35217: Merged DEV to V4.0-BUG-FIX 35214: ALF-12745 : AD-LDAP: alfresco hangs when upload user csv file Disable 'Upload User CSV File' button in Share admin console in case of AD-LDAP 35221: Avoid a NPE if Repository.getPerson() is called when no RunAsUser is active, instead return Null as for users with no defined NodeRef git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@35229 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
4459 lines
172 KiB
Java
4459 lines
172 KiB
Java
/*
|
|
* Copyright (C) 2005-2012 Alfresco Software Limited.
|
|
*
|
|
* This file is part of Alfresco
|
|
*
|
|
* Alfresco is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Alfresco is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.alfresco.repo.domain.node;
|
|
|
|
import java.io.Serializable;
|
|
import java.net.InetAddress;
|
|
import java.net.UnknownHostException;
|
|
import java.sql.Savepoint;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.SortedSet;
|
|
import java.util.Stack;
|
|
import java.util.TreeSet;
|
|
|
|
import org.alfresco.error.AlfrescoRuntimeException;
|
|
import org.alfresco.ibatis.BatchingDAO;
|
|
import org.alfresco.ibatis.RetryingCallbackHelper;
|
|
import org.alfresco.ibatis.RetryingCallbackHelper.RetryingCallback;
|
|
import org.alfresco.model.ContentModel;
|
|
import org.alfresco.repo.cache.NullCache;
|
|
import org.alfresco.repo.cache.SimpleCache;
|
|
import org.alfresco.repo.cache.TransactionalCache;
|
|
import org.alfresco.repo.cache.lookup.EntityLookupCache;
|
|
import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
|
|
import org.alfresco.repo.domain.contentdata.ContentDataDAO;
|
|
import org.alfresco.repo.domain.control.ControlDAO;
|
|
import org.alfresco.repo.domain.locale.LocaleDAO;
|
|
import org.alfresco.repo.domain.permissions.AccessControlListDAO;
|
|
import org.alfresco.repo.domain.permissions.AclDAO;
|
|
import org.alfresco.repo.domain.qname.QNameDAO;
|
|
import org.alfresco.repo.domain.usage.UsageDAO;
|
|
import org.alfresco.repo.policy.BehaviourFilter;
|
|
import org.alfresco.repo.security.permissions.AccessControlListProperties;
|
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
|
|
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
|
|
import org.alfresco.repo.transaction.TransactionAwareSingleton;
|
|
import org.alfresco.repo.transaction.TransactionListenerAdapter;
|
|
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 CACHE_REGION_PARENT_ASSOCS = "N.PA";
|
|
|
|
protected Log logger = LogFactory.getLog(getClass());
|
|
private Log loggerPaths = LogFactory.getLog(getClass().getName() + ".paths");
|
|
|
|
protected final boolean isDebugEnabled = logger.isDebugEnabled();
|
|
private NodePropertyHelper nodePropertyHelper;
|
|
private ServerIdCallback serverIdCallback = new ServerIdCallback();
|
|
private UpdateTransactionListener updateTransactionListener = new UpdateTransactionListener();
|
|
private RetryingCallbackHelper childAssocRetryingHelper;
|
|
|
|
private TransactionService transactionService;
|
|
private DictionaryService dictionaryService;
|
|
private BehaviourFilter policyBehaviourFilter;
|
|
private AclDAO aclDAO;
|
|
private AccessControlListDAO accessControlListDAO;
|
|
private ControlDAO controlDAO;
|
|
private QNameDAO qnameDAO;
|
|
private ContentDataDAO contentDataDAO;
|
|
private LocaleDAO localeDAO;
|
|
private UsageDAO usageDAO;
|
|
|
|
/**
|
|
* Cache for the Store root nodes by StoreRef:<br/>
|
|
* KEY: StoreRef<br/>
|
|
* VALUE: Node representing the root node<br/>
|
|
* VALUE KEY: IGNORED<br/>
|
|
*/
|
|
private EntityLookupCache<StoreRef, Node, Serializable> rootNodesCache;
|
|
|
|
|
|
/**
|
|
* Cache for nodes with the root aspect by StoreRef:<br/>
|
|
* KEY: StoreRef<br/>
|
|
* VALUE: A set of nodes with the root aspect<br/>
|
|
*/
|
|
private SimpleCache<StoreRef, Set<NodeRef>> allRootNodesCache;
|
|
|
|
/**
|
|
* Bidirectional cache for the Node ID to Node lookups:<br/>
|
|
* KEY: Node ID<br/>
|
|
* VALUE: Node<br/>
|
|
* VALUE KEY: The Node's NodeRef<br/>
|
|
*/
|
|
private EntityLookupCache<Long, Node, NodeRef> nodesCache;
|
|
/**
|
|
* Backing transactional cache to allow read-through requests to be honoured
|
|
*/
|
|
private TransactionalCache<Serializable, Serializable> nodesTransactionalCache;
|
|
/**
|
|
* Cache for the QName values:<br/>
|
|
* KEY: NodeVersionKey<br/>
|
|
* VALUE: Set<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;
|
|
/**
|
|
* Cache for the Node parent assocs:<br/>
|
|
* KEY: NodeVersionKey<br/>
|
|
* VALUE: ParentAssocs<br/>
|
|
* VALUE KEY: None<br/s>
|
|
*/
|
|
private EntityLookupCache<NodeVersionKey, ParentAssocsInfo, Serializable> parentAssocsCache;
|
|
|
|
/**
|
|
* Cache for fast lookups of child nodes by <b>cm:name</b>.
|
|
*/
|
|
private SimpleCache<ChildByNameKey, ChildAssocEntity> childByNameCache;
|
|
|
|
/**
|
|
* Constructor. Set up various instance-specific members such as caches and locks.
|
|
*/
|
|
public AbstractNodeDAOImpl()
|
|
{
|
|
childAssocRetryingHelper = new RetryingCallbackHelper();
|
|
childAssocRetryingHelper.setRetryWaitMs(10);
|
|
childAssocRetryingHelper.setMaxRetries(5);
|
|
// Caches
|
|
rootNodesCache = new EntityLookupCache<StoreRef, Node, Serializable>(new RootNodesCacheCallbackDAO());
|
|
nodesCache = new EntityLookupCache<Long, Node, NodeRef>(new NodesCacheCallbackDAO());
|
|
aspectsCache = new EntityLookupCache<NodeVersionKey, Set<QName>, Serializable>(new AspectsCallbackDAO());
|
|
propertiesCache = new EntityLookupCache<NodeVersionKey, Map<QName, Serializable>, Serializable>(new PropertiesCallbackDAO());
|
|
parentAssocsCache = new EntityLookupCache<NodeVersionKey, ParentAssocsInfo, Serializable>(new ParentAssocsCallbackDAO());
|
|
childByNameCache = new NullCache<ChildByNameKey, ChildAssocEntity>();
|
|
}
|
|
|
|
/**
|
|
* @param transactionService the service to start post-txn processes
|
|
*/
|
|
public void setTransactionService(TransactionService transactionService)
|
|
{
|
|
this.transactionService = transactionService;
|
|
}
|
|
|
|
/**
|
|
* @param dictionaryService the service help determine <b>cm:auditable</b> characteristics
|
|
*/
|
|
public void setDictionaryService(DictionaryService dictionaryService)
|
|
{
|
|
this.dictionaryService = dictionaryService;
|
|
}
|
|
|
|
/**
|
|
* @param policyBehaviourFilter the service to determine the behaviour for <b>cm:auditable</b> and
|
|
* other inherent capabilities.
|
|
*/
|
|
public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter)
|
|
{
|
|
this.policyBehaviourFilter = policyBehaviourFilter;
|
|
}
|
|
|
|
/**
|
|
* @param aclDAO used to update permissions during certain operations
|
|
*/
|
|
public void setAclDAO(AclDAO aclDAO)
|
|
{
|
|
this.aclDAO = aclDAO;
|
|
}
|
|
|
|
/**
|
|
* @param accessControlListDAO used to update ACL inheritance during node moves
|
|
*/
|
|
public void setAccessControlListDAO(AccessControlListDAO accessControlListDAO)
|
|
{
|
|
this.accessControlListDAO = accessControlListDAO;
|
|
}
|
|
|
|
/**
|
|
* @param controlDAO create Savepoints
|
|
*/
|
|
public void setControlDAO(ControlDAO controlDAO)
|
|
{
|
|
this.controlDAO = controlDAO;
|
|
}
|
|
|
|
/**
|
|
* @param qnameDAO translates QName IDs into QName instances and vice-versa
|
|
*/
|
|
public void setQnameDAO(QNameDAO qnameDAO)
|
|
{
|
|
this.qnameDAO = qnameDAO;
|
|
}
|
|
|
|
/**
|
|
* @param contentDataDAO used to create and delete content references
|
|
*/
|
|
public void setContentDataDAO(ContentDataDAO contentDataDAO)
|
|
{
|
|
this.contentDataDAO = contentDataDAO;
|
|
}
|
|
|
|
/**
|
|
* @param localeDAO used to handle MLText properties
|
|
*/
|
|
public void setLocaleDAO(LocaleDAO localeDAO)
|
|
{
|
|
this.localeDAO = localeDAO;
|
|
}
|
|
|
|
/**
|
|
* @param usageDAO used to keep content usage calculations in line
|
|
*/
|
|
public void setUsageDAO(UsageDAO usageDAO)
|
|
{
|
|
this.usageDAO = usageDAO;
|
|
}
|
|
|
|
/**
|
|
* Set the cache that maintains the Store root node data
|
|
*
|
|
* @param cache the cache
|
|
*/
|
|
public void setRootNodesCache(SimpleCache<Serializable, Serializable> cache)
|
|
{
|
|
this.rootNodesCache = new EntityLookupCache<StoreRef, Node, Serializable>(
|
|
cache,
|
|
CACHE_REGION_ROOT_NODES,
|
|
new RootNodesCacheCallbackDAO());
|
|
}
|
|
|
|
/**
|
|
* Set the cache that maintains the extended Store root node data
|
|
*
|
|
* @param cache the cache
|
|
*/
|
|
public void setAllRootNodesCache(SimpleCache<StoreRef, Set<NodeRef>> allRootNodesCache)
|
|
{
|
|
this.allRootNodesCache = allRootNodesCache;
|
|
}
|
|
|
|
/**
|
|
* Set the cache that maintains node ID-NodeRef cross referencing data
|
|
*
|
|
* @param cache the cache
|
|
*/
|
|
public void setNodesCache(SimpleCache<Serializable, Serializable> cache)
|
|
{
|
|
this.nodesCache = new EntityLookupCache<Long, Node, NodeRef>(
|
|
cache,
|
|
CACHE_REGION_NODES,
|
|
new NodesCacheCallbackDAO());
|
|
if (cache instanceof TransactionalCache)
|
|
{
|
|
this.nodesTransactionalCache = (TransactionalCache<Serializable, Serializable>) cache;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the cache that maintains the Node QName IDs
|
|
*
|
|
* @param aspectsCache the cache
|
|
*/
|
|
public void setAspectsCache(SimpleCache<NodeVersionKey, Set<QName>> aspectsCache)
|
|
{
|
|
this.aspectsCache = new EntityLookupCache<NodeVersionKey, Set<QName>, Serializable>(
|
|
aspectsCache,
|
|
CACHE_REGION_ASPECTS,
|
|
new AspectsCallbackDAO());
|
|
}
|
|
|
|
/**
|
|
* Set the cache that maintains the Node property values
|
|
*
|
|
* @param propertiesCache the cache
|
|
*/
|
|
public void setPropertiesCache(SimpleCache<NodeVersionKey, Map<QName, Serializable>> propertiesCache)
|
|
{
|
|
this.propertiesCache = new EntityLookupCache<NodeVersionKey, Map<QName, Serializable>, Serializable>(
|
|
propertiesCache,
|
|
CACHE_REGION_PROPERTIES,
|
|
new PropertiesCallbackDAO());
|
|
}
|
|
|
|
/**
|
|
* Set the cache that maintains the Node parent associations
|
|
*
|
|
* @param parentAssocsCache the cache
|
|
*/
|
|
public void setParentAssocsCache(SimpleCache<NodeVersionKey, Serializable> parentAssocsCache)
|
|
{
|
|
this.parentAssocsCache = new EntityLookupCache<NodeVersionKey, ParentAssocsInfo, Serializable>(
|
|
parentAssocsCache,
|
|
CACHE_REGION_PARENT_ASSOCS,
|
|
new ParentAssocsCallbackDAO());
|
|
}
|
|
|
|
/**
|
|
* Set the cache that maintains lookups by child <b>cm:name</b>
|
|
*
|
|
* @param childByNameCache the cache
|
|
*/
|
|
public void setChildByNameCache(SimpleCache<ChildByNameKey, ChildAssocEntity> childByNameCache)
|
|
{
|
|
this.childByNameCache = childByNameCache;
|
|
}
|
|
|
|
/*
|
|
* Initialize
|
|
*/
|
|
|
|
public void init()
|
|
{
|
|
PropertyCheck.mandatory(this, "transactionService", transactionService);
|
|
PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
|
|
PropertyCheck.mandatory(this, "aclDAO", aclDAO);
|
|
PropertyCheck.mandatory(this, "accessControlListDAO", accessControlListDAO);
|
|
PropertyCheck.mandatory(this, "qnameDAO", qnameDAO);
|
|
PropertyCheck.mandatory(this, "contentDataDAO", contentDataDAO);
|
|
PropertyCheck.mandatory(this, "localeDAO", localeDAO);
|
|
PropertyCheck.mandatory(this, "usageDAO", usageDAO);
|
|
|
|
this.nodePropertyHelper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO);
|
|
}
|
|
|
|
/*
|
|
* Server
|
|
*/
|
|
|
|
/**
|
|
* Wrapper to get the server ID within the context of a lock
|
|
*/
|
|
private class ServerIdCallback extends ReadWriteLockExecuter<Long>
|
|
{
|
|
private TransactionAwareSingleton<Long> serverIdStorage = new TransactionAwareSingleton<Long>();
|
|
public Long getWithReadLock() throws Throwable
|
|
{
|
|
return serverIdStorage.get();
|
|
}
|
|
public Long getWithWriteLock() throws Throwable
|
|
{
|
|
if (serverIdStorage.get() != null)
|
|
{
|
|
return serverIdStorage.get();
|
|
}
|
|
// Avoid write operations in read-only transactions
|
|
// ALF-5456: IP address change can cause read-write errors on startup
|
|
if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Server IP address
|
|
String ipAddress = null;
|
|
try
|
|
{
|
|
ipAddress = InetAddress.getLocalHost().getHostAddress();
|
|
}
|
|
catch (UnknownHostException e)
|
|
{
|
|
throw new AlfrescoRuntimeException("Failed to get server IP address", e);
|
|
}
|
|
// Get the server instance
|
|
ServerEntity serverEntity = selectServer(ipAddress);
|
|
if (serverEntity != null)
|
|
{
|
|
serverIdStorage.put(serverEntity.getId());
|
|
return serverEntity.getId();
|
|
}
|
|
// Doesn't exist, so create it
|
|
Long serverId = insertServer(ipAddress);
|
|
serverIdStorage.put(serverId);
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Created server entity: " + serverEntity);
|
|
}
|
|
return serverId;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the ID of the current server, or <tt>null</tt> if there is no ID for the current
|
|
* server and one can't be created.
|
|
*
|
|
* @see ServerIdCallback
|
|
*/
|
|
private Long getServerId()
|
|
{
|
|
return serverIdCallback.execute();
|
|
}
|
|
|
|
/*
|
|
* Cache helpers
|
|
*/
|
|
|
|
private void clearCaches()
|
|
{
|
|
nodesCache.clear();
|
|
aspectsCache.clear();
|
|
propertiesCache.clear();
|
|
parentAssocsCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Invalidate cache entries for all children of a give node. This usually applies
|
|
* where the child associations or nodes are modified en-masse.
|
|
*
|
|
* @param parentNodeId the parent node of all child nodes to be invalidated (may be <tt>null</tt>)
|
|
* @param touchNodes <tt>true<tt> to also touch the nodes
|
|
* @return the number of child associations found (might be capped)
|
|
*/
|
|
private int invalidateNodeChildrenCaches(Long parentNodeId, boolean primary, boolean touchNodes)
|
|
{
|
|
Long txnId = getCurrentTransaction().getId();
|
|
|
|
int count = 0;
|
|
List<Long> childNodeIds = new ArrayList<Long>(256);
|
|
Long minAssocIdInclusive = Long.MIN_VALUE;
|
|
while (minAssocIdInclusive != null)
|
|
{
|
|
childNodeIds.clear();
|
|
List<ChildAssocEntity> childAssocs = selectChildNodeIds(
|
|
parentNodeId,
|
|
Boolean.valueOf(primary),
|
|
minAssocIdInclusive,
|
|
256);
|
|
// Remove the cache entries as we go
|
|
for (ChildAssocEntity childAssoc : childAssocs)
|
|
{
|
|
Long childAssocId = childAssoc.getId();
|
|
if (childAssocId.compareTo(minAssocIdInclusive) < 0)
|
|
{
|
|
throw new RuntimeException("Query results did not increase for assoc ID");
|
|
}
|
|
else
|
|
{
|
|
minAssocIdInclusive = new Long(childAssocId.longValue() + 1L);
|
|
}
|
|
// Invalidate the node cache
|
|
Long childNodeId = childAssoc.getChildNode().getId();
|
|
childNodeIds.add(childNodeId);
|
|
invalidateNodeCaches(childNodeId);
|
|
count++;
|
|
}
|
|
// Bring all the nodes into the transaction, if required
|
|
if (touchNodes)
|
|
{
|
|
updateNodes(txnId, childNodeIds);
|
|
}
|
|
// Now break out if we didn't have the full set of results
|
|
if (childAssocs.size() < 256)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
// Done
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Invalidates all cached artefacts for a particular node, forcing a refresh.
|
|
*
|
|
* @param nodeId the node ID
|
|
*/
|
|
private void invalidateNodeCaches(Long nodeId)
|
|
{
|
|
// Take the current value from the nodesCache and use that to invalidate the other caches
|
|
Node node = nodesCache.getValue(nodeId);
|
|
if (node != null)
|
|
{
|
|
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
|
|
invalidateNodeCaches(nodeVersionKey, true, true, true);
|
|
}
|
|
// Finally remove the node reference
|
|
nodesCache.removeByKey(nodeId);
|
|
}
|
|
|
|
/**
|
|
* Invalidate specific node caches using an exact key
|
|
*
|
|
* @param nodeVersionKey the node ID-VERSION key to use
|
|
*/
|
|
private void invalidateNodeCaches(
|
|
NodeVersionKey nodeVersionKey,
|
|
boolean invalidateNodeAspectsCache,
|
|
boolean invalidateNodePropertiesCache,
|
|
boolean invalidateParentAssocsCache)
|
|
{
|
|
if (invalidateNodeAspectsCache)
|
|
{
|
|
aspectsCache.removeByKey(nodeVersionKey);
|
|
}
|
|
if (invalidateNodePropertiesCache)
|
|
{
|
|
propertiesCache.removeByKey(nodeVersionKey);
|
|
}
|
|
if (invalidateParentAssocsCache)
|
|
{
|
|
parentAssocsCache.removeByKey(nodeVersionKey);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Transactions
|
|
*/
|
|
|
|
private static final String KEY_TRANSACTION = "node.transaction.id";
|
|
|
|
/**
|
|
* Wrapper to update the current transaction to get the change time correct
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.4
|
|
*/
|
|
private class UpdateTransactionListener extends TransactionListenerAdapter
|
|
{
|
|
@Override
|
|
public void beforeCommit(boolean readOnly)
|
|
{
|
|
if (readOnly)
|
|
{
|
|
return;
|
|
}
|
|
TransactionEntity txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
|
|
Long txnId = txn.getId();
|
|
// Update it
|
|
Long now = System.currentTimeMillis();
|
|
updateTransaction(txnId, now);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Returns a new transaction or an existing one if already active
|
|
*/
|
|
private TransactionEntity getCurrentTransaction()
|
|
{
|
|
TransactionEntity txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
|
|
if (txn != null)
|
|
{
|
|
// We have been busy here before
|
|
return txn;
|
|
}
|
|
// Check that this is a writable txn
|
|
if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE)
|
|
{
|
|
throw new ReadOnlyServerException();
|
|
}
|
|
// Have to create a new transaction entry
|
|
Long serverId = getServerId();
|
|
Long now = System.currentTimeMillis();
|
|
String changeTxnId = AlfrescoTransactionSupport.getTransactionId();
|
|
Long txnId = insertTransaction(serverId, changeTxnId, now);
|
|
// Store it for later
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Create txn: " + txnId);
|
|
}
|
|
txn = new TransactionEntity();
|
|
txn.setId(txnId);
|
|
txn.setChangeTxnId(changeTxnId);
|
|
txn.setCommitTimeMs(now);
|
|
ServerEntity server = new ServerEntity();
|
|
server.setId(serverId);
|
|
txn.setServer(server);
|
|
|
|
AlfrescoTransactionSupport.bindResource(KEY_TRANSACTION, txn);
|
|
// Listen for the end of the transaction
|
|
AlfrescoTransactionSupport.bindListener(updateTransactionListener);
|
|
// Done
|
|
return txn;
|
|
}
|
|
|
|
public Long getCurrentTransactionId(boolean ensureNew)
|
|
{
|
|
TransactionEntity txn;
|
|
if (ensureNew)
|
|
{
|
|
txn = getCurrentTransaction();
|
|
}
|
|
else
|
|
{
|
|
txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
|
|
}
|
|
return txn == null ? null : txn.getId();
|
|
}
|
|
|
|
/*
|
|
* Stores
|
|
*/
|
|
|
|
public List<Pair<Long, StoreRef>> getStores()
|
|
{
|
|
List<StoreEntity> storeEntities = selectAllStores();
|
|
List<Pair<Long, StoreRef>> storeRefs = new ArrayList<Pair<Long,StoreRef>>(storeEntities.size());
|
|
for (StoreEntity storeEntity : storeEntities)
|
|
{
|
|
storeRefs.add(new Pair<Long, StoreRef>(storeEntity.getId(), storeEntity.getStoreRef()));
|
|
}
|
|
return storeRefs;
|
|
}
|
|
|
|
/**
|
|
* @throws InvalidStoreRefException if the store is invalid
|
|
*/
|
|
private StoreEntity getStoreNotNull(StoreRef storeRef)
|
|
{
|
|
Pair<StoreRef, Node> rootNodePair = rootNodesCache.getByKey(storeRef);
|
|
if (rootNodePair == null)
|
|
{
|
|
throw new InvalidStoreRefException(storeRef);
|
|
}
|
|
else
|
|
{
|
|
return rootNodePair.getSecond().getStore();
|
|
}
|
|
}
|
|
|
|
public boolean exists(StoreRef storeRef)
|
|
{
|
|
Pair<StoreRef, Node> rootNodePair = rootNodesCache.getByKey(storeRef);
|
|
return rootNodePair != null;
|
|
}
|
|
|
|
public Pair<Long, NodeRef> getRootNode(StoreRef storeRef)
|
|
{
|
|
Pair<StoreRef, Node> rootNodePair = rootNodesCache.getByKey(storeRef);
|
|
if (rootNodePair == null)
|
|
{
|
|
throw new InvalidStoreRefException(storeRef);
|
|
}
|
|
else
|
|
{
|
|
return rootNodePair.getSecond().getNodePair();
|
|
}
|
|
}
|
|
|
|
public Set<NodeRef> getAllRootNodes(StoreRef storeRef)
|
|
{
|
|
Set<NodeRef> rootNodes = allRootNodesCache.get(storeRef);
|
|
if (rootNodes == null)
|
|
{
|
|
final Map<StoreRef, Set<NodeRef>> allRootNodes = new HashMap<StoreRef, Set<NodeRef>>(97);
|
|
getNodesWithAspects(Collections.singleton(ContentModel.ASPECT_ROOT), 0L, Long.MAX_VALUE, new NodeRefQueryCallback()
|
|
{
|
|
@Override
|
|
public boolean handle(Pair<Long, NodeRef> nodePair)
|
|
{
|
|
NodeRef nodeRef = nodePair.getSecond();
|
|
StoreRef storeRef = nodeRef.getStoreRef();
|
|
Set<NodeRef> rootNodes = allRootNodes.get(storeRef);
|
|
if (rootNodes == null)
|
|
{
|
|
rootNodes = new HashSet<NodeRef>(97);
|
|
allRootNodes.put(storeRef, rootNodes);
|
|
}
|
|
rootNodes.add(nodeRef);
|
|
return true;
|
|
}
|
|
});
|
|
rootNodes = allRootNodes.get(storeRef);
|
|
if (rootNodes == null)
|
|
{
|
|
rootNodes = Collections.emptySet();
|
|
allRootNodes.put(storeRef, rootNodes);
|
|
}
|
|
for (Map.Entry<StoreRef, Set<NodeRef>> entry : allRootNodes.entrySet())
|
|
{
|
|
StoreRef entryStoreRef = entry.getKey();
|
|
// Prevent unnecessary cross-invalidation
|
|
if (!allRootNodesCache.contains(entryStoreRef))
|
|
{
|
|
allRootNodesCache.put(entryStoreRef, entry.getValue());
|
|
}
|
|
}
|
|
}
|
|
return rootNodes;
|
|
}
|
|
|
|
public Pair<Long, NodeRef> newStore(StoreRef storeRef)
|
|
{
|
|
// Create the store
|
|
StoreEntity store = new StoreEntity();
|
|
store.setProtocol(storeRef.getProtocol());
|
|
store.setIdentifier(storeRef.getIdentifier());
|
|
|
|
Long storeId = insertStore(store);
|
|
store.setId(storeId);
|
|
|
|
// Get an ACL for the root node
|
|
Long aclId = aclDAO.createAccessControlList();
|
|
|
|
// Create a root node
|
|
Long nodeTypeQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_STOREROOT).getFirst();
|
|
NodeEntity rootNode = newNodeImpl(store, null, nodeTypeQNameId, null, aclId, false, null);
|
|
Long rootNodeId = rootNode.getId();
|
|
addNodeAspects(rootNodeId, Collections.singleton(ContentModel.ASPECT_ROOT));
|
|
|
|
// Now update the store with the root node ID
|
|
store.setRootNode(rootNode);
|
|
updateStoreRoot(store);
|
|
|
|
// Push the value into the caches
|
|
rootNodesCache.setValue(storeRef, rootNode);
|
|
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Created store: \n" + " " + store);
|
|
}
|
|
return new Pair<Long, NodeRef>(rootNode.getId(), rootNode.getNodeRef());
|
|
}
|
|
|
|
@Override
|
|
public void moveStore(StoreRef oldStoreRef, StoreRef newStoreRef)
|
|
{
|
|
StoreEntity store = getStoreNotNull(oldStoreRef);
|
|
store.setProtocol(newStoreRef.getProtocol());
|
|
store.setIdentifier(newStoreRef.getIdentifier());
|
|
// Update it
|
|
int count = updateStore(store);
|
|
if (count != 1)
|
|
{
|
|
throw new ConcurrencyFailureException("Store not updated: " + oldStoreRef);
|
|
}
|
|
// All the NodeRef-based caches are invalid. ID-based caches are fine.
|
|
rootNodesCache.removeByKey(oldStoreRef);
|
|
allRootNodesCache.remove(oldStoreRef);
|
|
nodesCache.clear();
|
|
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Moved store: " + oldStoreRef + " --> " + newStoreRef);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback to cache store root nodes by {@link StoreRef}.
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.4
|
|
*/
|
|
private class RootNodesCacheCallbackDAO extends EntityLookupCallbackDAOAdaptor<StoreRef, Node, Serializable>
|
|
{
|
|
/**
|
|
* @throws UnsupportedOperationException Stores must be created externally
|
|
*/
|
|
public Pair<StoreRef, Node> createValue(Node value)
|
|
{
|
|
throw new UnsupportedOperationException("Root node creation is done externally: " + value);
|
|
}
|
|
|
|
/**
|
|
* @param key the store ID
|
|
*/
|
|
public Pair<StoreRef, Node> findByKey(StoreRef storeRef)
|
|
{
|
|
NodeEntity node = selectStoreRootNode(storeRef);
|
|
return node == null ? null : new Pair<StoreRef, Node>(storeRef, node);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Nodes
|
|
*/
|
|
|
|
/**
|
|
* Callback to cache nodes by ID and {@link NodeRef}. When looking up objects based on the
|
|
* value key, only the referencing properties need be populated. <b>ALL</b> nodes are cached,
|
|
* not just live nodes.
|
|
*
|
|
* @see NodeEntity
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.4
|
|
*/
|
|
private class NodesCacheCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Node, NodeRef>
|
|
{
|
|
/**
|
|
* @throws UnsupportedOperationException Nodes are created externally
|
|
*/
|
|
public Pair<Long, Node> createValue(Node value)
|
|
{
|
|
throw new UnsupportedOperationException("Node creation is done externally: " + value);
|
|
}
|
|
|
|
/**
|
|
* @param nodeId the key node ID
|
|
*/
|
|
public Pair<Long, Node> findByKey(Long nodeId)
|
|
{
|
|
NodeEntity node = selectNodeById(nodeId, null);
|
|
if (node != null)
|
|
{
|
|
// Lock it to prevent 'accidental' modification
|
|
node.lock();
|
|
return new Pair<Long, Node>(nodeId, node);
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Returns the Node's NodeRef
|
|
*/
|
|
@Override
|
|
public NodeRef getValueKey(Node value)
|
|
{
|
|
return value.getNodeRef();
|
|
}
|
|
|
|
/**
|
|
* Looks the node up based on the NodeRef of the given node
|
|
*/
|
|
@Override
|
|
public Pair<Long, Node> findByValue(Node node)
|
|
{
|
|
NodeRef nodeRef = node.getNodeRef();
|
|
node = selectNodeByNodeRef(nodeRef, null);
|
|
if (node != null)
|
|
{
|
|
// Lock it to prevent 'accidental' modification
|
|
node.lock();
|
|
return new Pair<Long, Node>(node.getId(), node);
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean exists(Long nodeId)
|
|
{
|
|
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
|
|
return pair != null && !pair.getSecond().getDeleted();
|
|
}
|
|
|
|
public boolean exists(NodeRef nodeRef)
|
|
{
|
|
NodeEntity node = new NodeEntity(nodeRef);
|
|
Pair<Long, Node> pair = nodesCache.getByValue(node);
|
|
return pair != null && !pair.getSecond().getDeleted();
|
|
}
|
|
|
|
@Override
|
|
public boolean isInCurrentTxn(Long nodeId)
|
|
{
|
|
Long currentTxnId = getCurrentTransactionId(false);
|
|
if (currentTxnId == null)
|
|
{
|
|
// No transactional changes have been made to any nodes, therefore the node cannot
|
|
// be part of the current transaction
|
|
return false;
|
|
}
|
|
Node node = getNodeNotNull(nodeId);
|
|
Long nodeTxnId = node.getTransaction().getId();
|
|
return nodeTxnId.equals(currentTxnId);
|
|
}
|
|
|
|
public Status getNodeRefStatus(NodeRef nodeRef)
|
|
{
|
|
Node node = new NodeEntity(nodeRef);
|
|
Pair<Long, Node> nodePair = nodesCache.getByValue(node);
|
|
// The nodesCache gets both live and deleted nodes.
|
|
if (nodePair == null)
|
|
{
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
return nodePair.getSecond().getNodeStatus();
|
|
}
|
|
}
|
|
|
|
public Pair<Long, NodeRef> getNodePair(NodeRef nodeRef)
|
|
{
|
|
NodeEntity node = new NodeEntity(nodeRef);
|
|
Pair<Long, Node> pair = nodesCache.getByValue(node);
|
|
// The noderef is currently invalid WRT to the cache. Let's just check the database
|
|
if (pair == null || pair.getSecond().getDeleted())
|
|
{
|
|
Node dbNode = selectNodeByNodeRef(nodeRef, null);
|
|
if (dbNode == null)
|
|
{
|
|
// The DB agrees. This is an invalid noderef. Why are you trying to use it?
|
|
return null;
|
|
}
|
|
Long nodeId = dbNode.getId();
|
|
if (dbNode.getDeleted())
|
|
{
|
|
// The node is actually deleted as the cache said.
|
|
throw new InvalidNodeRefException(nodeRef);
|
|
}
|
|
else
|
|
{
|
|
// The cache was wrong, possibly due to it caching negative results earlier. Let's repair it and carry on!
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Stale cache detected for Node " + nodeRef + ": previously though to be deleted. Repairing cache.");
|
|
}
|
|
invalidateNodeCaches(nodeId);
|
|
nodesCache.setValue(dbNode.getId(), dbNode);
|
|
return dbNode.getNodePair();
|
|
}
|
|
}
|
|
return pair.getSecond().getNodePair();
|
|
}
|
|
|
|
public Pair<Long, NodeRef> getNodePair(Long nodeId)
|
|
{
|
|
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
|
|
return (pair == null || pair.getSecond().getDeleted()) ? null : pair.getSecond().getNodePair();
|
|
}
|
|
|
|
/**
|
|
* Find an undeleted node
|
|
*
|
|
* @param nodeId the node
|
|
* @return Returns the fully populated node
|
|
* @throws ConcurrencyFailureException if the ID doesn't reference a <b>live</b> node
|
|
*/
|
|
private Node getNodeNotNull(Long nodeId)
|
|
{
|
|
return getNodeNotNullImpl(nodeId, false);
|
|
}
|
|
|
|
private Node getNodeNotNullImpl(Long nodeId, boolean deleted)
|
|
{
|
|
Pair<Long, Node> pair = nodesCache.getByKey(nodeId);
|
|
|
|
if (pair == null || (pair.getSecond().getDeleted() && (!deleted)))
|
|
{
|
|
// Force a removal from the cache
|
|
nodesCache.removeByKey(nodeId);
|
|
// Go back to the database and get what is there
|
|
NodeEntity dbNode = selectNodeById(nodeId, null);
|
|
if (pair == null)
|
|
{
|
|
throw new ConcurrencyFailureException(
|
|
"No node exists: \n" +
|
|
" ID: " + nodeId + "\n" +
|
|
" DB row: " + dbNode);
|
|
}
|
|
else
|
|
{
|
|
logger.warn("No live node exists: \n" +
|
|
" ID: " + nodeId + "\n" +
|
|
" Cache row: " + pair.getSecond() + "\n" +
|
|
" DB row: " + dbNode);
|
|
throw new NotLiveNodeException(pair);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return pair.getSecond();
|
|
}
|
|
}
|
|
|
|
public QName getNodeType(Long nodeId)
|
|
{
|
|
Node node = getNodeNotNull(nodeId);
|
|
Long nodeTypeQNameId = node.getTypeQNameId();
|
|
return qnameDAO.getQName(nodeTypeQNameId).getSecond();
|
|
}
|
|
|
|
public Long getNodeAclId(Long nodeId)
|
|
{
|
|
Node node = getNodeNotNull(nodeId);
|
|
return node.getAclId();
|
|
}
|
|
|
|
@Override
|
|
public ChildAssocEntity newNode(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
StoreRef storeRef,
|
|
String uuid,
|
|
QName nodeTypeQName,
|
|
Locale nodeLocale,
|
|
String childNodeName,
|
|
Map<QName, Serializable> auditableProperties) throws InvalidTypeException
|
|
{
|
|
Assert.notNull(parentNodeId, "parentNodeId");
|
|
Assert.notNull(assocTypeQName, "assocTypeQName");
|
|
Assert.notNull(assocQName, "assocQName");
|
|
Assert.notNull(storeRef, "storeRef");
|
|
|
|
if (auditableProperties == null)
|
|
{
|
|
auditableProperties = Collections.emptyMap();
|
|
}
|
|
|
|
// Get the parent node
|
|
Node parentNode = getNodeNotNull(parentNodeId);
|
|
// Find an initial ACL for the node
|
|
Long parentAclId = parentNode.getAclId();
|
|
Long childAclId = null;
|
|
if (parentAclId != null)
|
|
{
|
|
try
|
|
{
|
|
Long inheritedACL = aclDAO.getInheritedAccessControlList(parentAclId);
|
|
AccessControlListProperties inheritedAcl = aclDAO.getAccessControlListProperties(inheritedACL);
|
|
if (inheritedAcl != null)
|
|
{
|
|
childAclId = inheritedAcl.getId();
|
|
}
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
// The get* calls above actually do writes. So pessimistically get rid of the
|
|
// parent node from the cache in case it was wrong somehow.
|
|
invalidateNodeCaches(parentNodeId);
|
|
}
|
|
}
|
|
// Build the cm:auditable properties
|
|
AuditablePropertiesEntity auditableProps = new AuditablePropertiesEntity();
|
|
boolean setAuditProps = auditableProps.setAuditValues(null, null, auditableProperties);
|
|
if (!setAuditProps)
|
|
{
|
|
// No cm:auditable properties were supplied
|
|
auditableProps = null;
|
|
}
|
|
|
|
// Get the store
|
|
StoreEntity store = getStoreNotNull(storeRef);
|
|
// Create the node (it is not a root node)
|
|
Long nodeTypeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
|
|
Long nodeLocaleId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
|
|
NodeEntity node = newNodeImpl(store, uuid, nodeTypeQNameId, nodeLocaleId, childAclId, false, auditableProps);
|
|
Long nodeId = node.getId();
|
|
|
|
// Protect the node's cm:auditable if it was explicitly set
|
|
if (setAuditProps)
|
|
{
|
|
NodeRef nodeRef = node.getNodeRef();
|
|
policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
|
|
}
|
|
|
|
// Now create a primary association for it
|
|
if (childNodeName == null)
|
|
{
|
|
childNodeName = node.getUuid();
|
|
}
|
|
ChildAssocEntity assoc = newChildAssocImpl(
|
|
parentNodeId, nodeId, true, assocTypeQName, assocQName, childNodeName, 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
|
|
* @param deleted <tt>true</tt> to create an already-deleted node (used for leaving trails of moved nodes)
|
|
* @throws NodeExistsException if the target reference is already taken by a live node
|
|
*/
|
|
private NodeEntity newNodeImpl(
|
|
StoreEntity store,
|
|
String uuid,
|
|
Long nodeTypeQNameId,
|
|
Long nodeLocaleId,
|
|
Long aclId,
|
|
boolean deleted,
|
|
AuditablePropertiesEntity auditableProps) throws InvalidTypeException
|
|
{
|
|
NodeEntity node = new NodeEntity();
|
|
// Store
|
|
node.setStore(store);
|
|
// UUID
|
|
if (uuid == null)
|
|
{
|
|
node.setUuid(GUID.generate());
|
|
}
|
|
else
|
|
{
|
|
node.setUuid(uuid);
|
|
}
|
|
// QName
|
|
node.setTypeQNameId(nodeTypeQNameId);
|
|
QName nodeTypeQName = qnameDAO.getQName(nodeTypeQNameId).getSecond();
|
|
// Locale
|
|
if (nodeLocaleId == null)
|
|
{
|
|
nodeLocaleId = localeDAO.getOrCreateDefaultLocalePair().getFirst();
|
|
}
|
|
node.setLocaleId(nodeLocaleId);
|
|
// ACL (may be null)
|
|
node.setAclId(aclId);
|
|
// Deleted
|
|
node.setDeleted(deleted);
|
|
// Transaction
|
|
TransactionEntity txn = getCurrentTransaction();
|
|
node.setTransaction(txn);
|
|
|
|
// Audit
|
|
boolean addAuditableAspect = false;
|
|
if (auditableProps != null)
|
|
{
|
|
// Client-supplied cm:auditable values
|
|
node.setAuditableProperties(auditableProps);
|
|
addAuditableAspect = true;
|
|
}
|
|
else if (AuditablePropertiesEntity.hasAuditableAspect(nodeTypeQName, dictionaryService))
|
|
{
|
|
// Automatically-generated cm:auditable values
|
|
auditableProps = new AuditablePropertiesEntity();
|
|
auditableProps.setAuditValues(null, null, true, 0L);
|
|
node.setAuditableProperties(auditableProps);
|
|
addAuditableAspect = true;
|
|
}
|
|
|
|
Long id = null;
|
|
Savepoint savepoint = controlDAO.createSavepoint("newNodeImpl");
|
|
try
|
|
{
|
|
// First try a straight insert and risk the constraint violation if the node exists
|
|
id = insertNode(node);
|
|
controlDAO.releaseSavepoint(savepoint);
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
controlDAO.rollbackToSavepoint(savepoint);
|
|
// This is probably because there is an existing node. We can handle existing deleted nodes.
|
|
NodeRef targetNodeRef = node.getNodeRef();
|
|
NodeEntity liveNode = selectNodeByNodeRef(targetNodeRef, false); // Only look for live nodes
|
|
if (liveNode != null)
|
|
{
|
|
throw new NodeExistsException(liveNode.getNodePair(), e);
|
|
}
|
|
NodeEntity deletedNode = selectNodeByNodeRef(targetNodeRef, true); // Only look for deleted nodes
|
|
if (deletedNode != null)
|
|
{
|
|
Long deletedNodeId = deletedNode.getId();
|
|
deleteNodeById(deletedNodeId, true);
|
|
// Now repeat, but let any further problems just be thrown out
|
|
id = insertNode(node);
|
|
}
|
|
else
|
|
{
|
|
throw new AlfrescoRuntimeException("Failed to insert new node: " + node, e);
|
|
}
|
|
}
|
|
node.setId(id);
|
|
|
|
Set<QName> nodeAspects = null;
|
|
if (addAuditableAspect && !deleted)
|
|
{
|
|
Long auditableAspectQNameId = qnameDAO.getOrCreateQName(ContentModel.ASPECT_AUDITABLE).getFirst();
|
|
insertNodeAspect(id, auditableAspectQNameId);
|
|
nodeAspects = Collections.<QName>singleton(ContentModel.ASPECT_AUDITABLE);
|
|
}
|
|
else
|
|
{
|
|
nodeAspects = Collections.<QName>emptySet();
|
|
}
|
|
|
|
// Lock the node and cache
|
|
node.lock();
|
|
nodesCache.setValue(id, node);
|
|
// Pre-populate some of the other caches so that we don't immediately query
|
|
setNodeAspectsCached(id, nodeAspects);
|
|
setNodePropertiesCached(id, Collections.<QName, Serializable>emptyMap());
|
|
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Created new node: \n" + " " + node);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
public Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>> moveNode(
|
|
final Long childNodeId,
|
|
final Long newParentNodeId,
|
|
final QName assocTypeQName,
|
|
final QName assocQName)
|
|
{
|
|
final Node newParentNode = getNodeNotNull(newParentNodeId);
|
|
final StoreEntity newParentStore = newParentNode.getStore();
|
|
final Node childNode = getNodeNotNull(childNodeId);
|
|
final StoreEntity childStore = childNode.getStore();
|
|
ChildAssocEntity primaryParentAssoc = getPrimaryParentAssocImpl(childNodeId);
|
|
final Long oldParentAclId;
|
|
if (primaryParentAssoc == null)
|
|
{
|
|
oldParentAclId = null;
|
|
}
|
|
else
|
|
{
|
|
if (primaryParentAssoc.getParentNode() == null)
|
|
{
|
|
oldParentAclId = null;
|
|
}
|
|
else
|
|
{
|
|
Long oldParentNodeId = primaryParentAssoc.getParentNode().getId();
|
|
oldParentAclId = getNodeNotNull(oldParentNodeId).getAclId();
|
|
}
|
|
}
|
|
|
|
// Need the child node's name here in case it gets removed
|
|
final String childNodeName = (String) getNodeProperty(childNodeId, ContentModel.PROP_NAME);
|
|
|
|
// First attempt to move the node, which may rollback to a savepoint
|
|
Node newChildNode = childNode;
|
|
// Store
|
|
if (!childStore.getId().equals(newParentStore.getId()))
|
|
{
|
|
// Remove the cm:auditable aspect from the source node
|
|
// Remove the cm:auditable aspect from the old node as the new one will get new values as required
|
|
Set<Long> aspectIdsToDelete = qnameDAO.convertQNamesToIds(
|
|
Collections.singleton(ContentModel.ASPECT_AUDITABLE),
|
|
true);
|
|
deleteNodeAspects(childNodeId, aspectIdsToDelete);
|
|
// ... but make sure we copy over the cm:auditable data from the originating node
|
|
AuditablePropertiesEntity auditableProps = childNode.getAuditableProperties();
|
|
// Create a new node and copy all the data over to it
|
|
newChildNode = newNodeImpl(
|
|
newParentStore,
|
|
childNode.getUuid(),
|
|
childNode.getTypeQNameId(),
|
|
childNode.getLocaleId(),
|
|
childNode.getAclId(),
|
|
false,
|
|
auditableProps);
|
|
Long newChildNodeId = newChildNode.getId();
|
|
moveNodeData(
|
|
childNode.getId(),
|
|
newChildNodeId);
|
|
// The new node will have new data not present in the cache, yet
|
|
invalidateNodeCaches(newChildNodeId);
|
|
invalidateNodeChildrenCaches(newChildNodeId, true, true);
|
|
invalidateNodeChildrenCaches(newChildNodeId, false, true);
|
|
// Now update the original to be 'deleted'
|
|
NodeUpdateEntity childNodeUpdate = new NodeUpdateEntity();
|
|
childNodeUpdate.setId(childNodeId);
|
|
childNodeUpdate.setAclId(null);
|
|
childNodeUpdate.setUpdateAclId(true);
|
|
childNodeUpdate.setTypeQNameId(qnameDAO.getOrCreateQName(ContentModel.TYPE_CMOBJECT).getFirst());
|
|
childNodeUpdate.setUpdateTypeQNameId(true);
|
|
childNodeUpdate.setLocaleId(localeDAO.getOrCreateDefaultLocalePair().getFirst());
|
|
childNodeUpdate.setUpdateLocaleId(true);
|
|
childNodeUpdate.setDeleted(Boolean.TRUE);
|
|
childNodeUpdate.setUpdateDeleted(true);
|
|
// Update the entity.
|
|
// Note: We don't use delete here because that will attempt to clean everything up again.
|
|
updateNodeImpl(childNode, childNodeUpdate, null);
|
|
// There is no need to invalidate the caches as the touched node's version will have progressed
|
|
}
|
|
else
|
|
{
|
|
// Touch the node; make sure parent assocs are invalidated
|
|
touchNode(childNodeId, null, null, false, false, true);
|
|
}
|
|
|
|
final Long newChildNodeId = newChildNode.getId();
|
|
// Now update the primary parent assoc
|
|
RetryingCallback<Integer> callback = new RetryingCallback<Integer>()
|
|
{
|
|
public Integer execute() throws Throwable
|
|
{
|
|
// Because we are retrying in-transaction i.e. absorbing exceptions, we need a Savepoint
|
|
Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
|
|
// We use the child node's UUID if there is no cm:name
|
|
String childNodeNameToUse = childNodeName == null ? childNode.getUuid() : childNodeName;
|
|
|
|
try
|
|
{
|
|
int updated = updatePrimaryParentAssocs(
|
|
newChildNodeId,
|
|
newParentNodeId,
|
|
assocTypeQName,
|
|
assocQName,
|
|
childNodeNameToUse);
|
|
controlDAO.releaseSavepoint(savepoint);
|
|
return updated;
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
controlDAO.rollbackToSavepoint(savepoint);
|
|
// We assume that this is from the child cm:name constraint violation
|
|
throw new DuplicateChildNodeNameException(
|
|
newParentNode.getNodeRef(),
|
|
assocTypeQName,
|
|
childNodeName,
|
|
e);
|
|
}
|
|
}
|
|
};
|
|
childAssocRetryingHelper.doWithRetry(callback);
|
|
|
|
// Check for cyclic relationships
|
|
// TODO: This adds a lot of overhead when moving hierarchies.
|
|
// While getPaths is faster, it would be better to avoid the parentAssocsCache
|
|
// completely.
|
|
getPaths(newChildNode.getNodePair(), false);
|
|
// cycleCheck(newChildNodeId);
|
|
|
|
// Update ACLs for moved tree
|
|
Long newParentAclId = newParentNode.getAclId();
|
|
accessControlListDAO.updateInheritance(newChildNodeId, oldParentAclId, newParentAclId);
|
|
|
|
// Done
|
|
Pair<Long, ChildAssociationRef> assocPair = getPrimaryParentAssoc(newChildNode.getId());
|
|
Pair<Long, NodeRef> nodePair = newChildNode.getNodePair();
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Moved node: " + assocPair + " ... " + nodePair);
|
|
}
|
|
return new Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>>(assocPair, nodePair);
|
|
}
|
|
|
|
@Override
|
|
public boolean updateNode(Long nodeId, QName nodeTypeQName, Locale nodeLocale)
|
|
{
|
|
// Get the existing node; we need to check for a change in store or UUID
|
|
Node oldNode = getNodeNotNull(nodeId);
|
|
final Long nodeTypeQNameId;
|
|
if (nodeTypeQName == null)
|
|
{
|
|
nodeTypeQNameId = oldNode.getTypeQNameId();
|
|
}
|
|
else
|
|
{
|
|
nodeTypeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
|
|
}
|
|
final Long nodeLocaleId;
|
|
if (nodeLocale == null)
|
|
{
|
|
nodeLocaleId = oldNode.getLocaleId();
|
|
}
|
|
else
|
|
{
|
|
nodeLocaleId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
|
|
}
|
|
|
|
// Wrap all the updates into one
|
|
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
|
|
nodeUpdate.setId(nodeId);
|
|
nodeUpdate.setStore(oldNode.getStore()); // Need node reference
|
|
nodeUpdate.setUuid(oldNode.getUuid()); // Need node reference
|
|
// TypeQName (if necessary)
|
|
if (!nodeTypeQNameId.equals(oldNode.getTypeQNameId()))
|
|
{
|
|
nodeUpdate.setTypeQNameId(nodeTypeQNameId);
|
|
nodeUpdate.setUpdateTypeQNameId(true);
|
|
}
|
|
// Locale (if necessary)
|
|
if (!nodeLocaleId.equals(oldNode.getLocaleId()))
|
|
{
|
|
nodeUpdate.setLocaleId(nodeLocaleId);
|
|
nodeUpdate.setUpdateLocaleId(true);
|
|
}
|
|
|
|
return updateNodeImpl(oldNode, nodeUpdate, null);
|
|
}
|
|
|
|
|
|
@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);
|
|
}
|
|
catch (DataIntegrityViolationException e)
|
|
{
|
|
// The ID doesn't reference a live node.
|
|
// We do nothing w.r.t. touching
|
|
return false;
|
|
}
|
|
|
|
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
|
|
nodeUpdate.setId(nodeId);
|
|
nodeUpdate.setAuditableProperties(auditableProps);
|
|
// Update it
|
|
boolean updatedNode = updateNodeImpl(node, nodeUpdate, nodeAspects);
|
|
// Handle the cache invalidation requests
|
|
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
|
|
if (updatedNode)
|
|
{
|
|
Node newNode = getNodeNotNull(nodeId);
|
|
NodeVersionKey newNodeVersionKey = newNode.getNodeVersionKey();
|
|
// The version will have moved on, effectively rendering our caches invalid.
|
|
// Copy over caches that DON'T need invalidating
|
|
if (!invalidateNodeAspectsCache)
|
|
{
|
|
copyNodeAspectsCached(nodeVersionKey, newNodeVersionKey);
|
|
}
|
|
if (!invalidateNodePropertiesCache)
|
|
{
|
|
copyNodePropertiesCached(nodeVersionKey, newNodeVersionKey);
|
|
}
|
|
if (!invalidateParentAssocsCache)
|
|
{
|
|
copyParentAssocsCached(nodeVersionKey, newNodeVersionKey);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The node was not touched. By definition it MUST be in the current transaction.
|
|
// We invalidate the caches as specifically requested
|
|
invalidateNodeCaches(
|
|
nodeVersionKey,
|
|
invalidateNodeAspectsCache,
|
|
invalidateNodePropertiesCache,
|
|
invalidateParentAssocsCache);
|
|
}
|
|
|
|
return updatedNode;
|
|
}
|
|
|
|
/**
|
|
* Helper method that updates the node, bringing it into the current transaction with
|
|
* the appropriate <b>cm:auditable</b> and transaction behaviour.
|
|
* <p>
|
|
* If the <tt>NodeRef</tt> of the node is changing (usually a store move) then deleted
|
|
* nodes are cleaned out where they might exist.
|
|
*
|
|
* @param oldNode the existing node, fully populated
|
|
* @param nodeUpdate the node update with all update elements populated
|
|
* @param nodeAspects the node's aspects or <tt>null</tt> to look them up
|
|
* @return <tt>true</tt> if any updates were made
|
|
*/
|
|
private boolean updateNodeImpl(Node oldNode, NodeUpdateEntity nodeUpdate, Set<QName> nodeAspects)
|
|
{
|
|
Long nodeId = oldNode.getId();
|
|
|
|
// Make sure that the ID has been populated
|
|
if (!EqualsHelper.nullSafeEquals(nodeId, nodeUpdate.getId()))
|
|
{
|
|
throw new IllegalArgumentException("NodeUpdateEntity node ID is not correct: " + nodeUpdate);
|
|
}
|
|
|
|
// Copy of the reference data
|
|
nodeUpdate.setStore(oldNode.getStore());
|
|
nodeUpdate.setUuid(oldNode.getUuid());
|
|
|
|
// Ensure that other values are set for completeness when caching
|
|
if (!nodeUpdate.isUpdateTypeQNameId())
|
|
{
|
|
nodeUpdate.setTypeQNameId(oldNode.getTypeQNameId());
|
|
}
|
|
if (!nodeUpdate.isUpdateLocaleId())
|
|
{
|
|
nodeUpdate.setLocaleId(oldNode.getLocaleId());
|
|
}
|
|
if (!nodeUpdate.isUpdateAclId())
|
|
{
|
|
nodeUpdate.setAclId(oldNode.getAclId());
|
|
}
|
|
if (!nodeUpdate.isUpdateDeleted())
|
|
{
|
|
nodeUpdate.setDeleted(oldNode.getDeleted());
|
|
}
|
|
|
|
nodeUpdate.setVersion(oldNode.getVersion());
|
|
// Update the transaction
|
|
TransactionEntity txn = getCurrentTransaction();
|
|
nodeUpdate.setTransaction(txn);
|
|
if (!txn.getId().equals(oldNode.getTransaction().getId()))
|
|
{
|
|
// Only update if the txn has changed
|
|
nodeUpdate.setUpdateTransaction(true);
|
|
}
|
|
// Update auditable
|
|
if (nodeAspects == null)
|
|
{
|
|
nodeAspects = getNodeAspects(nodeId);
|
|
}
|
|
if (nodeAspects.contains(ContentModel.ASPECT_AUDITABLE))
|
|
{
|
|
NodeRef oldNodeRef = oldNode.getNodeRef();
|
|
if (policyBehaviourFilter.isEnabled(oldNodeRef, ContentModel.ASPECT_AUDITABLE))
|
|
{
|
|
// Make sure that auditable properties are present
|
|
AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
|
|
if (auditableProps == null)
|
|
{
|
|
auditableProps = new AuditablePropertiesEntity();
|
|
}
|
|
else
|
|
{
|
|
auditableProps = new AuditablePropertiesEntity(auditableProps);
|
|
}
|
|
long modifiedDateToleranceMs = 1000L;
|
|
|
|
if (nodeUpdate.isUpdateTransaction())
|
|
{
|
|
// allow update cm:modified property for new transaction
|
|
modifiedDateToleranceMs = 0L;
|
|
}
|
|
|
|
boolean updateAuditableProperties = auditableProps.setAuditValues(null, null, false, modifiedDateToleranceMs);
|
|
nodeUpdate.setAuditableProperties(auditableProps);
|
|
nodeUpdate.setUpdateAuditableProperties(updateAuditableProperties);
|
|
}
|
|
else if (nodeUpdate.getAuditableProperties() == null)
|
|
{
|
|
// cache the explicit setting of auditable properties when creating node (note: auditable aspect is not yet present)
|
|
AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
|
|
if (auditableProps != null)
|
|
{
|
|
nodeUpdate.setAuditableProperties(auditableProps); // Can reuse the locked instance
|
|
nodeUpdate.setUpdateAuditableProperties(true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ALF-4117: NodeDAO: Allow cm:auditable to be set
|
|
// The nodeUpdate had auditable properties set, so we just use that directly
|
|
nodeUpdate.setUpdateAuditableProperties(true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Make sure that any auditable properties are removed
|
|
AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
|
|
if (auditableProps != null)
|
|
{
|
|
nodeUpdate.setAuditableProperties(null);
|
|
nodeUpdate.setUpdateAuditableProperties(true);
|
|
}
|
|
}
|
|
|
|
// Just bug out if nothing has changed
|
|
if (!nodeUpdate.isUpdateAnything())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// The node is remaining in the current store
|
|
int count = 0;
|
|
Throwable concurrencyException = null;
|
|
try
|
|
{
|
|
count = updateNode(nodeUpdate);
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
concurrencyException = e;
|
|
}
|
|
// Do concurrency check
|
|
if (count != 1)
|
|
{
|
|
// Drop the value from the cache in case the cache is stale
|
|
nodesCache.removeByKey(nodeId);
|
|
nodesCache.removeByValue(nodeUpdate);
|
|
|
|
throw new ConcurrencyFailureException("Failed to update node " + nodeId, concurrencyException);
|
|
}
|
|
else
|
|
{
|
|
// Check for wrap-around in the version number
|
|
if (nodeUpdate.getVersion().equals(LONG_ZERO))
|
|
{
|
|
// The version was wrapped back to zero
|
|
// The caches that are keyed by version are now unreliable
|
|
propertiesCache.clear();
|
|
aspectsCache.clear();
|
|
parentAssocsCache.clear();
|
|
}
|
|
// Update the caches
|
|
nodeUpdate.lock();
|
|
nodesCache.setValue(nodeId, nodeUpdate);
|
|
// The node's version has moved on so no need to invalidate caches
|
|
}
|
|
|
|
// Done
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug(
|
|
"Updated Node: \n" +
|
|
" OLD: " + oldNode + "\n" +
|
|
" NEW: " + nodeUpdate);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void setNodeAclId(Long nodeId, Long aclId)
|
|
{
|
|
Node oldNode = getNodeNotNull(nodeId);
|
|
NodeUpdateEntity nodeUpdateEntity = new NodeUpdateEntity();
|
|
nodeUpdateEntity.setId(nodeId);
|
|
nodeUpdateEntity.setAclId(aclId);
|
|
nodeUpdateEntity.setUpdateAclId(true);
|
|
updateNodeImpl(oldNode, nodeUpdateEntity, null);
|
|
}
|
|
|
|
public void setPrimaryChildrenSharedAclId(
|
|
Long primaryParentNodeId,
|
|
Long optionalOldSharedAlcIdInAdditionToNull,
|
|
Long newSharedAclId)
|
|
{
|
|
Long txnId = getCurrentTransaction().getId();
|
|
updatePrimaryChildrenSharedAclId(
|
|
txnId,
|
|
primaryParentNodeId,
|
|
optionalOldSharedAlcIdInAdditionToNull,
|
|
newSharedAclId);
|
|
invalidateNodeChildrenCaches(primaryParentNodeId, true, false);
|
|
}
|
|
|
|
public void deleteNode(Long nodeId)
|
|
{
|
|
Node node = getNodeNotNull(nodeId);
|
|
// Gather data for later
|
|
Long aclId = node.getAclId();
|
|
Set<QName> nodeAspects = getNodeAspects(nodeId);
|
|
|
|
// Finally mark the node as deleted
|
|
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
|
|
nodeUpdate.setId(nodeId);
|
|
// ACL
|
|
nodeUpdate.setAclId(null);
|
|
nodeUpdate.setUpdateAclId(true);
|
|
// Deleted
|
|
nodeUpdate.setDeleted(true);
|
|
nodeUpdate.setUpdateDeleted(true);
|
|
// Use a 'deleted' type QName
|
|
Long deletedQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_DELETED).getFirst();
|
|
nodeUpdate.setTypeQNameId(deletedQNameId);
|
|
nodeUpdate.setUpdateTypeQNameId(true);
|
|
|
|
boolean updated = updateNodeImpl(node, nodeUpdate, nodeAspects);
|
|
if (!updated)
|
|
{
|
|
invalidateNodeCaches(nodeId);
|
|
// Should not be attempting to delete a deleted node
|
|
throw new ConcurrencyFailureException(
|
|
"Failed to delete an existing live node: \n" +
|
|
" Before: " + node + "\n" +
|
|
" Update: " + nodeUpdate);
|
|
}
|
|
|
|
// Clean up content data
|
|
Set<QName> contentQNames = new HashSet<QName>(dictionaryService.getAllProperties(DataTypeDefinition.CONTENT));
|
|
Set<Long> contentQNamesToRemoveIds = qnameDAO.convertQNamesToIds(contentQNames, false);
|
|
contentDataDAO.deleteContentDataForNode(nodeId, contentQNamesToRemoveIds);
|
|
|
|
// Delete content usage deltas
|
|
usageDAO.deleteDeltas(nodeId);
|
|
|
|
// Handle sys:aspect_root
|
|
if (nodeAspects.contains(ContentModel.ASPECT_ROOT))
|
|
{
|
|
StoreRef storeRef = node.getStore().getStoreRef();
|
|
allRootNodesCache.remove(storeRef);
|
|
}
|
|
|
|
// Remove peer associations (no associated cache)
|
|
deleteNodeAssocsToAndFrom(nodeId);
|
|
|
|
// Remove child associations (invalidate children)
|
|
invalidateNodeChildrenCaches(nodeId, true, true);
|
|
invalidateNodeChildrenCaches(nodeId, false, true);
|
|
deleteChildAssocsToAndFrom(nodeId);
|
|
|
|
// Remove aspects
|
|
deleteNodeAspects(nodeId, null);
|
|
|
|
// Remove properties
|
|
deleteNodeProperties(nodeId, (Set<Long>) null);
|
|
|
|
// Remove subscriptions
|
|
deleteSubscriptions(nodeId);
|
|
|
|
// Remove ACLs
|
|
if (aclId != null)
|
|
{
|
|
aclDAO.deleteAclForNode(aclId, false);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int purgeNodes(long maxTxnCommitTimeMs)
|
|
{
|
|
return deleteNodesByCommitTime(true, maxTxnCommitTimeMs);
|
|
}
|
|
|
|
/*
|
|
* Node Properties
|
|
*/
|
|
|
|
public Map<QName, Serializable> getNodeProperties(Long nodeId)
|
|
{
|
|
Map<QName, Serializable> props = getNodePropertiesCached(nodeId);
|
|
// Create a shallow copy to allow additions
|
|
props = new HashMap<QName, Serializable>(props);
|
|
|
|
Node node = getNodeNotNull(nodeId);
|
|
// Handle sys:referenceable
|
|
ReferenceablePropertiesEntity.addReferenceableProperties(node, props);
|
|
// Handle sys:localized
|
|
LocalizedPropertiesEntity.addLocalizedProperties(localeDAO, node, props);
|
|
// Handle cm:auditable
|
|
if (hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
|
|
{
|
|
AuditablePropertiesEntity auditableProperties = node.getAuditableProperties();
|
|
if (auditableProperties == null)
|
|
{
|
|
auditableProperties = new AuditablePropertiesEntity();
|
|
}
|
|
props.putAll(auditableProperties.getAuditableProperties());
|
|
}
|
|
|
|
// 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);
|
|
AuditablePropertiesEntity auditableProperties = node.getAuditableProperties();
|
|
if (auditableProperties != null)
|
|
{
|
|
value = auditableProperties.getAuditableProperty(propertyQName);
|
|
}
|
|
}
|
|
else if (ReferenceablePropertiesEntity.isReferenceableProperty(propertyQName)) // sys:referenceable
|
|
{
|
|
Node node = getNodeNotNull(nodeId);
|
|
value = ReferenceablePropertiesEntity.getReferenceableProperty(node, propertyQName);
|
|
}
|
|
else if (LocalizedPropertiesEntity.isLocalizedProperty(propertyQName)) // sys:localized
|
|
{
|
|
Node node = getNodeNotNull(nodeId);
|
|
value = LocalizedPropertiesEntity.getLocalizedProperty(localeDAO, node, propertyQName);
|
|
}
|
|
else
|
|
{
|
|
Map<QName, Serializable> props = getNodePropertiesCached(nodeId);
|
|
// 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. It is only necessary to pass in old and new values for
|
|
* <i>changes</i> i.e. when setting a single property, it is only necessary to pass that
|
|
* property's value in the <b>old</b> and </b>new</b> maps; this improves execution speed
|
|
* significantly - although it has no effect on the number of resulting DB operations.
|
|
* <p/>
|
|
* Note: The cached properties are not updated
|
|
*
|
|
* @param nodeId the node ID
|
|
* @param newProps the properties to add or update
|
|
* @param isAddOnly <tt>true</tt> if the new properties are just an update or
|
|
* <tt>false</tt> if the properties are a complete set
|
|
* @return Returns <tt>true</tt> if any properties were changed
|
|
*/
|
|
private boolean setNodePropertiesImpl(
|
|
Long nodeId,
|
|
Map<QName, Serializable> newProps,
|
|
boolean isAddOnly)
|
|
{
|
|
if (isAddOnly && newProps.size() == 0)
|
|
{
|
|
return false; // No point adding nothing
|
|
}
|
|
|
|
// Get the current node
|
|
Node node = getNodeNotNull(nodeId);
|
|
// Create an update node
|
|
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
|
|
nodeUpdate.setId(nodeId);
|
|
|
|
// Copy inbound values
|
|
newProps = new HashMap<QName, Serializable>(newProps);
|
|
|
|
// Copy cm:auditable
|
|
if (!policyBehaviourFilter.isEnabled(node.getNodeRef(), ContentModel.ASPECT_AUDITABLE))
|
|
{
|
|
// Only bother if cm:auditable properties are present
|
|
if (AuditablePropertiesEntity.hasAuditableProperty(newProps.keySet()))
|
|
{
|
|
AuditablePropertiesEntity auditableProps = node.getAuditableProperties();
|
|
if (auditableProps == null)
|
|
{
|
|
auditableProps = new AuditablePropertiesEntity();
|
|
}
|
|
else
|
|
{
|
|
auditableProps = new AuditablePropertiesEntity(auditableProps); // Unlocked instance
|
|
}
|
|
boolean containedAuditProperties = auditableProps.setAuditValues(null, null, newProps);
|
|
if (!containedAuditProperties)
|
|
{
|
|
// Double-check (previous hasAuditableProperty should cover it)
|
|
// The behaviour is disabled, but no audit properties were passed in
|
|
auditableProps = null;
|
|
}
|
|
nodeUpdate.setAuditableProperties(auditableProps);
|
|
nodeUpdate.setUpdateAuditableProperties(true);
|
|
}
|
|
}
|
|
|
|
// Remove cm:auditable
|
|
newProps.keySet().removeAll(AuditablePropertiesEntity.getAuditablePropertyQNames());
|
|
|
|
// Check if the sys:localized property is being changed
|
|
Long oldNodeLocaleId = node.getLocaleId();
|
|
Locale newLocale = DefaultTypeConverter.INSTANCE.convert(
|
|
Locale.class,
|
|
newProps.get(ContentModel.PROP_LOCALE));
|
|
if (newLocale != null)
|
|
{
|
|
Long newNodeLocaleId = localeDAO.getOrCreateLocalePair(newLocale).getFirst();
|
|
if (!newNodeLocaleId.equals(oldNodeLocaleId))
|
|
{
|
|
nodeUpdate.setLocaleId(newNodeLocaleId);
|
|
nodeUpdate.setUpdateLocaleId(true);
|
|
}
|
|
}
|
|
// else: a 'null' new locale is completely ignored. This is the behaviour we choose.
|
|
|
|
// Remove sys:localized
|
|
LocalizedPropertiesEntity.removeLocalizedProperties(node, newProps);
|
|
|
|
// Remove sys:referenceable
|
|
ReferenceablePropertiesEntity.removeReferenceableProperties(node, newProps);
|
|
// Load the current properties.
|
|
// This means that we have to go to the DB during cold-write operations,
|
|
// but usually a write occurs after a node has been fetched of viewed in
|
|
// some way by the client code. Loading the existing properties has the
|
|
// advantage that the differencing code can eliminate unnecessary writes
|
|
// completely.
|
|
Map<QName, Serializable> oldPropsCached = getNodePropertiesCached(nodeId); // Keep pristine for caching
|
|
Map<QName, Serializable> oldProps = new HashMap<QName, Serializable>(oldPropsCached);
|
|
// If we're adding, remove current properties that are not of interest
|
|
if (isAddOnly)
|
|
{
|
|
oldProps.keySet().retainAll(newProps.keySet());
|
|
}
|
|
// We need to convert the new properties to our internally-used format,
|
|
// which is compatible with model i.e. people may have passed in data
|
|
// which needs to be converted to a model-compliant format. We do this
|
|
// before comparisons to avoid false negatives.
|
|
Map<NodePropertyKey, NodePropertyValue> newPropsRaw = nodePropertyHelper.convertToPersistentProperties(newProps);
|
|
newProps = nodePropertyHelper.convertToPublicProperties(newPropsRaw);
|
|
// Now find out what's changed
|
|
Map<QName, MapValueComparison> diff = EqualsHelper.getMapComparison(
|
|
oldProps,
|
|
newProps);
|
|
// Keep track of properties to delete and add
|
|
Set<QName> propsToDelete = new HashSet<QName>(oldProps.size()*2);
|
|
Map<QName, Serializable> propsToAdd = new HashMap<QName, Serializable>(newProps.size() * 2);
|
|
Set<QName> contentQNamesToDelete = new HashSet<QName>(5);
|
|
for (Map.Entry<QName, MapValueComparison> entry : diff.entrySet())
|
|
{
|
|
QName qname = entry.getKey();
|
|
|
|
PropertyDefinition removePropDef = dictionaryService.getProperty(qname);
|
|
boolean isContent = (removePropDef != null &&
|
|
removePropDef.getDataType().getName().equals(DataTypeDefinition.CONTENT));
|
|
|
|
switch (entry.getValue())
|
|
{
|
|
case EQUAL:
|
|
// Ignore
|
|
break;
|
|
case LEFT_ONLY:
|
|
// Not in the new properties
|
|
propsToDelete.add(qname);
|
|
if (isContent)
|
|
{
|
|
contentQNamesToDelete.add(qname);
|
|
}
|
|
break;
|
|
case NOT_EQUAL:
|
|
// Must remove from the LHS
|
|
propsToDelete.add(qname);
|
|
if (isContent)
|
|
{
|
|
contentQNamesToDelete.add(qname);
|
|
}
|
|
// Fall through to load up the RHS
|
|
case RIGHT_ONLY:
|
|
// We're adding this
|
|
Serializable value = newProps.get(qname);
|
|
if (isContent && value != null)
|
|
{
|
|
ContentData newContentData = (ContentData) value;
|
|
Long newContentDataId = contentDataDAO.createContentData(newContentData).getFirst();
|
|
value = new ContentDataWithId(newContentData, newContentDataId);
|
|
}
|
|
propsToAdd.put(qname, value);
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Unknown MapValueComparison: " + entry.getValue());
|
|
}
|
|
}
|
|
|
|
boolean modifyProps = propsToDelete.size() > 0 || propsToAdd.size() > 0;
|
|
boolean updated = modifyProps || nodeUpdate.isUpdateAnything();
|
|
|
|
// Bring the node into the current transaction
|
|
if (nodeUpdate.isUpdateAnything())
|
|
{
|
|
// We have to explicitly update the node (sys:locale or cm:auditable)
|
|
if (updateNodeImpl(node, nodeUpdate, null))
|
|
{
|
|
// Copy the caches across
|
|
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
|
|
NodeVersionKey newNodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
|
|
copyNodeAspectsCached(nodeVersionKey, newNodeVersionKey);
|
|
copyNodePropertiesCached(nodeVersionKey, newNodeVersionKey);
|
|
copyParentAssocsCached(nodeVersionKey, newNodeVersionKey);
|
|
}
|
|
}
|
|
else if (modifyProps)
|
|
{
|
|
// Touch the node; all caches are fine
|
|
touchNode(nodeId, null, null, false, false, false);
|
|
}
|
|
|
|
// Touch to bring into current txn
|
|
if (modifyProps)
|
|
{
|
|
// Clean up content properties
|
|
try
|
|
{
|
|
if (contentQNamesToDelete.size() > 0)
|
|
{
|
|
Set<Long> contentQNameIdsToDelete = qnameDAO.convertQNamesToIds(contentQNamesToDelete, false);
|
|
contentDataDAO.deleteContentDataForNode(nodeId, contentQNameIdsToDelete);
|
|
}
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
throw new AlfrescoRuntimeException(
|
|
"Failed to delete content properties: \n" +
|
|
" Node: " + nodeId + "\n" +
|
|
" Delete Tried: " + contentQNamesToDelete,
|
|
e);
|
|
}
|
|
|
|
try
|
|
{
|
|
// Apply deletes
|
|
Set<Long> propQNameIdsToDelete = qnameDAO.convertQNamesToIds(propsToDelete, true);
|
|
deleteNodeProperties(nodeId, propQNameIdsToDelete);
|
|
// Now create the raw properties for adding
|
|
newPropsRaw = nodePropertyHelper.convertToPersistentProperties(propsToAdd);
|
|
insertNodeProperties(nodeId, newPropsRaw);
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
// Don't trust the caches for the node
|
|
invalidateNodeCaches(nodeId);
|
|
// Focused error
|
|
throw new AlfrescoRuntimeException(
|
|
"Failed to write property deltas: \n" +
|
|
" Node: " + nodeId + "\n" +
|
|
" Old: " + oldProps + "\n" +
|
|
" New: " + newProps + "\n" +
|
|
" Diff: " + diff + "\n" +
|
|
" Delete Tried: " + propsToDelete + "\n" +
|
|
" Add Tried: " + propsToAdd,
|
|
e);
|
|
}
|
|
|
|
// Build the properties to cache based on whether this is an append or replace
|
|
Map<QName, Serializable> propsToCache = null;
|
|
if (isAddOnly)
|
|
{
|
|
// 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);
|
|
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).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).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).getStore().getStoreRef();
|
|
allRootNodesCache.remove(storeRef);
|
|
// Touch the node; parent assocs need invalidation
|
|
touchNode(nodeId, null, newAspectQNames, false, false, true);
|
|
}
|
|
else
|
|
{
|
|
// Touch the node; all caches are fine
|
|
touchNode(nodeId, null, newAspectQNames, false, false, false);
|
|
}
|
|
|
|
// Manually update the cache
|
|
setNodeAspectsCached(nodeId, newAspectQNames);
|
|
|
|
// Done
|
|
return true;
|
|
}
|
|
|
|
public boolean removeNodeAspects(Long nodeId)
|
|
{
|
|
Set<QName> newAspectQNames = Collections.<QName>emptySet();
|
|
|
|
// Touch the node; all caches are fine
|
|
touchNode(nodeId, null, newAspectQNames, false, false, false);
|
|
|
|
// Just delete all the node's aspects
|
|
int deleteCount = deleteNodeAspects(nodeId, null);
|
|
|
|
// Manually update the cache
|
|
setNodeAspectsCached(nodeId, newAspectQNames);
|
|
|
|
// Done
|
|
return deleteCount > 0;
|
|
}
|
|
|
|
public boolean removeNodeAspects(Long nodeId, Set<QName> aspectQNames)
|
|
{
|
|
if (aspectQNames.size() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
// Get the current aspects
|
|
Set<QName> existingAspectQNames = getNodeAspects(nodeId);
|
|
|
|
// Collate the new set of aspects so that touch works correctly against cm:auditable
|
|
Set<QName> newAspectQNames = new HashSet<QName>(existingAspectQNames);
|
|
newAspectQNames.removeAll(aspectQNames);
|
|
|
|
// Touch the node; all caches are fine
|
|
touchNode(nodeId, null, newAspectQNames, false, false, false);
|
|
|
|
// Now remove each aspect
|
|
Set<Long> aspectQNameIdsToRemove = qnameDAO.convertQNamesToIds(aspectQNames, false);
|
|
int deleteCount = deleteNodeAspects(nodeId, aspectQNameIdsToRemove);
|
|
if (deleteCount == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Handle sys:aspect_root
|
|
if (aspectQNames.contains(ContentModel.ASPECT_ROOT))
|
|
{
|
|
// invalidate root nodes cache for the store
|
|
StoreRef storeRef = getNodeNotNull(nodeId).getStore().getStoreRef();
|
|
allRootNodesCache.remove(storeRef);
|
|
// Touch the node; parent assocs need invalidation
|
|
touchNode(nodeId, null, newAspectQNames, false, false, true);
|
|
}
|
|
else
|
|
{
|
|
// Touch the node; all caches are fine
|
|
touchNode(nodeId, null, newAspectQNames, false, false, false);
|
|
}
|
|
|
|
// Manually update the cache
|
|
setNodeAspectsCached(nodeId, newAspectQNames);
|
|
|
|
// Done
|
|
return deleteCount > 0;
|
|
}
|
|
|
|
public void getNodesWithAspects(
|
|
Set<QName> aspectQNames,
|
|
Long minNodeId, Long maxNodeId,
|
|
NodeRefQueryCallback resultsCallback)
|
|
{
|
|
Set<Long> qnameIdsSet = qnameDAO.convertQNamesToIds(aspectQNames, false);
|
|
if (qnameIdsSet.size() == 0)
|
|
{
|
|
// No point running a query
|
|
return;
|
|
}
|
|
List<Long> qnameIds = new ArrayList<Long>(qnameIdsSet);
|
|
selectNodesWithAspects(qnameIds, minNodeId, maxNodeId, resultsCallback);
|
|
}
|
|
|
|
/**
|
|
* @return Returns a writable copy of the cached aspects set
|
|
*/
|
|
private Set<QName> getNodeAspectsCached(Long nodeId)
|
|
{
|
|
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
|
|
Pair<NodeVersionKey, Set<QName>> cacheEntry = aspectsCache.getByKey(nodeVersionKey);
|
|
if (cacheEntry == null)
|
|
{
|
|
invalidateNodeCaches(nodeId);
|
|
throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
|
|
}
|
|
return new HashSet<QName>(cacheEntry.getSecond());
|
|
}
|
|
|
|
/**
|
|
* Update the node aspects cache. The incoming set will be wrapped to be unmodifiable.
|
|
*/
|
|
private void setNodeAspectsCached(Long nodeId, Set<QName> aspects)
|
|
{
|
|
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
|
|
aspectsCache.setValue(nodeVersionKey, Collections.unmodifiableSet(aspects));
|
|
}
|
|
|
|
/**
|
|
* Helper method to copy cache values from one key to another
|
|
*/
|
|
private void copyNodeAspectsCached(NodeVersionKey from, NodeVersionKey to)
|
|
{
|
|
Set<QName> cacheEntry = aspectsCache.getValue(from);
|
|
if (cacheEntry != null)
|
|
{
|
|
aspectsCache.setValue(to, cacheEntry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback to cache node aspects. The DAO callback only does the simple {@link #findByKey(Long)}.
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.4
|
|
*/
|
|
private class AspectsCallbackDAO extends EntityLookupCallbackDAOAdaptor<NodeVersionKey, Set<QName>, Serializable>
|
|
{
|
|
public Pair<NodeVersionKey, Set<QName>> createValue(Set<QName> value)
|
|
{
|
|
throw new UnsupportedOperationException("A node always has a 'set' of aspects.");
|
|
}
|
|
|
|
public Pair<NodeVersionKey, Set<QName>> findByKey(NodeVersionKey nodeVersionKey)
|
|
{
|
|
Long nodeId = nodeVersionKey.getNodeId();
|
|
Set<Long> nodeIds = Collections.singleton(nodeId);
|
|
Map<NodeVersionKey, Set<QName>> nodeAspectQNameIdsByVersionKey = selectNodeAspects(nodeIds);
|
|
Set<QName> nodeAspectQNames = nodeAspectQNameIdsByVersionKey.get(nodeVersionKey);
|
|
if (nodeAspectQNames == null)
|
|
{
|
|
// Didn't find a match. Is this because there are none?
|
|
if (nodeAspectQNameIdsByVersionKey.size() == 0)
|
|
{
|
|
// This is OK. The node has no properties
|
|
nodeAspectQNames = Collections.emptySet();
|
|
}
|
|
else
|
|
{
|
|
// We found properties associated with a different node ID and version
|
|
invalidateNodeCaches(nodeId);
|
|
throw new DataIntegrityViolationException(
|
|
"Detected stale node entry: " + nodeVersionKey +
|
|
" (now " + nodeAspectQNameIdsByVersionKey.keySet() + ")");
|
|
}
|
|
}
|
|
// Done
|
|
return new Pair<NodeVersionKey, Set<QName>>(nodeVersionKey, Collections.unmodifiableSet(nodeAspectQNames));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Node assocs
|
|
*/
|
|
|
|
@Override
|
|
public Long newNodeAssoc(Long sourceNodeId, Long targetNodeId, QName assocTypeQName, int assocIndex)
|
|
{
|
|
if (assocIndex == 0)
|
|
{
|
|
throw new IllegalArgumentException("Index is 1-based, or -1 to indicate 'next value'.");
|
|
}
|
|
|
|
// Touch the node; all caches are fine
|
|
touchNode(sourceNodeId, null, null, false, false, false);
|
|
|
|
// Resolve type QName
|
|
Long assocTypeQNameId = qnameDAO.getOrCreateQName(assocTypeQName).getFirst();
|
|
|
|
// Get the current max; we will need this no matter what
|
|
if (assocIndex <= 0)
|
|
{
|
|
int maxIndex = selectNodeAssocMaxIndex(sourceNodeId, assocTypeQNameId);
|
|
assocIndex = maxIndex + 1;
|
|
}
|
|
|
|
Long result = null;
|
|
Savepoint savepoint = controlDAO.createSavepoint("NodeService.newNodeAssoc");
|
|
try
|
|
{
|
|
result = insertNodeAssoc(sourceNodeId, targetNodeId, assocTypeQNameId, assocIndex);
|
|
controlDAO.releaseSavepoint(savepoint);
|
|
return result;
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
controlDAO.rollbackToSavepoint(savepoint);
|
|
throw new AssociationExistsException(sourceNodeId, targetNodeId, assocTypeQName);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setNodeAssocIndex(Long id, int assocIndex)
|
|
{
|
|
int updated = updateNodeAssoc(id, assocIndex);
|
|
if (updated != 1)
|
|
{
|
|
throw new ConcurrencyFailureException("Expected to update exactly one row: " + id);
|
|
}
|
|
}
|
|
|
|
public int removeNodeAssoc(Long sourceNodeId, Long targetNodeId, QName assocTypeQName)
|
|
{
|
|
Pair<Long, QName> assocTypeQNamePair = qnameDAO.getQName(assocTypeQName);
|
|
if (assocTypeQNamePair == null)
|
|
{
|
|
// Never existed
|
|
return 0;
|
|
}
|
|
|
|
Long assocTypeQNameId = assocTypeQNamePair.getFirst();
|
|
int deleted = deleteNodeAssoc(sourceNodeId, targetNodeId, assocTypeQNameId);
|
|
if (deleted > 0)
|
|
{
|
|
// Touch the node; all caches are fine
|
|
touchNode(sourceNodeId, null, null, false, false, false);
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
public int removeNodeAssocsToAndFrom(Long nodeId)
|
|
{
|
|
int deleted = deleteNodeAssocsToAndFrom(nodeId);
|
|
if (deleted > 0)
|
|
{
|
|
// Touch the node; all caches are fine
|
|
touchNode(nodeId, null, null, false, false, false);
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
public int removeNodeAssocsToAndFrom(Long nodeId, Set<QName> assocTypeQNames)
|
|
{
|
|
Set<Long> assocTypeQNameIds = qnameDAO.convertQNamesToIds(assocTypeQNames, false);
|
|
if (assocTypeQNameIds.size() == 0)
|
|
{
|
|
// Never existed
|
|
return 0;
|
|
}
|
|
|
|
int deleted = deleteNodeAssocsToAndFrom(nodeId, assocTypeQNameIds);
|
|
if (deleted > 0)
|
|
{
|
|
// Touch the node; all caches are fine
|
|
touchNode(nodeId, null, null, false, false, false);
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
@Override
|
|
public int removeNodeAssocs(List<Long> ids)
|
|
{
|
|
int toDelete = ids.size();
|
|
if (toDelete == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
int deleted = deleteNodeAssocs(ids);
|
|
if (toDelete != deleted)
|
|
{
|
|
throw new ConcurrencyFailureException("Deleted " + deleted + " but expected " + toDelete);
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
@Override
|
|
public Collection<Pair<Long, AssociationRef>> getSourceNodeAssocs(Long targetNodeId, QName typeQName)
|
|
{
|
|
Long typeQNameId = null;
|
|
if (typeQName != null)
|
|
{
|
|
Pair<Long, QName> typeQNamePair = qnameDAO.getQName(typeQName);
|
|
if (typeQNamePair == null)
|
|
{
|
|
// No such QName
|
|
return Collections.emptyList();
|
|
}
|
|
typeQNameId = typeQNamePair.getFirst();
|
|
}
|
|
List<NodeAssocEntity> nodeAssocEntities = selectNodeAssocsByTarget(targetNodeId, typeQNameId);
|
|
List<Pair<Long, AssociationRef>> results = new ArrayList<Pair<Long,AssociationRef>>(nodeAssocEntities.size());
|
|
for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
|
|
{
|
|
Long assocId = nodeAssocEntity.getId();
|
|
AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
|
|
results.add(new Pair<Long, AssociationRef>(assocId, assocRef));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@Override
|
|
public Collection<Pair<Long, AssociationRef>> getTargetNodeAssocs(Long sourceNodeId, QName typeQName)
|
|
{
|
|
Long typeQNameId = null;
|
|
if (typeQName != null)
|
|
{
|
|
Pair<Long, QName> typeQNamePair = qnameDAO.getQName(typeQName);
|
|
if (typeQNamePair == null)
|
|
{
|
|
// No such QName
|
|
return Collections.emptyList();
|
|
}
|
|
typeQNameId = typeQNamePair.getFirst();
|
|
}
|
|
List<NodeAssocEntity> nodeAssocEntities = selectNodeAssocsBySource(sourceNodeId, typeQNameId);
|
|
List<Pair<Long, AssociationRef>> results = new ArrayList<Pair<Long,AssociationRef>>(nodeAssocEntities.size());
|
|
for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
|
|
{
|
|
Long assocId = nodeAssocEntity.getId();
|
|
AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
|
|
results.add(new Pair<Long, AssociationRef>(assocId, assocRef));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@Override
|
|
public Pair<Long, AssociationRef> getNodeAssocOrNull(Long assocId)
|
|
{
|
|
NodeAssocEntity nodeAssocEntity = selectNodeAssocById(assocId);
|
|
if (nodeAssocEntity == null)
|
|
{
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
|
|
return new Pair<Long, AssociationRef>(assocId, assocRef);
|
|
}
|
|
}
|
|
|
|
public Pair<Long, AssociationRef> getNodeAssoc(Long assocId)
|
|
{
|
|
Pair<Long, AssociationRef> ret = getNodeAssocOrNull(assocId);
|
|
if (ret == null)
|
|
{
|
|
throw new ConcurrencyFailureException("Assoc ID does not point to a valid association: " + assocId);
|
|
}
|
|
else
|
|
{
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Child assocs
|
|
*/
|
|
|
|
private ChildAssocEntity newChildAssocImpl(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
boolean isPrimary,
|
|
final QName assocTypeQName,
|
|
QName assocQName,
|
|
final String childNodeName,
|
|
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);
|
|
final Node childNode = getNodeNotNullImpl(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);
|
|
|
|
// SQL Server retry
|
|
if (e.getMessage().contains("Snapshot isolation transaction aborted"))
|
|
{
|
|
logger.warn("insertChildAssoc: SQL Server snapshot isolation retry: "+assoc);
|
|
throw new ConcurrencyFailureException("SQL Server snapshot isolation retry...", e);
|
|
}
|
|
|
|
// FK conflict retry, eg.
|
|
// SQL Server - The INSERT statement conflicted with the FOREIGN KEY constraint
|
|
// MySQL - Cannot add or update a child row: a foreign key constraint fails
|
|
if (e.getMessage().toUpperCase().contains("FOREIGN KEY"))
|
|
{
|
|
logger.warn("insertChildAssoc: FK conflict retry: "+assoc);
|
|
throw new ConcurrencyFailureException("FK conflict retry...", e);
|
|
}
|
|
|
|
// We assume that this is from the child cm:name constraint violation
|
|
throw new DuplicateChildNodeNameException(
|
|
parentNode.getNodeRef(),
|
|
assocTypeQName,
|
|
childNodeName,
|
|
e);
|
|
}
|
|
}
|
|
};
|
|
Long assocId = childAssocRetryingHelper.doWithRetry(callback);
|
|
// Persist it
|
|
assoc.setId(assocId);
|
|
|
|
// Primary associations accompany new nodes, so we only have to bring the
|
|
// node into the current transaction for secondary associations
|
|
if (!isPrimary)
|
|
{
|
|
updateNode(childNodeId, null, null);
|
|
}
|
|
|
|
// Done
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug("Created child association: " + assoc);
|
|
}
|
|
return assoc;
|
|
}
|
|
|
|
public Pair<Long, ChildAssociationRef> newChildAssoc(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
String childNodeName)
|
|
{
|
|
ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId);
|
|
// Create it
|
|
ChildAssocEntity assoc = newChildAssocImpl(
|
|
parentNodeId, childNodeId, false, assocTypeQName, assocQName, childNodeName, false);
|
|
Long assocId = assoc.getId();
|
|
// Touch the node; all caches are fine
|
|
touchNode(childNodeId, null, null, false, false, false);
|
|
// update cache
|
|
parentAssocInfo = parentAssocInfo.addAssoc(assocId, assoc);
|
|
setParentAssocsCached(childNodeId, parentAssocInfo);
|
|
// Done
|
|
return assoc.getPair(qnameDAO);
|
|
}
|
|
|
|
public void deleteChildAssoc(Long assocId)
|
|
{
|
|
ChildAssocEntity assoc = selectChildAssoc(assocId);
|
|
if (assoc == null)
|
|
{
|
|
throw new ConcurrencyFailureException("Child association not found: " + assocId);
|
|
}
|
|
// Update cache
|
|
Long childNodeId = assoc.getChildNode().getId();
|
|
ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId);
|
|
// Delete it
|
|
int count = deleteChildAssocById(assocId);
|
|
if (count != 1)
|
|
{
|
|
throw new ConcurrencyFailureException("Child association not deleted: " + assocId);
|
|
}
|
|
// Touch the node; all caches are fine
|
|
touchNode(childNodeId, null, null, false, false, false);
|
|
// Update cache
|
|
parentAssocInfo = parentAssocInfo.removeAssoc(assocId);
|
|
setParentAssocsCached(childNodeId, parentAssocInfo);
|
|
}
|
|
|
|
public int setChildAssocIndex(Long parentNodeId, Long childNodeId, QName assocTypeQName, QName assocQName, int index)
|
|
{
|
|
int count = updateChildAssocIndex(parentNodeId, childNodeId, assocTypeQName, assocQName, index);
|
|
if (count > 0)
|
|
{
|
|
// Touch the node; parent assocs are out of sync
|
|
touchNode(childNodeId, null, null, false, false, true);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* TODO: See about pulling automatic cm:name update logic into this DAO
|
|
*/
|
|
public void setChildAssocsUniqueName(final Long childNodeId, final String childName)
|
|
{
|
|
RetryingCallback<Integer> callback = new RetryingCallback<Integer>()
|
|
{
|
|
public Integer execute() throws Throwable
|
|
{
|
|
Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
|
|
try
|
|
{
|
|
Integer count = updateChildAssocsUniqueName(childNodeId, childName);
|
|
controlDAO.releaseSavepoint(savepoint);
|
|
return count;
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
controlDAO.rollbackToSavepoint(savepoint);
|
|
// We assume that this is from the child cm:name constraint violation
|
|
throw new DuplicateChildNodeNameException(null, null, childName, e);
|
|
}
|
|
}
|
|
};
|
|
Integer count = childAssocRetryingHelper.doWithRetry(callback);
|
|
if (count > 0)
|
|
{
|
|
// Touch the node; parent assocs are out of sync
|
|
touchNode(childNodeId, null, null, false, false, true);
|
|
}
|
|
|
|
if (isDebugEnabled)
|
|
{
|
|
logger.debug(
|
|
"Updated cm:name to parent assocs: \n" +
|
|
" Node: " + childNodeId + "\n" +
|
|
" Name: " + childName + "\n" +
|
|
" Updated: " + count);
|
|
}
|
|
}
|
|
|
|
public Pair<Long, ChildAssociationRef> getChildAssoc(Long assocId)
|
|
{
|
|
ChildAssocEntity assoc = selectChildAssoc(assocId);
|
|
if (assoc == null)
|
|
{
|
|
throw new ConcurrencyFailureException("Child association not found: " + assocId);
|
|
}
|
|
return assoc.getPair(qnameDAO);
|
|
}
|
|
|
|
public List<NodeIdAndAclId> getPrimaryChildrenAcls(Long nodeId)
|
|
{
|
|
return selectPrimaryChildAcls(nodeId);
|
|
}
|
|
|
|
public Pair<Long, ChildAssociationRef> getChildAssoc(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName)
|
|
{
|
|
List<ChildAssocEntity> assocs = selectChildAssoc(parentNodeId, childNodeId, assocTypeQName, assocQName);
|
|
if (assocs.size() == 0)
|
|
{
|
|
return null;
|
|
}
|
|
else if (assocs.size() == 1)
|
|
{
|
|
return assocs.get(0).getPair(qnameDAO);
|
|
}
|
|
// Keep the primary association or, if there isn't one, the association with the smallest ID
|
|
Map<Long, ChildAssocEntity> assocsToDeleteById = new HashMap<Long, ChildAssocEntity>(assocs.size() * 2);
|
|
Long minId = null;
|
|
Long primaryId = null;
|
|
for (ChildAssocEntity assoc : assocs)
|
|
{
|
|
// First store it
|
|
Long assocId = assoc.getId();
|
|
assocsToDeleteById.put(assocId, assoc);
|
|
if (minId == null || minId.compareTo(assocId) > 0)
|
|
{
|
|
minId = assocId;
|
|
}
|
|
if (assoc.isPrimary())
|
|
{
|
|
primaryId = assocId;
|
|
}
|
|
}
|
|
// Remove either the primary or min assoc
|
|
Long assocToKeepId = primaryId == null ? minId : primaryId;
|
|
ChildAssocEntity assocToKeep = assocsToDeleteById.remove(assocToKeepId);
|
|
// If the current transaction allows, remove the other associations
|
|
if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_WRITE)
|
|
{
|
|
for (Long assocIdToDelete : assocsToDeleteById.keySet())
|
|
{
|
|
deleteChildAssoc(assocIdToDelete);
|
|
}
|
|
}
|
|
// Done
|
|
return assocToKeep.getPair(qnameDAO);
|
|
}
|
|
|
|
/**
|
|
* Callback that applies node preloading if required.
|
|
* <p/>
|
|
* Instances must be used and discarded per query.
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.4
|
|
*/
|
|
private class ChildAssocRefBatchingQueryCallback implements ChildAssocRefQueryCallback
|
|
{
|
|
private final ChildAssocRefQueryCallback callback;
|
|
private final boolean preload;
|
|
private final List<NodeRef> nodeRefs;
|
|
/**
|
|
* @param callback the callback to batch around
|
|
*/
|
|
private ChildAssocRefBatchingQueryCallback(ChildAssocRefQueryCallback callback)
|
|
{
|
|
this.callback = callback;
|
|
this.preload = callback.preLoadNodes();
|
|
if (preload)
|
|
{
|
|
nodeRefs = new LinkedList<NodeRef>(); // No memory required
|
|
}
|
|
else
|
|
{
|
|
nodeRefs = null; // No list needed
|
|
}
|
|
}
|
|
/**
|
|
* @throws UnsupportedOperationException always
|
|
*/
|
|
public boolean preLoadNodes()
|
|
{
|
|
throw new UnsupportedOperationException("Expected to be used internally only.");
|
|
}
|
|
/**
|
|
* Defers to delegate
|
|
*/
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return callback.orderResults();
|
|
}
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
if (preload)
|
|
{
|
|
nodeRefs.add(childNodePair.getSecond());
|
|
}
|
|
return callback.handle(childAssocPair, parentNodePair, childNodePair);
|
|
}
|
|
public void done()
|
|
{
|
|
// Finish the batch
|
|
if (preload && nodeRefs.size() > 0)
|
|
{
|
|
cacheNodes(nodeRefs);
|
|
nodeRefs.clear();
|
|
}
|
|
// Done
|
|
callback.done();
|
|
}
|
|
}
|
|
|
|
public void getChildAssocs(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
Boolean isPrimary,
|
|
Boolean sameStore,
|
|
ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
selectChildAssocs(
|
|
parentNodeId, childNodeId,
|
|
assocTypeQName, assocQName, isPrimary, sameStore,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
}
|
|
|
|
@Override
|
|
public void getChildAssocs(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
int maxResults,
|
|
ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
selectChildAssocs(
|
|
parentNodeId,
|
|
assocTypeQName,
|
|
assocQName,
|
|
maxResults,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
}
|
|
|
|
public void getChildAssocs(Long parentNodeId, Set<QName> assocTypeQNames, ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
switch (assocTypeQNames.size())
|
|
{
|
|
case 0:
|
|
return; // No results possible
|
|
case 1:
|
|
QName assocTypeQName = assocTypeQNames.iterator().next();
|
|
selectChildAssocs(
|
|
parentNodeId, null, assocTypeQName, (QName) null, null, null,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
break;
|
|
default:
|
|
selectChildAssocs(
|
|
parentNodeId, assocTypeQNames,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks a cache and then queries.
|
|
* <p/>
|
|
* Note: If we were to cach misses, then we would have to ensure that the cache is
|
|
* kept up to date whenever any affection association is changed. This is actually
|
|
* not possible without forcing the cache to be fully clustered. So to
|
|
* avoid clustering the cache, we instead watch the node child version,
|
|
* which relies on a cache that is already clustered.
|
|
*/
|
|
public Pair<Long, ChildAssociationRef> getChildAssoc(Long parentNodeId, QName assocTypeQName, String childName)
|
|
{
|
|
ChildByNameKey key = new ChildByNameKey(parentNodeId, assocTypeQName, childName);
|
|
ChildAssocEntity assoc = childByNameCache.get(key);
|
|
boolean query = false;
|
|
if (assoc == null)
|
|
{
|
|
query = true;
|
|
}
|
|
else
|
|
{
|
|
// Check that the resultant child node has not moved on
|
|
Node childNode = assoc.getChildNode();
|
|
Long childNodeId = childNode.getId();
|
|
NodeVersionKey childNodeVersionKey = childNode.getNodeVersionKey();
|
|
Pair<Long, Node> childNodeFromCache = nodesCache.getByKey(childNodeId);
|
|
if (childNodeFromCache == null)
|
|
{
|
|
// Child node no longer exists (or never did)
|
|
query = true;
|
|
}
|
|
else
|
|
{
|
|
NodeVersionKey childNodeFromCacheVersionKey = childNodeFromCache.getSecond().getNodeVersionKey();
|
|
if (!childNodeFromCacheVersionKey.equals(childNodeVersionKey))
|
|
{
|
|
// The child node has moved on. We don't know why, but must query again.
|
|
query = true;
|
|
}
|
|
}
|
|
}
|
|
if (query)
|
|
{
|
|
assoc = selectChildAssoc(parentNodeId, assocTypeQName, childName);
|
|
if (assoc != null)
|
|
{
|
|
childByNameCache.put(key, assoc);
|
|
}
|
|
else
|
|
{
|
|
// We do not cache misses. See javadoc.
|
|
}
|
|
}
|
|
// Now return, checking the assoc's ID for null
|
|
return assoc == null ? null : assoc.getPair(qnameDAO);
|
|
}
|
|
|
|
public void getChildAssocs(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
Collection<String> childNames,
|
|
ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
selectChildAssocs(
|
|
parentNodeId, assocTypeQName, childNames,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
}
|
|
|
|
public void getChildAssocsByPropertyValue(
|
|
Long parentNodeId,
|
|
QName propertyQName,
|
|
Serializable value,
|
|
ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName);
|
|
NodePropertyValue nodeValue = nodePropertyHelper.makeNodePropertyValue(propertyDef, value);
|
|
|
|
if(nodeValue != null)
|
|
{
|
|
switch (nodeValue.getPersistedType())
|
|
{
|
|
case 1: // Boolean
|
|
case 3: // long
|
|
case 5: // double
|
|
case 6: // string
|
|
// no floats due to the range errors testing equality on a float.
|
|
break;
|
|
|
|
default:
|
|
throw new IllegalArgumentException("method not supported for persisted value type " + nodeValue.getPersistedType());
|
|
}
|
|
|
|
selectChildAssocsByPropertyValue(parentNodeId,
|
|
propertyQName,
|
|
nodeValue,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
}
|
|
}
|
|
|
|
public void getChildAssocsByChildTypes(
|
|
Long parentNodeId,
|
|
Set<QName> childNodeTypeQNames,
|
|
ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
selectChildAssocsByChildTypes(
|
|
parentNodeId, childNodeTypeQNames,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
}
|
|
|
|
public void getChildAssocsWithoutParentAssocsOfType(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
selectChildAssocsWithoutParentAssocsOfType(
|
|
parentNodeId, assocTypeQName,
|
|
new ChildAssocRefBatchingQueryCallback(resultsCallback));
|
|
}
|
|
|
|
public Pair<Long, ChildAssociationRef> getPrimaryParentAssoc(Long childNodeId)
|
|
{
|
|
ChildAssocEntity childAssocEntity = getPrimaryParentAssocImpl(childNodeId);
|
|
if(childAssocEntity == null)
|
|
{
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
return childAssocEntity.getPair(qnameDAO);
|
|
}
|
|
}
|
|
|
|
private ChildAssocEntity getPrimaryParentAssocImpl(Long childNodeId)
|
|
{
|
|
ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
|
|
return parentAssocs.getPrimaryParentAssoc();
|
|
}
|
|
|
|
private static final int PARENT_ASSOCS_CACHE_FILTER_THRESHOLD = 2000;
|
|
|
|
public void getParentAssocs(
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
Boolean isPrimary,
|
|
ChildAssocRefQueryCallback resultsCallback)
|
|
{
|
|
if (assocTypeQName == null && assocQName == null && isPrimary == null)
|
|
{
|
|
// Go for the cache (and return all)
|
|
ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
|
|
for (ChildAssocEntity assoc : parentAssocs.getParentAssocs().values())
|
|
{
|
|
resultsCallback.handle(
|
|
assoc.getPair(qnameDAO),
|
|
assoc.getParentNode().getNodePair(),
|
|
assoc.getChildNode().getNodePair());
|
|
}
|
|
resultsCallback.done();
|
|
}
|
|
else
|
|
{
|
|
// Decide whether we query or filter
|
|
ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
|
|
if (parentAssocs.getParentAssocs().size() > PARENT_ASSOCS_CACHE_FILTER_THRESHOLD)
|
|
{
|
|
// Query
|
|
selectParentAssocs(childNodeId, assocTypeQName, assocQName, isPrimary, resultsCallback);
|
|
}
|
|
else
|
|
{
|
|
// Go for the cache (and filter)
|
|
for (ChildAssocEntity assoc : parentAssocs.getParentAssocs().values())
|
|
{
|
|
Pair<Long, ChildAssociationRef> assocPair = assoc.getPair(qnameDAO);
|
|
if (((assocTypeQName == null) || (assocPair.getSecond().getTypeQName().equals(assocTypeQName))) &&
|
|
((assocQName == null) || (assocPair.getSecond().getQName().equals(assocQName))))
|
|
{
|
|
resultsCallback.handle(
|
|
assocPair,
|
|
assoc.getParentNode().getNodePair(),
|
|
assoc.getChildNode().getNodePair());
|
|
}
|
|
}
|
|
resultsCallback.done();
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Potentially cheaper than evaluating all of a node's paths to check for child association cycles
|
|
* <p/>
|
|
* TODO: When is it cheaper to go up and when is it cheaper to go down?
|
|
* Look at using direct queries to pass through layers both up and down.
|
|
*
|
|
* @param nodeId the node to start with
|
|
*/
|
|
public void cycleCheck(Long nodeId)
|
|
{
|
|
CycleCallBack callback = new CycleCallBack();
|
|
callback.cycleCheck(nodeId);
|
|
if (callback.toThrow != null)
|
|
{
|
|
throw callback.toThrow;
|
|
}
|
|
}
|
|
|
|
private class CycleCallBack implements ChildAssocRefQueryCallback
|
|
{
|
|
final Set<Long> nodeIds = new HashSet<Long>(97);
|
|
CyclicChildRelationshipException toThrow;
|
|
|
|
@Override
|
|
public void done()
|
|
{
|
|
}
|
|
|
|
@Override
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
Long nodeId = childNodePair.getFirst();
|
|
if (!nodeIds.add(nodeId))
|
|
{
|
|
ChildAssociationRef childAssociationRef = childAssocPair.getSecond();
|
|
// Remember exception we want to throw and exit. If we throw within here, it will be wrapped by IBatis
|
|
toThrow = new CyclicChildRelationshipException(
|
|
"Child Association Cycle detected hitting nodes: " + nodeIds,
|
|
childAssociationRef);
|
|
return false;
|
|
}
|
|
cycleCheck(nodeId);
|
|
nodeIds.remove(nodeId);
|
|
return toThrow == null;
|
|
}
|
|
|
|
/**
|
|
* No preloading required
|
|
*/
|
|
@Override
|
|
public boolean preLoadNodes()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* No ordering required
|
|
*/
|
|
@Override
|
|
public boolean orderResults()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public void cycleCheck(Long nodeId)
|
|
{
|
|
getChildAssocs(nodeId, null, null, null, null, null, this);
|
|
}
|
|
};
|
|
|
|
|
|
public List<Path> getPaths(Pair<Long, NodeRef> nodePair, boolean primaryOnly) throws InvalidNodeRefException
|
|
{
|
|
// create storage for the paths - only need 1 bucket if we are looking for the primary path
|
|
List<Path> paths = new ArrayList<Path>(primaryOnly ? 1 : 10);
|
|
// create an empty current path to start from
|
|
Path currentPath = new Path();
|
|
// create storage for touched associations
|
|
Stack<Long> assocIdStack = new Stack<Long>();
|
|
|
|
// call recursive method to sort it out
|
|
prependPaths(nodePair, null, currentPath, paths, assocIdStack, primaryOnly);
|
|
|
|
// check that for the primary only case we have exactly one path
|
|
if (primaryOnly && paths.size() != 1)
|
|
{
|
|
throw new RuntimeException("Node has " + paths.size() + " primary paths: " + nodePair);
|
|
}
|
|
|
|
// done
|
|
if (loggerPaths.isDebugEnabled())
|
|
{
|
|
StringBuilder sb = new StringBuilder(256);
|
|
if (primaryOnly)
|
|
{
|
|
sb.append("Primary paths");
|
|
}
|
|
else
|
|
{
|
|
sb.append("Paths");
|
|
}
|
|
sb.append(" for node ").append(nodePair);
|
|
for (Path path : paths)
|
|
{
|
|
sb.append("\n").append(" ").append(path);
|
|
}
|
|
loggerPaths.debug(sb);
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
private void bindFixAssocAndCollectLostAndFound(final Pair<Long, NodeRef> lostNodePair, final String lostName, final ChildAssocEntity assoc)
|
|
{
|
|
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 (assoc == null)
|
|
{
|
|
// 'child' with missing parent assoc => collect lost+found orphan child
|
|
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
|
|
deleteChildAssoc(assoc.getId());
|
|
logger.error("ALF-12358: Deleted parent - removed child assoc: "+assoc.getId());
|
|
|
|
if (assoc.isPrimary())
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
|
|
/*
|
|
// 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)
|
|
{
|
|
lostFoundNode = getNodeNotNull(nodes.get(0).getFirst());
|
|
|
|
if (nodes.size() > 1)
|
|
{
|
|
logger.warn("More than one lost_found, using first: "+lostFoundNode.getNodeRef());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
lostFoundNode = newNode(rootParentNodeId, ContentModel.ASSOC_LOST_AND_FOUND, ContentModel.ASSOC_LOST_AND_FOUND, storeRef, null, ContentModel.TYPE_LOST_AND_FOUND, Locale.US, 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
|
|
|
|
// does the node have parents
|
|
boolean hasParents = parentAssocInfo.getParentAssocs().size() > 0;
|
|
// does the current node have a root aspect?
|
|
|
|
// look for a root. If we only want the primary root, then ignore all but the top-level root.
|
|
if (!(primaryOnly && hasParents) && parentAssocInfo.isRoot()) // exclude primary search with parents present
|
|
{
|
|
// create a one-sided assoc ref for the root node and prepend to the stack
|
|
// this effectively spoofs the fact that the current node is not below the root
|
|
// - we put this assoc in as the first assoc in the path must be a one-sided
|
|
// reference pointing to the root node
|
|
ChildAssociationRef assocRef = new ChildAssociationRef(null, null, null, currentRootNodePair.getSecond());
|
|
// create a path to save and add the 'root' assoc
|
|
Path pathToSave = new Path();
|
|
Path.ChildAssocElement first = null;
|
|
for (Path.Element element : currentPath)
|
|
{
|
|
if (first == null)
|
|
{
|
|
first = (Path.ChildAssocElement) element;
|
|
}
|
|
else
|
|
{
|
|
pathToSave.append(element);
|
|
}
|
|
}
|
|
if (first != null)
|
|
{
|
|
// mimic an association that would appear if the current node was below the root node
|
|
// or if first beneath the root node it will make the real thing
|
|
ChildAssociationRef updateAssocRef = new ChildAssociationRef(
|
|
parentAssocInfo.isStoreRoot() ? ContentModel.ASSOC_CHILDREN : first.getRef().getTypeQName(),
|
|
currentRootNodePair.getSecond(),
|
|
first.getRef().getQName(),
|
|
first.getRef().getChildRef());
|
|
Path.Element newFirst = new Path.ChildAssocElement(updateAssocRef);
|
|
pathToSave.prepend(newFirst);
|
|
}
|
|
|
|
Path.Element element = new Path.ChildAssocElement(assocRef);
|
|
pathToSave.prepend(element);
|
|
|
|
// store the path just built
|
|
completedPaths.add(pathToSave);
|
|
}
|
|
|
|
if (!hasParents && !parentAssocInfo.isRoot())
|
|
{
|
|
// We appear to have an orphaned node. But we may just have a temporarily out of sync clustered cache or a
|
|
// transaction that started ages before the one that committed the cache content!. So double check the node
|
|
// isn't actually deleted.
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Stale cache detected for Node #" + currentNodeId + ": removing from cache.");
|
|
}
|
|
invalidateNodeCaches(currentNodeId);
|
|
|
|
Status currentNodeStatus = getNodeRefStatus(currentNodeRef);
|
|
if (currentNodeStatus == null || currentNodeStatus.isDeleted())
|
|
{
|
|
// Force a retry. The cached node was stale
|
|
throw new DataIntegrityViolationException("Stale cache detected for Node #" + currentNodeId);
|
|
}
|
|
|
|
// We have a corrupt repository - non-root node has a missing parent ?!
|
|
bindFixAssocAndCollectLostAndFound(currentNodePair, "nonRootNodeWithoutParents", null);
|
|
|
|
// throw - error will be logged and then bound txn listener (afterRollback) will be called
|
|
throw new NonRootNodeWithoutParentsException(currentNodePair);
|
|
}
|
|
// 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);
|
|
|
|
try
|
|
{
|
|
prependPaths(parentNodePair, currentRootNodePair, path, completedPaths, assocIdStack, primaryOnly);
|
|
}
|
|
catch (final NotLiveNodeException re)
|
|
{
|
|
if (re.getNodePair().equals(parentNodePair))
|
|
{
|
|
// We have a corrupt repository - deleted parent pointing to live child ?!
|
|
bindFixAssocAndCollectLostAndFound(currentNodePair, "childNodeWithDeletedParent", assoc);
|
|
}
|
|
// rethrow - this will cause error/rollback
|
|
throw re;
|
|
}
|
|
|
|
assocIdStack.pop();
|
|
}
|
|
// done
|
|
}
|
|
|
|
/**
|
|
* @return Returns a node's parent associations
|
|
*/
|
|
private ParentAssocsInfo getParentAssocsCached(Long nodeId)
|
|
{
|
|
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
|
|
Pair<NodeVersionKey, ParentAssocsInfo> cacheEntry = parentAssocsCache.getByKey(nodeVersionKey);
|
|
if (cacheEntry == null)
|
|
{
|
|
invalidateNodeCaches(nodeId);
|
|
throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
|
|
}
|
|
return cacheEntry.getSecond();
|
|
}
|
|
|
|
/**
|
|
* Update a node's parent associations.
|
|
*/
|
|
private void setParentAssocsCached(Long nodeId, ParentAssocsInfo parentAssocs)
|
|
{
|
|
NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId).getNodeVersionKey();
|
|
parentAssocsCache.setValue(nodeVersionKey, parentAssocs);
|
|
}
|
|
|
|
/**
|
|
* Helper method to copy cache values from one key to another
|
|
*/
|
|
private void copyParentAssocsCached(NodeVersionKey from, NodeVersionKey to)
|
|
{
|
|
ParentAssocsInfo cacheEntry = parentAssocsCache.getValue(from);
|
|
if (cacheEntry != null)
|
|
{
|
|
parentAssocsCache.setValue(to, cacheEntry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback to cache node parent assocs.
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.4
|
|
*/
|
|
private class ParentAssocsCallbackDAO extends EntityLookupCallbackDAOAdaptor<NodeVersionKey, ParentAssocsInfo, Serializable>
|
|
{
|
|
public Pair<NodeVersionKey, ParentAssocsInfo> createValue(ParentAssocsInfo value)
|
|
{
|
|
throw new UnsupportedOperationException("Nodes are created independently.");
|
|
}
|
|
|
|
public Pair<NodeVersionKey, ParentAssocsInfo> findByKey(NodeVersionKey nodeVersionKey)
|
|
{
|
|
Long nodeId = nodeVersionKey.getNodeId();
|
|
// Find out if it is a root or store root
|
|
boolean isRoot = hasNodeAspect(nodeId, ContentModel.ASPECT_ROOT);
|
|
boolean isStoreRoot = getNodeType(nodeId).equals(ContentModel.TYPE_STOREROOT);
|
|
|
|
// Select all the parent associations
|
|
List<ChildAssocEntity> assocs = selectParentAssocs(nodeId);
|
|
|
|
// Build the cache object
|
|
ParentAssocsInfo value = new ParentAssocsInfo(isRoot, isStoreRoot, assocs);
|
|
|
|
// Now check if we are seeing the correct version of the node
|
|
if (assocs.isEmpty())
|
|
{
|
|
// No results. Currently Alfresco has very few parentless nodes (root nodes)
|
|
// and the lack of parent associations will be cached, anyway.
|
|
// But to match earlier fixes of ALF-12393, we do a double-check of the node's details
|
|
NodeEntity nodeCheckFromDb = selectNodeById(nodeId, null);
|
|
if (nodeCheckFromDb == null || !nodeCheckFromDb.getNodeVersionKey().equals(nodeVersionKey))
|
|
{
|
|
// The node is gone or has moved on in version
|
|
invalidateNodeCaches(nodeId);
|
|
throw new DataIntegrityViolationException(
|
|
"Detected stale node entry: " + nodeVersionKey +
|
|
" (now " + nodeCheckFromDb + ")");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ChildAssocEntity childAssoc = assocs.get(0);
|
|
// What is the real (at least to this txn) version of the child node?
|
|
NodeVersionKey childNodeVersionKeyFromDb = childAssoc.getChildNode().getNodeVersionKey();
|
|
if (!childNodeVersionKeyFromDb.equals(nodeVersionKey))
|
|
{
|
|
// This method was called with a stale version
|
|
invalidateNodeCaches(nodeId);
|
|
throw new DataIntegrityViolationException(
|
|
"Detected stale node entry: " + nodeVersionKey +
|
|
" (now " + childNodeVersionKeyFromDb + ")");
|
|
}
|
|
}
|
|
|
|
// Done
|
|
return new Pair<NodeVersionKey, ParentAssocsInfo>(nodeVersionKey, value);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Bulk caching
|
|
*/
|
|
|
|
@Override
|
|
public void setCheckNodeConsistency()
|
|
{
|
|
if (nodesTransactionalCache != null)
|
|
{
|
|
nodesTransactionalCache.setDisableSharedCacheReadForTransaction(true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void cacheNodesById(List<Long> nodeIds)
|
|
{
|
|
/*
|
|
* ALF-2712: Performance degradation from 3.1.0 to 3.1.2
|
|
* ALF-2784: Degradation of performance between 3.1.1 and 3.2x (observed in JSF)
|
|
*
|
|
* There is an obvious cost associated with querying the database to pull back nodes,
|
|
* and there is additional cost associated with putting the resultant entries into the
|
|
* caches. It is NO MORE expensive to check the cache than it is to put an entry into it
|
|
* - and probably cheaper considering cache replication - so we start checking nodes to see
|
|
* if they have entries before passing them over for batch loading.
|
|
*
|
|
* However, when running against a cold cache or doing a first-time query against some
|
|
* part of the repo, we will be checking for entries in the cache and consistently getting
|
|
* no results. To avoid unnecessary checking when the cache is PROBABLY cold, we
|
|
* examine the ratio of hits/misses at regular intervals.
|
|
*/
|
|
if (nodeIds.size() < 10)
|
|
{
|
|
// We only cache where the number of results is potentially
|
|
// a problem for the N+1 loading that might result.
|
|
return;
|
|
}
|
|
|
|
int foundCacheEntryCount = 0;
|
|
int missingCacheEntryCount = 0;
|
|
boolean forceBatch = false;
|
|
|
|
List<Long> batchLoadNodeIds = new ArrayList<Long>(nodeIds.size());
|
|
for (Long nodeId : nodeIds)
|
|
{
|
|
if (!forceBatch)
|
|
{
|
|
// Is this node in the cache?
|
|
if (nodesCache.getValue(nodeId) != null)
|
|
{
|
|
foundCacheEntryCount++; // Don't add it to the batch
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
missingCacheEntryCount++; // Fall through and add it to the batch
|
|
}
|
|
if (foundCacheEntryCount + missingCacheEntryCount % 100 == 0)
|
|
{
|
|
// We force the batch if the number of hits drops below the number of misses
|
|
forceBatch = foundCacheEntryCount < missingCacheEntryCount;
|
|
}
|
|
}
|
|
|
|
batchLoadNodeIds.add(nodeId);
|
|
}
|
|
|
|
int size = batchLoadNodeIds.size();
|
|
cacheNodesBatch(batchLoadNodeIds);
|
|
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Pre-loaded " + size + " nodes.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
* <p/>
|
|
* Loads properties, aspects, parent associations and the ID-noderef cache.
|
|
*/
|
|
public void cacheNodes(List<NodeRef> nodeRefs)
|
|
{
|
|
/*
|
|
* ALF-2712: Performance degradation from 3.1.0 to 3.1.2
|
|
* ALF-2784: Degradation of performance between 3.1.1 and 3.2x (observed in JSF)
|
|
*
|
|
* There is an obvious cost associated with querying the database to pull back nodes,
|
|
* and there is additional cost associated with putting the resultant entries into the
|
|
* caches. It is NO MORE expensive to check the cache than it is to put an entry into it
|
|
* - and probably cheaper considering cache replication - so we start checking nodes to see
|
|
* if they have entries before passing them over for batch loading.
|
|
*
|
|
* However, when running against a cold cache or doing a first-time query against some
|
|
* part of the repo, we will be checking for entries in the cache and consistently getting
|
|
* no results. To avoid unnecessary checking when the cache is PROBABLY cold, we
|
|
* examine the ratio of hits/misses at regular intervals.
|
|
*/
|
|
if (nodeRefs.size() < 10)
|
|
{
|
|
// We only cache where the number of results is potentially
|
|
// a problem for the N+1 loading that might result.
|
|
return;
|
|
}
|
|
int foundCacheEntryCount = 0;
|
|
int missingCacheEntryCount = 0;
|
|
boolean forceBatch = false;
|
|
|
|
// Group the nodes by store so that we don't *have* to eagerly join to store to get query performance
|
|
Map<StoreRef, List<String>> uuidsByStore = new HashMap<StoreRef, List<String>>(3);
|
|
for (NodeRef nodeRef : nodeRefs)
|
|
{
|
|
if (!forceBatch)
|
|
{
|
|
// Is this node in the cache?
|
|
if (nodesCache.getKey(nodeRef) != null)
|
|
{
|
|
foundCacheEntryCount++; // Don't add it to the batch
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
missingCacheEntryCount++; // Fall through and add it to the batch
|
|
}
|
|
if (foundCacheEntryCount + missingCacheEntryCount % 100 == 0)
|
|
{
|
|
// We force the batch if the number of hits drops below the number of misses
|
|
forceBatch = foundCacheEntryCount < missingCacheEntryCount;
|
|
}
|
|
}
|
|
|
|
StoreRef storeRef = nodeRef.getStoreRef();
|
|
List<String> uuids = (List<String>) uuidsByStore.get(storeRef);
|
|
if (uuids == null)
|
|
{
|
|
uuids = new ArrayList<String>(nodeRefs.size());
|
|
uuidsByStore.put(storeRef, uuids);
|
|
}
|
|
uuids.add(nodeRef.getId());
|
|
}
|
|
int size = nodeRefs.size();
|
|
nodeRefs = null;
|
|
// Now load all the nodes
|
|
for (Map.Entry<StoreRef, List<String>> entry : uuidsByStore.entrySet())
|
|
{
|
|
StoreRef storeRef = entry.getKey();
|
|
List<String> uuids = entry.getValue();
|
|
cacheNodes(storeRef, uuids);
|
|
}
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Pre-loaded " + size + " nodes.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the nodes into cache using batching.
|
|
*/
|
|
private void cacheNodes(StoreRef storeRef, List<String> uuids)
|
|
{
|
|
StoreEntity store = getStoreNotNull(storeRef);
|
|
Long storeId = store.getId();
|
|
|
|
int batchSize = 256;
|
|
SortedSet<String> batch = new TreeSet<String>();
|
|
for (String uuid : uuids)
|
|
{
|
|
batch.add(uuid);
|
|
if (batch.size() >= batchSize)
|
|
{
|
|
// Preload
|
|
cacheNodesNoBatch(selectNodesByUuids(storeId, batch, Boolean.FALSE));
|
|
batch.clear();
|
|
}
|
|
}
|
|
// Load any remaining nodes
|
|
if (batch.size() > 0)
|
|
{
|
|
cacheNodesNoBatch(selectNodesByUuids(storeId, batch, Boolean.FALSE));
|
|
}
|
|
}
|
|
|
|
private void cacheNodesBatch(List<Long> nodeIds)
|
|
{
|
|
int batchSize = 256;
|
|
SortedSet<Long> batch = new TreeSet<Long>();
|
|
for (Long nodeId : nodeIds)
|
|
{
|
|
batch.add(nodeId);
|
|
if (batch.size() >= batchSize)
|
|
{
|
|
// Preload
|
|
cacheNodesNoBatch(selectNodesByIds(batch, Boolean.FALSE));
|
|
batch.clear();
|
|
}
|
|
}
|
|
// Load any remaining nodes
|
|
if (batch.size() > 0)
|
|
{
|
|
cacheNodesNoBatch(selectNodesByIds(batch, Boolean.FALSE));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk-fetch the nodes for a given store. All nodes passed in are fetched.
|
|
*/
|
|
private void cacheNodesNoBatch(List<Node> nodes)
|
|
{
|
|
// Get the nodes
|
|
SortedSet<Long> aspectNodeIds = new TreeSet<Long>();
|
|
SortedSet<Long> propertiesNodeIds = new TreeSet<Long>();
|
|
Map<Long, NodeVersionKey> nodeVersionKeysFromCache = new HashMap<Long, NodeVersionKey>(nodes.size()*2); // Keep for quick lookup
|
|
for (Node node : nodes)
|
|
{
|
|
Long nodeId = node.getId();
|
|
NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
|
|
nodesCache.setValue(nodeId, node);
|
|
if (propertiesCache.getValue(nodeVersionKey) == null)
|
|
{
|
|
propertiesNodeIds.add(nodeId);
|
|
}
|
|
if (aspectsCache.getValue(nodeVersionKey) == null)
|
|
{
|
|
aspectNodeIds.add(nodeId);
|
|
}
|
|
nodeVersionKeysFromCache.put(nodeId, nodeVersionKey);
|
|
}
|
|
|
|
if(logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Pre-loaded " + propertiesNodeIds.size() + " properties");
|
|
logger.debug("Pre-loaded " + propertiesNodeIds.size() + " aspects");
|
|
}
|
|
|
|
Map<NodeVersionKey, Set<QName>> nodeAspects = selectNodeAspects(aspectNodeIds);
|
|
for (Map.Entry<NodeVersionKey, Set<QName>> entry : nodeAspects.entrySet())
|
|
{
|
|
NodeVersionKey nodeVersionKeyFromDb = entry.getKey();
|
|
Long nodeId = nodeVersionKeyFromDb.getNodeId();
|
|
Set<QName> qnames = entry.getValue();
|
|
setNodeAspectsCached(nodeId, qnames);
|
|
aspectNodeIds.remove(nodeId);
|
|
}
|
|
// Cache the absence of aspects too!
|
|
for (Long nodeId: aspectNodeIds)
|
|
{
|
|
setNodeAspectsCached(nodeId, Collections.<QName>emptySet());
|
|
}
|
|
|
|
// First ensure all content data are pre-cached, so we don't have to load them individually when converting properties
|
|
contentDataDAO.cacheContentDataForNodes(propertiesNodeIds);
|
|
|
|
// Now bulk load the properties
|
|
Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> propsByNodeId = selectNodeProperties(propertiesNodeIds);
|
|
for (Map.Entry<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> entry : propsByNodeId.entrySet())
|
|
{
|
|
Long nodeId = entry.getKey().getNodeId();
|
|
Map<NodePropertyKey, NodePropertyValue> propertyValues = entry.getValue();
|
|
Map<QName, Serializable> props = nodePropertyHelper.convertToPublicProperties(propertyValues);
|
|
setNodePropertiesCached(nodeId, props);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
* <p/>
|
|
* Simply clears out all the node-related caches.
|
|
*/
|
|
public void clear()
|
|
{
|
|
clearCaches();
|
|
}
|
|
|
|
/*
|
|
* Transactions
|
|
*/
|
|
|
|
public Long getMaxTxnIdByCommitTime(long maxCommitTime)
|
|
{
|
|
Transaction txn = selectLastTxnBeforeCommitTime(maxCommitTime);
|
|
return (txn == null ? null : txn.getId());
|
|
}
|
|
|
|
public int getTransactionCount()
|
|
{
|
|
return selectTransactionCount();
|
|
}
|
|
|
|
public Transaction getTxnById(Long txnId)
|
|
{
|
|
return selectTxnById(txnId);
|
|
}
|
|
|
|
public List<NodeRef.Status> getTxnChanges(Long txnId)
|
|
{
|
|
return getTxnChangesForStore(null, txnId);
|
|
}
|
|
|
|
public List<NodeRef.Status> getTxnChangesForStore(StoreRef storeRef, Long txnId)
|
|
{
|
|
Long storeId = (storeRef == null) ? null : getStoreNotNull(storeRef).getId();
|
|
List<NodeEntity> nodes = selectTxnChanges(txnId, storeId);
|
|
// Convert
|
|
List<NodeRef.Status> nodeStatuses = new ArrayList<NodeRef.Status>(nodes.size());
|
|
for (NodeEntity node : nodes)
|
|
{
|
|
nodeStatuses.add(node.getNodeStatus());
|
|
}
|
|
|
|
// Done
|
|
return nodeStatuses;
|
|
}
|
|
|
|
public int getTxnUpdateCount(Long txnId)
|
|
{
|
|
return selectTxnNodeChangeCount(txnId, Boolean.TRUE);
|
|
}
|
|
|
|
public int getTxnDeleteCount(Long txnId)
|
|
{
|
|
return selectTxnNodeChangeCount(txnId, Boolean.FALSE);
|
|
}
|
|
|
|
public List<Transaction> getTxnsByCommitTimeAscending(
|
|
Long fromTimeInclusive,
|
|
Long toTimeExclusive,
|
|
int count,
|
|
List<Long> excludeTxnIds,
|
|
boolean remoteOnly)
|
|
{
|
|
// Pass the current server ID if it is to be excluded
|
|
Long serverId = remoteOnly ? serverId = getServerId() : null;
|
|
return selectTxns(fromTimeInclusive, toTimeExclusive, count, null, excludeTxnIds, serverId, Boolean.TRUE);
|
|
}
|
|
|
|
public List<Transaction> getTxnsByCommitTimeDescending(
|
|
Long fromTimeInclusive,
|
|
Long toTimeExclusive,
|
|
int count,
|
|
List<Long> excludeTxnIds,
|
|
boolean remoteOnly)
|
|
{
|
|
// Pass the current server ID if it is to be excluded
|
|
Long serverId = remoteOnly ? serverId = getServerId() : null;
|
|
return selectTxns(fromTimeInclusive, toTimeExclusive, count, null, excludeTxnIds, serverId, Boolean.FALSE);
|
|
}
|
|
|
|
public List<Transaction> getTxnsByCommitTimeAscending(List<Long> includeTxnIds)
|
|
{
|
|
return selectTxns(null, null, null, includeTxnIds, null, null, Boolean.TRUE);
|
|
}
|
|
|
|
public List<Long> getTxnsUnused(Long minTxnId, long maxCommitTime, int count)
|
|
{
|
|
return selectTxnsUnused(minTxnId, maxCommitTime, count);
|
|
}
|
|
|
|
public void purgeTxn(Long txnId)
|
|
{
|
|
deleteTransaction(txnId);
|
|
}
|
|
|
|
public static final Long LONG_ZERO = 0L;
|
|
|
|
public Long getMinTxnCommitTime()
|
|
{
|
|
Long time = selectMinTxnCommitTime();
|
|
return (time == null ? LONG_ZERO : time);
|
|
}
|
|
|
|
public Long getMaxTxnCommitTime()
|
|
{
|
|
Long time = selectMaxTxnCommitTime();
|
|
return (time == null ? LONG_ZERO : time);
|
|
}
|
|
|
|
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 NodeEntity selectStoreRootNode(Long storeId);
|
|
protected abstract NodeEntity selectStoreRootNode(StoreRef storeRef);
|
|
protected abstract Long insertStore(StoreEntity store);
|
|
protected abstract int updateStoreRoot(StoreEntity store);
|
|
protected abstract int updateStore(StoreEntity store);
|
|
protected abstract Long insertNode(NodeEntity node);
|
|
protected abstract int updateNode(NodeUpdateEntity nodeUpdate);
|
|
protected abstract int updateNodes(Long txnId, List<Long> nodeIds);
|
|
protected abstract void updatePrimaryChildrenSharedAclId(
|
|
Long txnId,
|
|
Long primaryParentNodeId,
|
|
Long optionalOldSharedAlcIdInAdditionToNull,
|
|
Long newSharedAlcId);
|
|
protected abstract int deleteNodeById(Long nodeId, boolean deletedOnly);
|
|
protected abstract int deleteNodesByCommitTime(boolean deletedOnly, long maxTxnCommitTimeMs);
|
|
protected abstract NodeEntity selectNodeById(Long id, Boolean deleted);
|
|
protected abstract NodeEntity selectNodeByNodeRef(NodeRef nodeRef, Boolean deleted);
|
|
protected abstract List<Node> selectNodesByUuids(Long storeId, SortedSet<String> uuids, Boolean deleted);
|
|
protected abstract List<Node> selectNodesByIds(SortedSet<Long> ids, Boolean deleted);
|
|
protected abstract Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Set<Long> nodeIds);
|
|
protected abstract Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Long nodeId);
|
|
protected abstract Map<NodeVersionKey, Map<NodePropertyKey, NodePropertyValue>> selectNodeProperties(Long nodeId, Set<Long> qnameIds);
|
|
protected abstract int deleteNodeProperties(Long nodeId, Set<Long> qnameIds);
|
|
protected abstract int deleteNodeProperties(Long nodeId, List<NodePropertyKey> propKeys);
|
|
protected abstract void insertNodeProperties(Long nodeId, Map<NodePropertyKey, NodePropertyValue> persistableProps);
|
|
protected abstract Map<NodeVersionKey, Set<QName>> selectNodeAspects(Set<Long> nodeIds);
|
|
protected abstract void insertNodeAspect(Long nodeId, Long qnameId);
|
|
protected abstract int deleteNodeAspects(Long nodeId, Set<Long> qnameIds);
|
|
protected abstract void selectNodesWithAspects(
|
|
List<Long> qnameIds,
|
|
Long minNodeId, Long maxNodeId,
|
|
NodeRefQueryCallback resultsCallback);
|
|
protected abstract Long insertNodeAssoc(Long sourceNodeId, Long targetNodeId, Long assocTypeQNameId, int assocIndex);
|
|
protected abstract int updateNodeAssoc(Long id, int assocIndex);
|
|
protected abstract int deleteNodeAssoc(Long sourceNodeId, Long targetNodeId, Long assocTypeQNameId);
|
|
protected abstract int deleteNodeAssocsToAndFrom(Long nodeId);
|
|
protected abstract int deleteNodeAssocsToAndFrom(Long nodeId, Set<Long> assocTypeQNameIds);
|
|
protected abstract int deleteNodeAssocs(List<Long> ids);
|
|
protected abstract List<NodeAssocEntity> selectNodeAssocsBySource(Long sourceNodeId, Long typeQNameId);
|
|
protected abstract List<NodeAssocEntity> selectNodeAssocsByTarget(Long targetNodeId, Long typeQNameId);
|
|
protected abstract NodeAssocEntity selectNodeAssocById(Long assocId);
|
|
protected abstract int selectNodeAssocMaxIndex(Long sourceNodeId, Long assocTypeQNameId);
|
|
protected abstract Long insertChildAssoc(ChildAssocEntity assoc);
|
|
protected abstract int deleteChildAssocById(Long assocId);
|
|
protected abstract int updateChildAssocIndex(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
int index);
|
|
protected abstract int updateChildAssocsUniqueName(Long childNodeId, String name);
|
|
protected abstract int deleteChildAssocsToAndFrom(Long nodeId);
|
|
protected abstract ChildAssocEntity selectChildAssoc(Long assocId);
|
|
protected abstract List<ChildAssocEntity> selectChildNodeIds(
|
|
Long nodeId,
|
|
Boolean isPrimary,
|
|
Long minAssocIdInclusive,
|
|
int maxResults);
|
|
protected abstract List<NodeIdAndAclId> selectPrimaryChildAcls(Long nodeId);
|
|
protected abstract List<ChildAssocEntity> selectChildAssoc(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName);
|
|
/**
|
|
* Parameters are all optional except the parent node ID and the callback
|
|
*/
|
|
protected abstract void selectChildAssocs(
|
|
Long parentNodeId,
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
Boolean isPrimary,
|
|
Boolean sameStore,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
protected abstract void selectChildAssocs(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
int maxResults,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
protected abstract void selectChildAssocs(
|
|
Long parentNodeId,
|
|
Set<QName> assocTypeQNames,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
protected abstract ChildAssocEntity selectChildAssoc(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
String childName);
|
|
protected abstract void selectChildAssocs(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
Collection<String> childNames,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
protected abstract void selectChildAssocsByPropertyValue(
|
|
Long parentNodeId,
|
|
QName propertyQName,
|
|
NodePropertyValue nodeValue,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
protected abstract void selectChildAssocsByChildTypes(
|
|
Long parentNodeId,
|
|
Set<QName> childNodeTypeQNames,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
protected abstract void selectChildAssocsWithoutParentAssocsOfType(
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
/**
|
|
* Parameters are all optional except the parent node ID and the callback
|
|
*/
|
|
protected abstract void selectParentAssocs(
|
|
Long childNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
Boolean isPrimary,
|
|
ChildAssocRefQueryCallback resultsCallback);
|
|
protected abstract List<ChildAssocEntity> selectParentAssocs(Long childNodeId);
|
|
/**
|
|
* No DB constraint, so multiple returned
|
|
*/
|
|
protected abstract List<ChildAssocEntity> selectPrimaryParentAssocs(Long childNodeId);
|
|
protected abstract int updatePrimaryParentAssocs(
|
|
Long childNodeId,
|
|
Long parentNodeId,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
String childNodeName);
|
|
/**
|
|
* Moves all node-linked data from one node to another. The source node will be left
|
|
* in an orphaned state and without any attached data other than the current transaction.
|
|
*
|
|
* @param fromNodeId the source node
|
|
* @param toNodeId the target node
|
|
*/
|
|
protected abstract void moveNodeData(Long fromNodeId, Long toNodeId);
|
|
|
|
protected abstract void deleteSubscriptions(Long nodeId);
|
|
|
|
protected abstract Transaction selectLastTxnBeforeCommitTime(Long maxCommitTime);
|
|
protected abstract int selectTransactionCount();
|
|
protected abstract Transaction selectTxnById(Long txnId);
|
|
protected abstract List<NodeEntity> selectTxnChanges(Long txnId, Long storeId);
|
|
/**
|
|
* @param txnId the transaction ID (never <tt>null</tt>)
|
|
* @param updates <tt>TRUE</tt> to select node updates, <tt>FALSE</tt> to select
|
|
* node deletions or <tt>null</tt> to select all changes.
|
|
* @return Returns the number of nodes affected by the transaction
|
|
*/
|
|
protected abstract int selectTxnNodeChangeCount(Long txnId, Boolean updates);
|
|
protected abstract List<Transaction> selectTxns(
|
|
Long fromTimeInclusive,
|
|
Long toTimeExclusive,
|
|
Integer count,
|
|
List<Long> includeTxnIds,
|
|
List<Long> excludeTxnIds,
|
|
Long excludeServerId,
|
|
Boolean ascending);
|
|
protected abstract List<Long> selectTxnsUnused(Long minTxnId, Long maxCommitTime, Integer count);
|
|
protected abstract Long selectMinTxnCommitTime();
|
|
protected abstract Long selectMaxTxnCommitTime();
|
|
protected abstract Long selectMinTxnId();
|
|
protected abstract Long selectMaxTxnId();
|
|
protected abstract Long selectMinUnusedTxnCommitTime();
|
|
}
|