From 8739e205cb11af14b7d8febe940e50d896cf6d2c Mon Sep 17 00:00:00 2001 From: Dave Ward Date: Mon, 11 Apr 2011 19:26:26 +0000 Subject: [PATCH] Merged V3.4-TEAM to HEAD 24882: Branch for Team 24883: Version Edition label 24906: Partial implementation of ALF-6566. Replace gears image... This check-in adds support for the feature as described in JIRA. However we do not yet have the icon graphics files. I have therefore used some temporary icons in order to flush out implementation issues and to enable browser-based testing. A future check-in will add the correct placeholder icons and if they follow the naming convention, no change in code should be necessary. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added and modified some test cases. 24909: ALF-6606: Configurable sorting for Share Document Library. Also change Show/Hide Folder button to icon. (Placeholder icons) 24914: ALF-6564: Initial cut of JMXFormProcessor, will show all attributes of a given MBean and has the ability to persist an attribute value. 24916: Initial Alfresco Team project structure and build targets 24917: First tranche of thumbnail icons for ALF-6566. Replace gears icon. Have added icons for some basic mime types and removed some temporary icons. I had to do a clean repo build to flush out the temporary docx and jpeg icons. 24919: svn ignore patterns 24922: Missed files for team build targets 24932: Add a simple AVM to Zip exporter service, initially for use by the site exporter 24935: Start on a java backed webscript to export a site, including all the details (ALF-6567) 24945: Removed two unnecessary TODOs from the RatingService code. 24949: Improved documentation for RatingService webscripts. Added sample JSON responses for two webscripts. 24956: Implementation of ALF-6792. RatingSchemes should allow self-rating. Formerly, the cm:creator of a node could never apply a rating to it. An unchecked RatingServiceException would be thrown if they attempted to do so. With this check-in ratingScheme beans have a new property: selfRatingAllowed. The exception will now only be thrown if a cm:creator attempts to rate their own content in a scheme where this property is false. The property is true by default and in particular, is true for "likesRatingScheme". 24958: ALF-6599: First pass DND file upload - FireFox4/Chrome support for single/multifile DND upload with basic progress 24961: Added additional data to the response coming from rating.post. I have added averageRating, ratingsTotal and ratingsCount to that response. 24963: ALF-6625: Balloon Popup Framework. Initial checking. More work maybe required when we start using it, or when designs are complete. 24964: ALF-6598 "Configure Site page refactor" - page.lib.js - Freemarker lib to sort out used pages and which links and labels to use - Drag n drop * now supports horisontal lists * improved tabfocus and keynavigations * callbacks can be attached on dnd events: delete-clicks, enter-clicks, element-moved, element-duplicated - Alfresco.util.isVisible - Checks if the element and all its parents are visible and displayed in the ui. - Alfresco.util.PopupManager.getUserInput now accepts a "input" parameter that can be set to "text" ("textarea" is default) - Automatic click listeners can be defined as: ${msg("link.rename")} which will call: onRenameClick: function(pageId, anchor) {} 24965: Changing date format of appliedAt JSON property to xmldate. 24971: ALF-6600: Event handling performance improvements for DND file upload 24976: Added team.war to continuous assemble-tomcat 24977: ALF-6600: Initial pass at DND upload highlighting 24979: Add method AuthorityService.getAuthorityNodeRef, and unit tests for it 24980: UI label change for Team install 24981: Set security permission on the new AuthorityService.getAuthorityNodeRef method, in line with the other get methods 24987: ALF-6607 - Likes, favourite and comment actions. Also DocLib panel redesign. Updates to Rating Service to better match Team use cases. 24989: ALF-6601: Added JavaScript multipart data constructor for FireFox 3.6 support 24990: Fix "Access Denied" error when navigating into a "Liked" folder. 25000: Merged V3.4 to V3.4-TEAM 24999: ALF-6764 - Updated copyright year to 2011 25004: ALF-6601: Set file size limit, HTTP status code error handling 25007: Add user, group and person ACP exports to the Site Export webscript. (The user export is provisional, pending a service level way to access the user NodeRef) 25008: ALF-6788: Update dashboard template to provide full width component and presets updates to reflect new default layouts. 25017: Initial check-in for ALF-6809. A service to managed deleted items. This check in adds a basic REST API for GETting and DELETEing deleted items from the archive store. JUnit tests of the REST API are included but are not yet complete. 25018: Trivial fix for failing test case related to ALF-6809 25019: A previous check-in (25017) accidentally included an extra Java file that wasn't ready. File is related to ALF-6809 and will be implemented today. This check-in removes the internals of the class to allow it to compile. The class is not injected into the spring application context and is essentially dead code pending its implementation later. 25027: Adding nodeType to the archivednodes.get webscript response. Requested by UI as part of ALF-6809. 25035: ALF-6789: Personal dashboard welcome (with wireframe styling) 25036: ALF-6782 - Undelete administration console page (WIP) ALF-6784, ALF-6786, ALF-6787 25037: ALF-6564: Added operations to default form, all parameter-less operations are displayed as a button on the form. Clicking a button executes the operation after saving all attribute values. 25038: Added a node filter to the archivednodes.get webscript. Currently excluding cm:thumbnails. A new bean on the script's controller class that allows injection of nodetypes to be excluded. Should be trivial to exclude other node types later and easy to exclude based on other criteria. 25039: Part of ALF-6809. Deleted items were returned from archivednodes.get in the wrong order. They are now sorted by archive datetime most recent first. (Was previously sorted with most recent last.) Also switched the test case, which was backwards too! 25040: Changing some copyright headers from 2010 to 2011. 25041: ALF-6564: JMX forms can now be configured with selective attributes and the set of operations. Also includes an example form configuration for the outbound email almost matching Linton's wireframes. 25042: ALF-6788: Fix rendering of old (V3.4) user dashboards stored in AVM with new Team layout 25044: Part of ALF-6809. Added displayPath value to archivednodes.get. 25047: Part of ALF-6809. Reconfiguration of cm:thumbnail archive behaviour. Formerly, we were archiving cm:thumbnails on deletion and filtering them from the archivednodes.get webscript which was not ideal as they remained in the archive. This check in removes the filter on the GET webscript and prevents cm:thumbnails from being archived in the first place. 25049: ALF-6782 - added Trashcan to admin tools menu - cleaned up list, show display path 25050: ALF-6782 - css tweaks 25052: Added check for multiple calls to onHistoryManagerReady() as workaround for issue with multiple History Manager objects when using Alfresco.util.DataTable. 25053: Test case refactoring as part of ALF-6809 25055: ALF-6803: First drop of aggregate file upload progress information. 25056: Part of ALF-6809. A preliminary PUT webscript that is used to restore nodes from the archive to their original loaction. Currently only works on a single node per call (which is what the JIRA requires). Test code currently incomplete. I'm checking in now to give UI something to work with. 25057: Fix for failing test case, which was part of ALF-6566. 25058: ALF-6567 - AVM importer, which can load a zip file's contents into a specified AVM filesystem. Includes a bootstrap wrapper around the main importer. 25068: Part ALF-6893: RTEAM 03: Expose restrictions using RepoAdminService - Subtask of ALF-6832: AT17: License restriction reporting - Added RepoAdminService.getRestrictions in Java and Webscripts APIs - Currently nulls returned indication 'no restrictions' - Cleaned up unused AdminService 25069: ALF-6625 - adds a switch to control what calendar views are enabled and switches off Day, Week and Month views in Team. 25073: Test case overhaul and minor tidying of code. Part of ALF-6809. 25074: ALF-6803: Style updates to upload progress info 25075: ALF-6601: Updated in-memory file upload check so that failure is based on total (not individual) file size. 25081: Merged V3.4 to V3.4-TEAM 25051: Build Fix: ALF-6865 CopyServiceImplTest.testCopyToNewNodeWithPermissions failing on permission copying 25082: Part ALF-6893: RTEAM 03: Expose restrictions using RepoAdminService - Subtask of ALF-6832: AT17: License restriction reporting - Added RepoAdminService.getUsage in Java and Webscripts APIs 25084: Improve how the Site Exporter gets at the users for a site, and have the user authentication details export skipped if a non-repository based authenticator is enabled 25085: Final part of ALF-6809. Support for paging the results from archivednodes.get. Follows the standard maxItems, skipCount convention and enables paging in the trashcan view automatically thanks to Kev's use of the convention. 25088: Final, final part of ALF-6809. Throwing 4xx, 5xx exceptions for nodeArchiveService-level failures to restore nodes 25090: ALF-6601: Updated in-memory upload limit to be configurable 25096: ALF-6601: Updates to DND highlighting and behaviour, set correct upload limit in bytes, NLS updates 25101: Fixing failing test cases. Fallout from recent changes to the JSON response formats in these webscripts. 25103: ALF-6613 - SpringSurf improvements to allow easier refactoring of Document Details page - latest SpringSurf libs with RequestCachingConnector improvements - removed manual request level caching of remote calls responses in web-tier components - now completely automatic 25104: ALF-6803: DND upload dialog styling 25106: ALF-6802: Added feature detection to disable drag'n'drop events (disables for IE6, IE7 & IE8) 25108: ALF-6564: Added a check to the JMXFormProcessor to ensure the current user is an administrator. Also relaxed the rules in the checkbox control so it can be used for string values not just boolean values. 25113: ALF-6789: Updated CSS to support IE6/IE7 25116: Prevent server-side exception when navigating trashcan; related to paging. (ALF-6809) 25117: ALF-6824 - Only show "Like" and "Comment" actions if a user has the correct permissions. Additional fixes for IE7. 25118: Fix incorrect category aspect name. Missing file from r25117 25119: ALF-6645 - Share and Team branding updates 25121: Removed erroneous comment block 25123: Merged HEAD to BRANCHES/V3.4-TEAM: 25115: Fixes: ALF-6336 - resolved incorrectly translated date formatting strings. 25124: Add a new bootstrap component for bootstrapping Sites. Handles the contents, AVM and authentication, people and their group membership to follow. (Uses singleton spring beans in line with the patch service's use, to ensure that when loaded from a bootstrap extension things still occur in the correct order) 25128: Merged V3.4 to V3.4-TEAM 25127: Merged V3.3 to V3.4 25126: ALF-6903 - Share theme feature does not work. Also fixes issue with MultiThreadedHttpConnectionManager in SpringSurf. 25130: Continue with the site bootstrapper - finish supporting the loading of the site contents ACP 25132: Work on ALF-6832:TR25: License restriction reporting - ALF-6893: RTEAM 03: Expose restrictions using RepoAdminService - ALF-6911: RTEAM 02: Record and expose system attributes - Added RepoUsageComponent - Unit test incl. testLicenseUse - Persists and retrieves usage data using AttributeService 25133: ALF-6789: Added "Close" link to dynamic-welcome dashlet and associated webscript 25135: ALF-6789: Add missing localization 25136: ALF-6601: Defensively code against missing config 25137: Fix up of transfer test. 25138: Flattening of user preferences remote calls - ensures /preferences hits the RequestCachingConnector - reduces no. of remote calls by 3 for the doclib and by 4 for a site dashboard. 25139: ALF-6804: Disable drop outside of document list 25140: ALF-6887 - for sending HTML emails - prelim support - either text explicitly starts with " share.protocol, share.host, share.port, share.context (note: you may need to override for dev/demo env) 25178: ALF-6564: TR12: Implement JMX Form Processor. Added tests, fixed a couple of bugs in form processor and added all Mike's form config for MBeans we're provisionally exposing. 25187: ALF-6998 - activities feed email - quick fix if running within repo-only context (eg. via Eclipse) 25188: Prevented NPE with Enterprise edition built as community. 25189: Expose getAllowWrite on the TransactionService API - This is a user-independent flag as opposed to isReadOnly, which makes allowance for the 'System' user 25190: Work on ALF-6832:TR25: License restriction reporting - ALF-6893: RTEAM 03: Expose restrictions using RepoAdminService - Use TransactionService as the source of 'isReadOnly' 25191: Fix to issue where null could be passed as URI Tokens - fixes issue where pages failed to render on first display since rev 25166 25192: CSS tweak 25194: I18N'd system startup message 25195: Restrictions now sets current time and readOnly property. 25196: ALF-6790: Added user prefs to enable site welcome dashlet visibility 25198: ALF-6790: Fix user close welcome dashlet persistence. Added site welcome dashlet close reload. 25200: ALF-6803: Update upload dialog title to display (encoded) folder name (not entire path) 25203: ALF-6834 - TR15: Activities feed email notifications - ALF-6931 - RTEAM 23: update activity feed DAO to filter by min id and/or max items - ALF-6929 - RTEAM 21: now uses last feed id for each user - minor cleanup (eg. tweak template to deal with null message) 25204: ALF-6790: Fixed broken site preference logic 25206: Checkpoint ALF-6608 - Inline property editing - name field. Also paves the way for ALF-6611 - Inline property editing - any meta field. Currently does not constrain input values, nor handle repository errors (e.g. duplicate name). 25212: ALF-6602: Updates to reflect latest designs 25213: ALF-6564: More MBean form config 25248: A new schema version range for 3.5 25249: ALF-7049 - RTEAM 12: Disable Alfresco Explorer 25252: ALF-7050 - RTEAM 09: Disable MT 25263: Fix trailing comma for IE browsers 25268: Implementation of ALF-6829 Popularity of a node (RatingService). This check-in is actually a fairly generic support for rolled up property values within rating schemes. Popularity is currently the only concrete rollup in the system, but it should be possible to add more without too much difficulty. Given the published API for the rating service, we have added rollups for "ratingCount" and "ratingTotal" for the built-in rating schemes (likes). Therefore searching/sorting on either cm:likesRatingSchemeCount or cm:likesRatingSchemeTotal, both of which are defined in an aspect cm:likesRatingSchemeRollups should give popularity for the likes rating scheme. Additions to the content model. I have added aspects, properties for the built-in rollups, namely count and total. Spring changes The rating scheme(s) now have rollups injected into them. These rollups are algorithm classes that know how to calculate the rolled up props. Naming conventions are used to determine the output property and are captured in RatingRollupNamingConventionsUtil.java. API changes New methods: RatingService.getRatingRollup(NodeRef, String, String) & RatingScheme.getPropertyRollups() Changes to the RatingService implementation and JUnit test code. To roll your own rating rollups: To add your own rating rollup, you need to reuse one of the existing AbstractRatingRollupAlgorithm subclasses or create a new one. That class will produce the value of the rolled up property by iterating the ratings in the given rating scheme for a given node. You need to inject your rollup algorithm into the rating scheme bean in rating-services-context.xml On applyRating() and removeRating() the RatingService will check the rating scheme for any registered rollup algorithms and will have them recalculated. It will then use the naming conventions described in the JIRA and in RatingRollupNamingConventionsUtil to get aspect/property names for each of the rollups. It will then add the property value in the normal way. Therefore, you need to extend the content model to include the expected aspect and property values. Follow likesRatingScheme for a sample. 25273: Fix failing unit test. 25274: DocLib sorting support for "popularity" (Likes rating scheme) 25277: ALF-6602: Updated instructions to reflect new design. Info shown now dynamically based on site ownership/access rights/browser feature support 25278: Merged BRANCHES/V3.4 to BRANCHES/V3.4-TEAM: 25267: ALF-1070 - if site/user is deleted then immediately clean associated site/user feed 25279: ALF-6601: Updated to handle 0 byte dropped files (or folders) 25280: ALF-7009 WIP checkpoint - Also removed some code that is obsolete since ConsoleTool class extends Alfresco.component.Base 25286: ALF-6599: Changed folder DND target back to image (not row) 25298: ALF-7009 - Upload and replace Share logo 25311: ALF-7080 / ALF-7002 - ActivitiesFeed subsystem (+ option to enable/disable job triggers) - moved ActivitiesFeed into subsystem for dynamic mgmt via JMX - FeedNotifier repeat interval can be dynamically changed - also added "enabled" property to AbstractTriggerBean => FeedNotifier job can be disabled / re-enabled 25312: ROLLBACK (Partial) 25249 ALF-7049 Disable Alfresco Explorer. 25313: ROLLBACK 25252 : ALF-7050 - RTEAM 09: Disable MT 25317: ALF-6930 - bootstrap / patch activities email template 25331: ALF-6890 - RTEAM 16: License-based restrictions: Number of users 25333: Merged BRANCHES/V3.4 to BRANCHES/V3.4-TEAM: 25319: Build/test fix (fallout from ALF-1070) 25322: Build/test fix (fallout from ALF-1070) 25356: ALF-6930 - bootstrap / patch activities email template - missed file, sorry ! 25358: ALF-6834 - Activities email: tweaks - pass through repeatInterval (to template model) - add exclude email list (eg. for default admin) 25360: RTEAM 28: License restriction reporting: Force refresh - Added API for selective updates of usage data based on an enumeration: USAGE_USERS, USAGE_DOCUMENTS or USAGE_ALL - When people are added, for instance, usage will be updated and then retrieved for checking. 25363: ALF-6639 - Default collaboration dashboard - NOTE: may need Team overlay version on merge if this change is *not* the default for Swift 25364: Modified usage/restriction admin webscripts to use a common FTL lib for the json output and added some missing quotes around the license mode value. 25365: ALF-6597: Added DashletTitleBarActions widget and applied to WebView dashlet 25367: ALF-7082 - Remove Network Dashlet - Removed network dashlet from codeline - Support in WebScripts to allow hook for override of exception handling from webscripts for specific use cases - Surf LocalWebScriptRuntime overrides error handling looking for specific case of SC_NOT_FOUND - and silent ignores missing webscript components - Improved Share handling of missing webscript components that have already been bound into a dashboard (i.e. not a missing "slot" but an existing component binding that points to a missing webscript URL) - this will also allow for easier removal of other existing dashlets in the future without requiring repo-side patches or similar. 25369: ALF-7042 (ALF-6832) RTEAM 28: License restriction reporting: Force refresh - Added unit test for update WebScript 25375: Implementation of ALF-7024. Document versions service/webscript needs to return avatar url. The avatar url has been added to the JSON response as requested. If there is no avatar, a JSON null is returned as requested. 25377: Add maxDocs, maxUsers and license mode to License JMX bean 25378: Adding additional property to version.get webscript: a correctly-formatted ISO8601 date. This has been added as requested in the comments of ALF-7024. 25381: Refactor of LicenseComponent and related to produce LicenseMode in descriptor (ALF-6907 RTEAM 19) - Need actual enum to do later usage updates according to mode 25382: ALF-7053 - RTEAM 07: Disable Transfer 25387: ALF-6564: Finished JMXFormProcessor (again). There is now a configurable list of operations to ignore, revert is ignored by default. The labels for the buttons are also localisable now. 25390: Fixed up License MBean after changing the descriptor API 25391: ALF-6911: RTEAM 02: Record and expose system attributes - Added job locking around individual usage queries - It is possible to concurrently update user and document counts - Exposed true/false return value on updates and added this to usages webscripts 25392: Switched version edition back to Community - If we distribute Team using a community build, then we should know about it. - Team functionality is triggered by an Enterprise build or TEAM license. 25393: Undid rev 25392 25394: Work on ALF-6832:TR25: License restriction reporting - ALF-6893: RTEAM 03: Expose restrictions using RepoAdminService - ALF-6911: RTEAM 02: Record and expose system attributes - Added RepoUsageMonitor - Self-starting schedule - Only checks for restrictions that are in place - Issues warnings and errors; puts system into read-only mode on violation 25403: ALF-6890 - Switch over of user query to database backed query. 25408: WIP checkpoint for Agenda view refactor: - Adds Alfresco.util.friendlyDate to supply "Yesterday/Today/Tomorrow" style date - refactors getEvents into a common function in calendar-view.js (calendar-view-month not touched because it gets elements from the DOM rather than API) - Uses DataTables to display agenda events (currently unformatted) 25410: Merged BRANCHES/DEV/V3.4-BUG-FIX to BRANCHES/V3.4-TEAM: 25409: Merged BRANCHES/V3.4 to BRANCHES/DEV/V3.4-BUG-FIX: 25407: Merged BRANCHES/V3.3 to BRANCHES/V3.4: 25401: Allow continuous.xml database drop/create on postgresql and mysql to support the database being on a different machine 25406: We no longer need a 2nd _test database for the unit tests, so remove the code that created/removed them during the continuous build 25411: Fix to allow application logo to be uploaded with same filename as a previous logo. 25416: Implementation of ALF-7100. Comments webscript doesn't return dates in iso8601. I added iso8601-formatted date fields jsut as for ALF-7024. 25418: Implementation of ALF-7173. Remove the RatingService restriction whereby a single user can only rate a single node with one RatingScheme. Change to the RatingService javadoc so that it doesn't mention the restriction. Additional method in RatingService: getRatingsByCurrentUser - to return multiple ratings. Reimpl's testOneUserRatesInTwoSchemes test case from a -ve to a +ve test case. Rewrote the REST test cases slightly to cover the case of a single user multiply rating a node. Various changes through the apply() case to support this. 25419: Change the Site Loading for ALF-6567 from a bootstrap to a patch. This means that if a site is loaded then deleted, it won't be re-loaded again. 25420: Fixed unit test for ALF-6911: RTEAM 02: Record and expose system attributes - Forgot to prompt user usage changes to check numbers against 25422: Addition of selfRatingAllowed field to ratingdefinitions.get webscript. As this is now a configurable property on a scheme, it should be reflected in the REST API. 25423: Node rows that transition to the deleted state (not archive) are given type sys:deleted 25425: Add an example extension context file to patch-load an exported Site, and allow the import path to use defaults to reduce the number of settings required 25426: ALF-6597: Added temporary tooltip code for DashletToolbarActions (and updated to WebView dashlet to use it) 25427: ALF-6789: Removed dynamic welcome dashlet from add dashlets menu 25428: ALF-6599: Removed encoding of spaces in displayed location name on DND upload to folder 25430: ALF-6890 - tweaks to query and person service for user limit stuff. 25432: ALF-6597: Updated DashletToolBarActions to ensure fade in on first mouseover 25433: Implemented ALF-6613, ALF-6614, ALF-6615, ALF-6616, ALF-6617, ALF-6618, ALF-6619, ALF-6620, ALF-6621, ALF-6622 - PART #1 - ALF-6613 TR22: Document Details page improvements - ALF-6614 Page redesign and refactor * Components are atomic and doesn't need global events to work. - ALF-6615 Document Actions panel - ALF-6616 Tags panel - NEW, replaces the old "info (tags+permissions)" component - ALF-6617 Share links panel - ALF-6618 Properties panel - ALF-6619 Permissions panel - NEW, replaces the old "info (tags+permissions)" component - ALF-6620 Workflows panel - ALF-6621 Version History panel - ALF-6622 Comment component redesign and refactor - alfresco-macros.lib.ftl * <#function uriTemplate id> Helper for getting a uriTemplate in freemarker * <#function userProfileLink> Helper fopr rendering a userProfile link in freemarker - alfresco-util.js - Rhino Javascript-helpers * function error(code, message, redirect) Helper for redriting and throwing a webscript error * function param(name, defaultValue) Helper for placing a webscript "param" in the model, lloks for param in the following order args, page.templateArgs, properties, defaultValue. If no value is found and no default value was given an error is thrown. Useful to avoid webscripts "crashing" when accessed from /service/ rather than from /page/ path, i.e. when refreshed/reloaded using ajax. * function getRepositoryUrl() * function getRootNode() * function getDocumentDetails(nodeRef, site) - alfresco.js * New in Alfresco.util.DataTable c.dataSource.doBeforeParseData - to modify response before rendering, i.e. if an array is the respons rather than an object c.dataTable.config.className - if another css class than alfresco-datatable shall be used c.paginator.history - set to false to avoid browser history management to kick in reloadDataTable() - to reload getData(record) - to get data related to a row in the table * sanitizeMarkup() - moved out code from request(), strips out + + + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.html.status.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.html.status.ftl new file mode 100644 index 0000000000..fadb033362 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.html.status.ftl @@ -0,0 +1,34 @@ + + + + + +<#if status.code == 200> + <#if (args.successCallback?exists)> + + +<#else> + <#if (args.failureCallback?exists)> + + + + + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.json.ftl new file mode 100644 index 0000000000..4ed174b4f4 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.json.ftl @@ -0,0 +1,19 @@ +{ + "data": + { +<#escape x as jsonUtils.encodeJSONString(x)> + "totalUsers": ${totalUsers}, + "addedUsers": ${addedUsers}, + "users": + [ + <#list users?keys as username> + { + "username": "${username}", + "uploadStatus": "${users[username]}" + } + <#if username_has_next>, + + ] + + } +} diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.properties b/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.properties new file mode 100644 index 0000000000..eaf0749b2e --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post.properties @@ -0,0 +1,11 @@ +# User CSV Upload Web Script I18N +person.err.userCSV.invalidForm=The form wasn't uploaded as multipart +person.err.userCSV.noFile=No file was uploaded +person.err.userCSV.corruptFile=The file was corrupted or truncated + +person.err.userCSV.general=There was a problem with creating the users +person.err.userCSV.generalArgs=There was a problem with creating the users: {0} +person.err.userCSV.blankColumn=Column {0} (column number {1}) must not be blank, but was on line {2} + +person.msg.userCSV.created=Created for {0} +person.msg.userCSV.existing=Already exists diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.delete.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.delete.json.ftl index adf1572e9e..3b584ad1d2 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.delete.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.delete.json.ftl @@ -1,13 +1,11 @@ -<#macro dateFormat date>${date?string("dd MMM yyyy HH:mm:ss 'GMT'Z '('zzz')'")} <#escape x as jsonUtils.encodeJSONString(x)> { "data": { "nodeRef": "${nodeRef}", - "ratingScheme": "${rating.scheme.name!""}", - "rating": ${rating.score?c}, - "appliedAt": "<@dateFormat rating.appliedAt />", - "appliedBy": "${rating.appliedBy!""}" + "averageRating": ${averageRating?c}, + "ratingsTotal": ${ratingsTotal?c}, + "ratingsCount": ${ratingsCount?c} } } \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.desc.xml index eaa82959ba..33b333d5a8 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.desc.xml @@ -9,9 +9,22 @@ The rating will be applied using the fully authenticated user who makes the POST call.
The body of the post should be in the form, e.g.
{
-    "rating": 5,
+    "rating": 4.5,
   "ratingScheme": "fiveStarRatingScheme"
}
+
+ The response will be of the form, e.g.
+ {
+    "data":
+    {
+       "ratedNodeUrl": "\/api\/node\/workspace\/SpacesStore\/eb25f870-2e58-487a-aba1-fa5f8ae04c52\/ratings",
+       "rating": 4.5,
+       "ratingScheme": "fiveStarRatingScheme",
+       "averageRating": 3,
+       "ratingsTotal": 55.5,
+       "ratingsCount": 9
+    }
+ }
]]> /api/node/{store_type}/{store_id}/{id}/ratings diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.json.ftl index 45eed03611..99a0f9b305 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/rating.post.json.ftl @@ -4,7 +4,10 @@ { "ratedNodeUrl": "${ratedNode!""}", "rating": ${rating?c}, - "ratingScheme": "${ratingScheme!""}" + "ratingScheme": "${ratingScheme!""}", + "averageRating": ${averageRating?c}, + "ratingsTotal": ${ratingsTotal?c}, + "ratingsCount": ${ratingsCount?c} } } \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.desc.xml index 4a773a46cc..0b5d4e805a 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.desc.xml @@ -1,7 +1,23 @@ GET rating scheme definitions + Gets the list of rating scheme definitions e.g.
+ {
+    "data": {
+       "ratingSchemes": [
+          {
+             "maxRating": 1,
+             "name": "likesRatingScheme",
+             "minRating": 1
+          },
+          {
+             "maxRating": 5,
+             "name": "fiveStarRatingScheme",
+             "minRating": 1
+          }
+       ]
+    }
+ }
]]>
/api/rating/schemedefinitions diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.json.ftl index f6744d0953..29ffdd6b7d 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratingdefinitions.get.json.ftl @@ -8,7 +8,8 @@ { "name": "${schemeDefs[key].name!""}", "minRating": ${schemeDefs[key].minRating?c}, - "maxRating": ${schemeDefs[key].maxRating?c} + "maxRating": ${schemeDefs[key].maxRating?c}, + "selfRatingAllowed": ${schemeDefs[key].selfRatingAllowed?string} }<#if key_has_next>, ] diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.desc.xml index e62fe03b1b..fe28f83412 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.desc.xml @@ -9,14 +9,14 @@ {
  "nodeRef": "workspace:\/\/SpacesStore\/d0b163fe-050d-43f5-88e4-db1794a3e5cd",
  "ratings":
-   [
+   {
+     "fiveStarRatingScheme":
    {
-       "ratingScheme": "fiveStarRatingScheme",
      "rating": 5,
      "appliedAt": "12 Jul 2010 16:38:05 GMT+0100 (BST)",
      "appliedBy": "UserOne"
    }
-   ],
+   },
  "nodeStatistics":
  {
    "likesRatingScheme":
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.json.ftl index f6ebe1d378..b3b9f17aca 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/rating/ratings.get.json.ftl @@ -1,20 +1,19 @@ -<#macro dateFormat date>${date?string("dd MMM yyyy HH:mm:ss 'GMT'Z '('zzz')'")} <#escape x as jsonUtils.encodeJSONString(x)> { "data": { "nodeRef": "${nodeRef}", "ratings": - [ + { <#list ratings as rating> + "${rating.scheme.name!""}": { - "ratingScheme": "${rating.scheme.name!""}", "rating": ${rating.score?c}, - "appliedAt": "<@dateFormat rating.appliedAt />", + "appliedAt": "${xmldate(rating.appliedAt)}", "appliedBy": "${rating.appliedBy!""}" }<#if rating_has_next>, - ], + }, "nodeStatistics": { <#list averageRatings?keys as schemeName> diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/replication/replication-service-status.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/replication/replication-service-status.get.desc.xml new file mode 100644 index 0000000000..1a322a01fc --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/replication/replication-service-status.get.desc.xml @@ -0,0 +1,10 @@ + + Gets status information on the Replication Service + + Returns high-level information on the Replication Service status. + + /api/replication-service-status + + admin + required + diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/replication/replication-service-status.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/replication/replication-service-status.get.json.ftl new file mode 100644 index 0000000000..e04863ab14 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/replication/replication-service-status.get.json.ftl @@ -0,0 +1,8 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "enabled": ${enabled?string} + } +} + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/site/membership/membership.put.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/site/membership/membership.put.desc.xml index c01ceffeee..c94365f9d0 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/site/membership/membership.put.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/site/membership/membership.put.desc.xml @@ -4,7 +4,7 @@ Update the membership role for a user or group.
- 'shortname' is the shortname of the Web site, 'authorityname' is the full authority name for the membership. + 'shortname' is the shortname of the web site
Required parameters,
@@ -23,7 +23,7 @@ ]]> - /api/sites/{shortname}/memberships/{authorityname} + /api/sites/{shortname}/memberships argument user required diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/site/site-export.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/site/site-export.get.desc.xml new file mode 100644 index 0000000000..5b1d04b885 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/site/site-export.get.desc.xml @@ -0,0 +1,15 @@ + + Export Web Site + + + /api/sites/{shortname}/export + argument + admin + required + internal + diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.desc.xml index 3f7cb37597..c2108d234c 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.desc.xml @@ -1,8 +1,16 @@ - Get thumbnail - Gets a named thumbnail for a content resource. + Thumbnails + + Get a named thumbnail for a content resource.
+ The two URL templates which support a 'filename' template-arg are made available in order to + support scenarios (e.g. with third party libraries) where the repo URL being called includes an explicit filename suffix.
+ Please note that Alfresco does not currently use the filename template-arg and that it will be ignored. + Therefore a GET to these URLs will return the same resource as to the equivalent URLs without it. +
/api/node/{store_type}/{store_id}/{id}/content{property}/thumbnails/{thumbnailname}?c={queueforcecreate?}&ph={placeholder?} /api/path/{store_type}/{store_id}/{id}/content{property}/thumbnails/{thumbnailname}?c={queueforcecreate?}&ph={placeholder?} + /api/node/{store_type}/{store_id}/{id}/content{property}/thumbnails/{thumbnailname}/{filename}?c={queueforcecreate?}&ph={placeholder?} + /api/path/{store_type}/{store_id}/{id}/content{property}/thumbnails/{thumbnailname}/{filename}?c={queueforcecreate?}&ph={placeholder?} argument user required diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.js b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.js index 4e7866fd5a..12249311c0 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.js +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnail.get.js @@ -11,6 +11,13 @@ function main() status.setCode(status.STATUS_NOT_FOUND, "The thumbnail source node could not be found"); return; } + + // 400 if the node is not a subtype of cm:content + if (!node.isSubType("cm:content")) + { + status.setCode(status.STATUS_BAD_REQUEST, "The thumbnail source node is not a subtype of cm:content"); + return; + } // Get the thumbnail name from the JSON content var thumbnailName = url.templateArgs.thumbnailname; @@ -69,12 +76,13 @@ function main() if (ph == true) { - // Try and get the place holder resource - var phPath = thumbnailService.getPlaceHolderResourcePath(thumbnailName); + // Try and get the place holder resource. We use a method in the thumbnail service + // that by default gives us a resource based on the content's mime type. + var phPath = thumbnailService.getMimeAwarePlaceHolderResourcePath(thumbnailName, node.mimetype); if (phPath == null) { // 404 since no thumbnail was found - status.setCode(status.STATUS_NOT_FOUND, "Thumbnail was not found and no place holde resource set for '" + thumbnailName + "'"); + status.setCode(status.STATUS_NOT_FOUND, "Thumbnail was not found and no place holder resource set for '" + thumbnailName + "'"); return; } else diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.get.js b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.get.js index 2310c1c59a..d9576b2c58 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.get.js +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.get.js @@ -9,7 +9,14 @@ function main() if (node == null) { status.setCode(status.STATUS_NOT_FOUND, "The thumbnail source node could not be found"); - return; + return; + } + + // 400 if the node is not a subtype of cm:content + if (!node.isSubType("cm:content")) + { + status.setCode(status.STATUS_BAD_REQUEST, "The thumbnail source node is not a subtype of cm:content"); + return; } // Get the thumbnails diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.post.json.js b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.post.json.js index f4bf739541..11fc1e22e8 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.post.json.js +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/thumbnail/thumbnails.post.json.js @@ -9,7 +9,14 @@ function main() if (node == null) { status.setCode(status.STATUS_NOT_FOUND, "The thumbnail source node could not be found"); - return; + return; + } + + // 400 if the node is not a subtype of cm:content + if (!node.isSubType("cm:content")) + { + status.setCode(status.STATUS_BAD_REQUEST, "The thumbnail source node is not a subtype of cm:content"); + return; } // Get the thumbnail name from the JSON content @@ -23,6 +30,7 @@ function main() if (thumbnailName == null) { status.setCode(status.STATUS_BAD_REQUEST, "Thumbnail name was not provided"); + return; } // TODO double check that the thumbnail name is valid diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.desc.xml index 23d4b2df30..0fbc767b27 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.desc.xml @@ -6,17 +6,16 @@
HTML form data
    -
  • filedata, (mandatory) HTML type file
  • -
  • siteid
  • -
  • containerid
  • -
  • uploaddirectory
  • -
  • updatenoderef
  • -
  • filename
  • -
  • description
  • -
  • contenttype
  • -
  • majorversion
  • -
  • overwrite
  • -
  • thumbnails
  • +
  • filedata, (mandatory) HTML type file
  • +
  • siteid
  • +
  • containerid
  • +
  • uploaddirectory
  • +
  • updatenoderef
  • +
  • description
  • +
  • contenttype
  • +
  • majorversion
  • +
  • overwrite
  • +
  • thumbnails

@@ -27,7 +26,6 @@
Return status: STATUS_OK (200) - ]]> /api/upload diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.js b/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.js index c5ab82ae55..01c8151628 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.js +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.js @@ -14,7 +14,6 @@ function main() // Upload specific var uploadDirectory = null, - title = "", contentType = null, aspects = [], overwrite = true; // If a filename clashes for a versionable file @@ -78,10 +77,6 @@ function main() updateNodeRef = fnFieldValue(field); break; - case "filename": - title = fnFieldValue(field); - break; - case "description": description = field.value; break; @@ -120,7 +115,7 @@ function main() /** * Site or Non-site? */ - if (siteId !== null) + if (siteId !== null && siteId.length() > 0) { /** * Site mode. @@ -210,20 +205,8 @@ function main() if (!updateNode.hasAspect("cm:workingcopy")) { - // Ensure the original file is versionable - may have been uploaded via different route - if (!updateNode.hasAspect("cm:versionable")) - { - // Ensure the file is versionable - var props = new Array(1); - props["cm:autoVersionOnUpdateProps"] = false; - updateNode.addAspect("cm:versionable", props); - } - - if (updateNode.versionHistory == null) - { - // Create the first version manually so we have 1.0 before checkout - updateNode.createVersion("", true); - } + // Ensure the file is versionable (autoVersion = true, autoVersionProps = false) + updateNode.ensureVersioningEnabled(true, false); // It's not a working copy, do a check out to get the actual working copy updateNode = updateNode.checkoutForUpload(); @@ -322,7 +305,7 @@ function main() // Reapply mimetype as upload may have been via Flash - which always sends binary mimetype newFile.properties.content.guessMimetype(filename); newFile.properties.content.guessEncoding(); - newFile.save(); + newFile.save(); // Create thumbnail? if (thumbnailNames != null) @@ -340,23 +323,6 @@ function main() } } - // Extract metadata - via repository action for now. - // This should use the MetadataExtracter API to fetch properties, allowing for possible failures. - var emAction = actions.create("extract-metadata"); - if (emAction != null) - { - // Call using readOnly = false, newTransaction = false - emAction.execute(newFile, false, false); - } - - // Set the title if none set during meta-data extract - newFile.reset(); - if (newFile.properties.title == null) - { - newFile.properties.title = title; - newFile.save(); - } - // Additional aspects? if (aspects.length > 0) { @@ -366,6 +332,15 @@ function main() } } + // Extract metadata - via repository action for now. + // This should use the MetadataExtracter API to fetch properties, allowing for possible failures. + var emAction = actions.create("extract-metadata"); + if (emAction != null) + { + // Call using readOnly = false, newTransaction = false + emAction.execute(newFile, false, false); + } + model.document = newFile; } } diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/version/version.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/version/version.get.json.ftl index 136b79b696..e72738549c 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/version/version.get.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/version/version.get.json.ftl @@ -1,19 +1,20 @@ <#escape x as jsonUtils.encodeJSONString(x)> [ <#list versions as v> - { - "nodeRef": "${v.nodeRef}", - "name": "${v.name}", - "label": "${v.label}", - "description": "${v.description}", - "createdDate": "${v.createdDate?string("dd MMM yyyy HH:mm:ss 'GMT'Z '('zzz')'")}", - "creator": - { - "userName": "${v.creator.userName}", - "firstName": "${v.creator.firstName!""}", - "lastName": "${v.creator.lastName!""}" - } - }<#if (v_has_next)>, + { + "nodeRef": "${v.nodeRef}", + "name": "${v.name}", + "label": "${v.label}", + "description": "${v.description}", + "createdDate": "${v.createdDate?string("dd MMM yyyy HH:mm:ss 'GMT'Z '('zzz')'")}", + "createdDateISO": "${xmldate(v.createdDate)}", + "creator": + { + "userName": "${v.creator.userName}", + "firstName": "${v.creator.firstName!""}", + "lastName": "${v.creator.lastName!""}" + } + }<#if (v_has_next)>, ] \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.desc.xml new file mode 100644 index 0000000000..c778c1b467 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.desc.xml @@ -0,0 +1,9 @@ + + Logo Upload + Upload logo file content + + admin + required + /slingshot/application/uploadlogo + internal + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.html.ftl b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.html.ftl new file mode 100644 index 0000000000..97ef1e75de --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.html.ftl @@ -0,0 +1,16 @@ + + + Upload success + + +<#if (args.successCallback?exists)> + + + + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.html.status.ftl b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.html.status.ftl new file mode 100644 index 0000000000..7eb790a20e --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.html.status.ftl @@ -0,0 +1,19 @@ + + + Upload Logo Failure + + +<#if (args.failureCallback?exists)> + + + + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.js new file mode 100644 index 0000000000..552601b565 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.js @@ -0,0 +1,69 @@ +/** + * Application Log Upload method + * + * @method POST + * @param filedata {file} + */ + +function main() +{ + try + { + var filename = null; + var content = null; + + // locate file attributes + for each (field in formdata.fields) + { + if (field.name == "filedata" && field.isFile) + { + filename = field.filename; + content = field.content; + break; + } + } + + // ensure all mandatory attributes have been located + if (filename == undefined || content == undefined) + { + status.code = 400; + status.message = "Uploaded file cannot be located in request"; + status.redirect = true; + return; + } + + var sitesNode = companyhome.childrenByXPath("st:sites")[0]; + if (sitesNode == null) + { + status.code = 500; + status.message = "Failed to locate Sites folder."; + status.redirect = true; + return; + } + + // create the new image node + logoNode = sitesNode.createNode(new Date().getTime() + "_" + filename, "cm:content"); + logoNode.properties.content.write(content); + logoNode.properties.content.guessMimetype(filename); + logoNode.save(); + + // save ref to be returned + model.logo = logoNode; + model.name = filename; + } + catch (e) + { + var x = e; + status.code = 500; + status.message = "Unexpected error occured during upload of new content."; + if (x.message && x.message.indexOf("org.alfresco.service.cmr.usage.ContentQuotaException") == 0) + { + status.code = 413; + status.message = x.message; + } + status.redirect = true; + return; + } +} + +main(); \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.json.ftl new file mode 100644 index 0000000000..97e04553c0 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/application/logo.post.json.ftl @@ -0,0 +1,12 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "nodeRef": "${logo.nodeRef}", + "fileName": "${name}", + "status": + { + "code": 200, + "name": "OK", + "description" : "File uploaded successfully" + } +} + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/eventList.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/eventList.get.desc.xml index 0837724d02..829273936e 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/eventList.get.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/eventList.get.desc.xml @@ -2,6 +2,7 @@ Event Listing List of all upcoming events for a site /calendar/eventList + /calendar/eventList-{site}.ics argument guest required diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.js index 8dcdeb3af6..847488951c 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.js @@ -174,6 +174,7 @@ function getUserEvents(user, siteId, range) title: e.properties["ia:whatEvent"], where: e.properties["ia:whereEvent"] == null ? "" : e.properties["ia:whereEvent"], when: e.properties["ia:fromDate"], + description: e.properties["ia:descriptionEvent"], start: eStart, end: eEnd, site: eSite, diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.json.ftl index cd4a31a1ce..183602603d 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/userevents.get.json.ftl @@ -10,6 +10,7 @@ "title": "${event.title}", "where": "${event.where}", "when": "${xmldate(event.when)}", + "description": "${event.description}", "url": "page/site/${event.site}/calendar?date=${event.when?string("yyyy-MM-dd")}", "start": "${event.start?string("HH:mm")}", "end": "${event.end?string("HH:mm")}", diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/datalists/filters.lib.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/datalists/filters.lib.js index 8f8b649465..45ba9ae43d 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/datalists/filters.lib.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/datalists/filters.lib.js @@ -55,7 +55,7 @@ var Filters = // Common types and aspects to filter from the UI var filterQueryDefaults = - " -TYPE:\"systemfolder\"" + + " -TYPE:\"cm:systemfolder\"" + " -@cm\\:lockType:READ_ONLY_LOCK"; switch (String(filter.filterId)) diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/cancel-checkout.post.json.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/cancel-checkout.post.json.js index 6330ff256e..60b53f62de 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/cancel-checkout.post.json.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/cancel-checkout.post.json.js @@ -41,8 +41,9 @@ function runAction(p_params) } catch(e) { - status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, e.toString()); - return; + e.code = status.STATUS_INTERNAL_SERVER_ERROR; + e.message = e.toString(); + throw e; } return results; diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkin.post.json.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkin.post.json.js index e914ba9504..57409a25b6 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkin.post.json.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkin.post.json.js @@ -41,8 +41,9 @@ function runAction(p_params) } catch(e) { - status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, e.toString()); - return; + e.code = status.STATUS_INTERNAL_SERVER_ERROR; + e.message = e.toString(); + throw e; } return results; diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkout.post.json.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkout.post.json.js index f0cb62a471..4fddba9ffd 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkout.post.json.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/checkout.post.json.js @@ -22,19 +22,8 @@ function runAction(p_params) { var assetNode = p_params.destNode; - // Ensure the file is versionable - if (!assetNode.hasAspect("cm:versionable")) - { - var props = new Array(1); - props["cm:autoVersionOnUpdateProps"] = false; - assetNode.addAspect("cm:versionable", props); - } - - if (assetNode.versionHistory == null) - { - // Create the first version manually so we have 1.0 before checkout - assetNode.createVersion("", true); - } + // Ensure the file is versionable (autoVersion = true, autoVersionProps = false) + assetNode.ensureVersioningEnabled(true, false); // Checkout the asset var workingCopy = assetNode.checkout(); @@ -65,8 +54,9 @@ function runAction(p_params) } catch(e) { - status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, e.toString()); - return; + e.code = status.STATUS_INTERNAL_SERVER_ERROR; + e.message = e.toString(); + throw e; } return results; diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/activity.post.json.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/activity.post.json.js index 7de38d25a0..f553af3536 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/activity.post.json.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/activity.post.json.js @@ -40,6 +40,8 @@ function postActivity() { case "file-added": case "file-updated": + case "file-liked": + case "folder-liked": case "google-docs-checkout": case "google-docs-checkin": case "inline-edit": diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/doclist.get.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/doclist.get.js index 4ee4f182d8..adb65ec038 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/doclist.get.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/doclist.get.js @@ -79,6 +79,7 @@ function getDoclist() } else { + // TODO: Sorting with folders at end -- swap order of concat() nodes = folderNodes.concat(documentNodes); } totalRecords = nodes.length; @@ -117,7 +118,8 @@ function getDoclist() if (item !== null) { item.isFavourite = (favourites[item.node.nodeRef] === true); - + item.likes = Common.getLikes(node); + // Does this collection of nodes have potentially differering paths? if (filterParams.variablePath || item.isLink) { @@ -145,7 +147,7 @@ function getDoclist() item.location = location; // Is our thumbnail type registered? - if (isThumbnailNameRegistered) + if (isThumbnailNameRegistered && item.node.isSubType("cm:content")) { // Make sure we have a thumbnail. thumbnail = item.node.getThumbnail(THUMBNAIL_NAME); @@ -187,6 +189,7 @@ function getDoclist() if (String(items[i].node.nodeRef) == workingCopyOriginal) { fnArrayRemove(items, i); + --totalRecords; break; } } diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/evaluator.lib.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/evaluator.lib.js index 88ea7dbb10..75046233ee 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/evaluator.lib.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/evaluator.lib.js @@ -1,3 +1,4 @@ + var Evaluator = { /** @@ -278,7 +279,7 @@ var Evaluator = } } catch (e) {} - + return( { node: node, @@ -295,7 +296,8 @@ var Evaluator = activeWorkflows: activeWorkflows, custom: jsonUtils.toJSONString(custom), customObj: custom, - actionLabels: actionLabels + actionLabels: actionLabels, + }); } else diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/filters.lib.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/filters.lib.js index 1e65886e00..f7370668a4 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/filters.lib.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/filters.lib.js @@ -55,6 +55,19 @@ var Filters = optional = optional || {}; + // Sorting parameters specified? + var sortAscending = args.sortAsc, + sortField = args.sortField; + + if (sortAscending == "false") + { + filterParams.sort[0].ascending = false; + } + if (sortField !== null) + { + filterParams.sort[0].column = (sortField.indexOf(":") != -1 ? "@" : "") + sortField; + } + // Max returned results specified? var argMax = args.max; if ((argMax !== null) && !isNaN(argMax)) @@ -67,15 +80,17 @@ var Filters = { favourites = []; } - + // Create query based on passed-in arguments var filterData = String(args.filterData), filterQuery = ""; // Common types and aspects to filter from the UI - known subtypes of cm:content and cm:folder var filterQueryDefaults = - " -TYPE:\"thumbnail\"" + - " -TYPE:\"systemfolder\"" + + " -TYPE:\"cm:thumbnail\"" + + " -TYPE:\"cm:failedThumbnail\"" + + " -TYPE:\"cm:systemfolder\"" + + " -TYPE:\"rating\"" + " -TYPE:\"fm:forums\"" + " -TYPE:\"fm:forum\"" + " -TYPE:\"fm:topic\"" + diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/item.lib.ftl b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/item.lib.ftl index 4485dbed8f..9c9112bcbd 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/item.lib.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/item.lib.ftl @@ -55,9 +55,16 @@ "webdavUrl": "${node.webdavUrl}", "actionSet": "${item.actionSet}", "tags": <#noescape>[${tags}], + <#if node.hasAspect("cm:generalclassifiable")> "categories": [<#list node.properties.categories![] as c>["${c.name}", "${c.displayPath?replace("/categories/General","")}"]<#if c_has_next>,], + <#if item.activeWorkflows??>"activeWorkflows": "<#list item.activeWorkflows as aw>${aw}<#if aw_has_next>,", <#if item.isFavourite??>"isFavourite": ${item.isFavourite?string}, + "likes":<#if item.likes??> + { + "isLiked": ${item.likes.isLiked?string}, + "totalLikes": ${item.likes.totalLikes?c} + }<#else>null, "location": { "repositoryId": "${(node.properties["trx:repositoryId"])!(server.id)}", @@ -72,7 +79,7 @@ <#if item.location.parent.nodeRef??> "nodeRef": "${item.location.parent.nodeRef!""}" - + } }, <#if node.hasAspect("cm:geographic")>"geolocation": diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/node.get.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/node.get.js index f341b76614..18aaf02a24 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/node.get.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/node.get.js @@ -35,6 +35,7 @@ function getDoclist() item = Evaluator.run(node); item.isFavourite = (favourites[node.nodeRef] === true); + item.likes = Common.getLikes(node); item.location = { @@ -57,22 +58,16 @@ function getDoclist() { item.location.file = ""; } - else if (node.isContainer) - { - // Strip off the extra path that will have been added by default - var paths = item.location.path.split("/"); - item.location.path = "/" + paths.slice(1, paths.length - 1).join("/"); - } // Is our thumbnail type registered? - if (isThumbnailNameRegistered) + if (isThumbnailNameRegistered && item.node.isSubType("cm:content")) { // Make sure we have a thumbnail. - thumbnail = node.getThumbnail(THUMBNAIL_NAME); + thumbnail = item.node.getThumbnail(THUMBNAIL_NAME); if (thumbnail === null) { // No thumbnail, so queue creation - node.createThumbnail(THUMBNAIL_NAME, true); + item.node.createThumbnail(THUMBNAIL_NAME, true); } } diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/parse-args.lib.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/parse-args.lib.js index 30afcc7ec6..209d0245fb 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/parse-args.lib.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/parse-args.lib.js @@ -1,7 +1,8 @@ const THUMBNAIL_NAME = "doclib", TYPE_SITES = "st:sites", PREF_DOCUMENT_FAVOURITES = "org.alfresco.share.documents.favourites", - PREF_FOLDER_FAVOURITES = "org.alfresco.share.folders.favourites"; + PREF_FOLDER_FAVOURITES = "org.alfresco.share.folders.favourites", + LIKES_SCHEME = "likesRatingScheme"; var Common = { @@ -201,29 +202,18 @@ var Common = if (libraryRoot) { - if (node.isContainer && String(node.nodeRef) != String(libraryRoot.nodeRef)) - { - // We want the path to include the parent folder name - displayPaths = displayPaths.concat([node.name]); - } - // Generate the path from the supplied library root location = { site: null, siteTitle: null, container: null, - path: "/" + displayPaths.slice(libraryRoot.displayPath.split("/").length + 1, displayPaths.length).join("/") + path: "/" + displayPaths.slice(libraryRoot.displayPath.split("/").length + 1, displayPaths.length).join("/"), + file: node.name }; } else if ((qnamePaths.length > 4) && (qnamePaths[2] == TYPE_SITES)) { - if (node.isContainer) - { - // We want the path to include the parent folder name - displayPaths = displayPaths.concat([node.name]); - } - var siteId = displayPaths[3], siteNode = Common.getSite(siteId), containerId = qnamePaths[4].substr(3); @@ -237,7 +227,8 @@ var Common = siteTitle: siteNode.title, container: containerId, containerNode: siteNode.getContainer(containerId), - path: "/" + displayPaths.slice(5, displayPaths.length).join("/") + path: "/" + displayPaths.slice(5, displayPaths.length).join("/"), + file: node.name }; } } @@ -249,11 +240,38 @@ var Common = site: null, siteTitle: null, container: null, - path: "/" + displayPaths.slice(2, displayPaths.length).join("/") + path: "/" + displayPaths.slice(2, displayPaths.length).join("/"), + file: node.name }; } return location; + }, + + /** + * Returns an object literal representing the current "likes" rating for a node + * + * @method getLikes + * @param node {ScriptNode} Node to query + * @return {object} Likes object literal. + */ + getLikes: function Common_getLikes(node) + { + var isLiked = false, + totalLikes = 0; + + try + { + isLiked = ratingService.getRating(node, LIKES_SCHEME) !== -1; + totalLikes = ratingService.getRatingsCount(node, LIKES_SCHEME); + } + catch (e) {} + + return ( + { + isLiked: isLiked, + totalLikes: totalLikes + }); } }; @@ -272,7 +290,8 @@ var ParseArgs = rootNode = null, pathNode = null, nodeRef = null, - path = ""; + path = "", + location = null; // Is this library rooted from a non-site nodeRef? if (libraryRoot !== null) @@ -280,6 +299,7 @@ var ParseArgs = libraryRoot = ParseArgs.resolveNode(libraryRoot); } + if (url.templateArgs.store_type !== null) { /** @@ -354,12 +374,23 @@ var ParseArgs = return null; } + // Parent location parameter adjustment + location = Common.getLocation(pathNode, libraryRoot); + if (path !== "") + { + location.path = ParseArgs.combinePaths(location.path, location.file); + } + if (args.filter !== "node" && !pathNode.isContainer) + { + location.file = ""; + } + var objRet = { rootNode: rootNode, pathNode: pathNode, libraryRoot: libraryRoot, - location: Common.getLocation(pathNode, libraryRoot), + location: location, path: path, nodeRef: nodeRef, type: type @@ -465,5 +496,29 @@ var ParseArgs = // Return the values array, or the error string if it was set return (error !== null ? error : values); + }, + + /** + * Append multiple parts of a path, ensuring duplicate path separators are removed. + * + * @method combinePaths + * @param path1 {string} First path + * @param path2 {string} Second path + * @param ... + * @param pathN {string} Nth path + * @return {string} A string containing the combined paths + */ + combinePaths: function ParseArgs_combinePaths() + { + var path = "", i, ii; + for (i = 0, ii = arguments.length; i < ii; i++) + { + if (arguments[i] !== null) + { + path += arguments[i] + (arguments[i] !== "/" ? "/" : ""); + } + } + + return path.replace(/\/{2,}/g, "/").replace(/(.)\/$/g, "$1"); } }; diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/edit-metadata/node-type.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/slingshot/edit-metadata/node-type.get.json.ftl index 0d7d96a36e..3c40abf083 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/edit-metadata/node-type.get.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/edit-metadata/node-type.get.json.ftl @@ -4,7 +4,8 @@ { "nodeRef": "${node.nodeRef}", "type": "${node.typeShort}", - "isContainer": ${node.isContainer?string} + "isContainer": ${node.isContainer?string}, + "fileName": "${node.name}" } diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/avatar.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/avatar.get.desc.xml new file mode 100644 index 0000000000..914ca97401 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/avatar.get.desc.xml @@ -0,0 +1,12 @@ + + Avatar + + Returns a user avatar image in the format specified by the thumbnailname, or the "avatar" preset if omitted. + + /slingshot/profile/avatar/{username}/thumbnail/{thumbnailname} + /slingshot/profile/avatar/{username} + argument + user + required + internal + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/avatar.get.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/avatar.get.js new file mode 100644 index 0000000000..04ed94a728 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/avatar.get.js @@ -0,0 +1,75 @@ +/** + * User Profile - User avatar GET method + * + * Returns a user avatar image in the format specified by the thumbnailname, or the "avatar" preset if omitted. + * + * @method GET + */ + +function getPlaceholder(thumbnailName) +{ + // Try and get the place holder resource for a png avatar. + var phPath = thumbnailService.getMimeAwarePlaceHolderResourcePath(thumbnailName, "images/png"); + if (phPath == null) + { + // 404 since no thumbnail was found + status.setCode(status.STATUS_NOT_FOUND, "Thumbnail was not found and no place holder resource set for '" + thumbnailName + "'"); + return; + } + + return phPath; +} + +function main() +{ + var userName = url.templateArgs.username, + thumbnailName = url.templateArgs.thumbnailname || "avatar", + person = people.getPerson(userName); + + if (person == null) + { + // Stream the placeholder image + model.contentPath = getPlaceholder(thumbnailName); + return; + } + + // Retrieve the avatar NodeRef for this person, if there is one. + var avatarAssoc = person.assocs["cm:avatar"]; + + if (avatarAssoc != null) + { + var avatarNode = avatarAssoc[0]; + if (avatarNode != null) + { + // Get the thumbnail + var thumbnail = avatarNode.getThumbnail(thumbnailName); + if (thumbnail == null || thumbnail.size == 0) + { + // Remove broken thumbnail + if (thumbnail != null) + { + thumbnail.remove(); + } + + // Force the creation of the thumbnail + thumbnail = avatarNode.createThumbnail(thumbnailName, false); + if (thumbnail != null) + { + model.contentNode = thumbnail; + return; + } + } + else + { + // Place the details of the thumbnail into the model, this will be used to stream the content to the client + model.contentNode = thumbnail; + return; + } + } + } + + // Stream the placeholder image + model.contentPath = getPlaceholder(thumbnailName); +} + +main(); \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/usercontents.get.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/usercontents.get.js index 3c38e3c188..36d5d7341d 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/usercontents.get.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/profile/usercontents.get.js @@ -15,13 +15,13 @@ function getContents(user, type, maxResults) var userType = (type == 'created') ? 'creator' : 'modifier'; var query = "+PATH:\"/app:company_home/st:sites/*//*\"" + - " +TYPE:\"content\"" + - " -TYPE:\"dl:dataListItem\"" + " +@cm\\:" + userType + ":\"" + user + "\"" + " +@cm\\:" + type + ":[" + fromQuery + "T00\\:00\\:00 TO " + toQuery + "T23\\:59\\:59]" + - " -TYPE:\"thumbnail\""; + " +TYPE:\"cm:content\"" + + " -TYPE:\"dl:dataListItem\"" + + " -TYPE:\"cm:thumbnail\""; - var nodes = search.luceneSearch(query, "cm:" + type, false, maxResults); + var nodes = search.luceneSearch(query, "@cm:" + type, false, maxResults); //reset processed results (in search.lib.js) processedCache = {} return processResults(nodes, maxResults); diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.lib.js b/config/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.lib.js index 36e12f8015..0d82c9ee26 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.lib.js +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.lib.js @@ -613,6 +613,40 @@ function processResults(nodes, maxResults) }); } +/** + * Helper to escape the localname part of a QName string so it is valid inside an fts-alfresco query. + * The language supports the SQL92 identifier standard. + * + * @param qname The QName string to escape + * @return escaped string + */ +function escapeQName(qname) +{ + var separator = qname.indexOf(':'), + result = qname.substring(0, separator + 1), + localname = qname.substring(separator + 1); + for (var i=0,c; i= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) + { + result += '\\'; + } + } + else + { + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || c == '#')) + { + result += '\\'; + } + } + result += c; + } + return result; +} + /** * Return Search results with the given search terms. * @@ -632,7 +666,7 @@ function getSearchResults(params) // Simple keyword search and tag specific search if (term !== null && term.length !== 0) { - ftsQuery = "(" + term + ") PATH:\"/cm:taggable/cm:" + search.ISO9075Encode(term) + "/member\" "; + ftsQuery = "(" + term + " PATH:\"/cm:taggable/cm:" + search.ISO9075Encode(term) + "/member\") "; } else if (tag !== null && tag.length !== 0) { @@ -699,12 +733,12 @@ function getSearchResults(params) from = (sepindex === 0 ? "MIN" : propValue.substr(0, sepindex)); to = (sepindex === propValue.length - 1 ? "MAX" : propValue.substr(sepindex + 1)); } - formQuery += (first ? '' : ' AND ') + propName + ':"' + from + '".."' + to + '"'; + formQuery += (first ? '' : ' AND ') + escapeQName(propName) + ':"' + from + '".."' + to + '"'; } } else { - formQuery += (first ? '' : ' AND ') + propName + ':"' + propValue + '"'; + formQuery += (first ? '' : ' AND ') + escapeQName(propName) + ':"' + propValue + '"'; } } else diff --git a/config/alfresco/templates/webscripts/org/alfresco/slingshot/wiki/pagelist.get.rss.ftl b/config/alfresco/templates/webscripts/org/alfresco/slingshot/wiki/pagelist.get.rss.ftl index 7d450c41b0..78b6188eb3 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/slingshot/wiki/pagelist.get.rss.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/slingshot/wiki/pagelist.get.rss.ftl @@ -3,8 +3,8 @@ Alfresco - Wiki Page Alfresco Wiki Page - Recent Changes - ${xmldate(date)} - ${xmldate(date)} + ${date?string("EEE, dd MMM yyyy HH:mm:ss zzz")} + ${date?string("EEE, dd MMM yyyy HH:mm:ss zzz")} Alfresco ${server.edition} v${server.version} Alfresco - Wiki Page Recent Changes @@ -13,9 +13,9 @@ <#list wiki.pages?sort_by(['modified'])?reverse as p> <#assign page = p.page> - ${page.properties.title!""?html} + ${(page.properties.title!"")?html} ${absurl(url.context)?replace("alfresco", "share/page/site/${siteId}/wiki-page?title=${page.name}")} - ${xmldate(page.properties.modified)} + ${page.properties.modified?string("EEE, dd MMM yyyy HH:mm:ss zzz")} ${page.id} diff --git a/config/alfresco/web-scripts-application-context.xml b/config/alfresco/web-scripts-application-context.xml index 6e681d2e78..491312ed3c 100644 --- a/config/alfresco/web-scripts-application-context.xml +++ b/config/alfresco/web-scripts-application-context.xml @@ -322,6 +322,9 @@ + + + @@ -599,10 +602,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -815,6 +849,52 @@ parent="abstractRatingWebScript"> + + + + + + + + + + + + + + + + + + + + + + + + + @@ -913,10 +993,46 @@ parent="abstractAuditWebScript"> + + + + + + + + + + + + + + + + + + + + + + + + + + . + */ +package org.alfresco.repo.cmis.rest; + +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.scripts.ScriptException; +import org.alfresco.service.cmr.model.FileExistsException; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.ScriptContent; +import org.springframework.extensions.webscripts.WebScriptException; + + +/** + * Extended WebScript for converting an exception to an appropriate CMIS + * status response code + * + * @author davidc + */ +public class CMISWebScript extends DeclarativeWebScript +{ + + @Override + protected void executeScript(ScriptContent location, Map model) + { + try + { + super.executeScript(location, model); + } + catch(ScriptException e) + { + Throwable root = e.getCause(); + if (root != null && root instanceof FileExistsException) + { + throw new WebScriptException(HttpServletResponse.SC_CONFLICT, e.getMessage(), e); + } + throw e; + } + } + +} diff --git a/source/java/org/alfresco/repo/cmis/rest/test/BaseCMISTest.java b/source/java/org/alfresco/repo/cmis/rest/test/BaseCMISTest.java index 27882c4cb7..6a770b1fc4 100644 --- a/source/java/org/alfresco/repo/cmis/rest/test/BaseCMISTest.java +++ b/source/java/org/alfresco/repo/cmis/rest/test/BaseCMISTest.java @@ -76,6 +76,7 @@ public abstract class BaseCMISTest extends BaseWebScriptTest // Create a dummy client. We won't / can't use it to make requests. cmisClient = new CMISClient(null, null, null, null); + cmisClient.setValidate(false); } protected CMISClient cmisClient; diff --git a/source/java/org/alfresco/repo/cmis/ws/AuthenticationInterceptor.java b/source/java/org/alfresco/repo/cmis/ws/AuthenticationInterceptor.java index d17cf0da0d..690cd77c55 100644 --- a/source/java/org/alfresco/repo/cmis/ws/AuthenticationInterceptor.java +++ b/source/java/org/alfresco/repo/cmis/ws/AuthenticationInterceptor.java @@ -21,6 +21,7 @@ package org.alfresco.repo.cmis.ws; import java.util.List; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.web.util.auth.Authorization; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.transaction.TransactionService; import org.apache.cxf.binding.soap.SoapMessage; @@ -59,7 +60,15 @@ public class AuthenticationInterceptor extends AbstractSoapInterceptor { try { - authenticationService.authenticate(principal.getName(), principal.getPassword().toCharArray()); + Authorization auth = new Authorization(principal.getName(), principal.getPassword()); + if (auth.isTicket()) + { + authenticationService.validate(auth.getTicket()); + } + else + { + authenticationService.authenticate(auth.getUserName(), auth.getPassword().toCharArray()); + } } catch (Throwable e) { diff --git a/source/java/org/alfresco/repo/cmis/ws/DMObjectServicePort.java b/source/java/org/alfresco/repo/cmis/ws/DMObjectServicePort.java index f3fea8ffa7..e0c53d6d3b 100644 --- a/source/java/org/alfresco/repo/cmis/ws/DMObjectServicePort.java +++ b/source/java/org/alfresco/repo/cmis/ws/DMObjectServicePort.java @@ -176,7 +176,7 @@ public class DMObjectServicePort extends DMAbstractServicePort implements Object } catch (FileExistsException e) { - throw ExceptionUtil.createCmisException("Document already exists", EnumServiceException.CONTENT_ALREADY_EXISTS); + throw ExceptionUtil.createCmisException("Document already exists", EnumServiceException.NAME_CONSTRAINT_VIOLATION); } catch (FileNotFoundException e) { @@ -222,18 +222,11 @@ public class DMObjectServicePort extends DMAbstractServicePort implements Object throw ExceptionUtil.createCmisException("Name property not found", EnumServiceException.INVALID_ARGUMENT); } - try - { - NodeRef newFolderNodeRef = fileFolderService.create(folderNodeRef, name, type.getTypeId().getQName()).getNodeRef(); - propertiesUtil.setProperties(newFolderNodeRef, properties, createPropertyFilter(createIgnoringFilter(new String[] { CMISDictionaryModel.PROP_NAME, - CMISDictionaryModel.PROP_OBJECT_TYPE_ID }))); - applyAclCarefully(newFolderNodeRef, addACEs, removeACEs, EnumACLPropagation.PROPAGATE, policies); - objectId.value = propertiesUtil.getProperty(newFolderNodeRef, CMISDictionaryModel.PROP_OBJECT_ID, null); - } - catch (FileExistsException e) - { - throw ExceptionUtil.createCmisException("Folder already exists", EnumServiceException.CONTENT_ALREADY_EXISTS); - } + NodeRef newFolderNodeRef = fileFolderService.create(folderNodeRef, name, type.getTypeId().getQName()).getNodeRef(); + propertiesUtil.setProperties(newFolderNodeRef, properties, createPropertyFilter(createIgnoringFilter(new String[] { CMISDictionaryModel.PROP_NAME, + CMISDictionaryModel.PROP_OBJECT_TYPE_ID }))); + applyAclCarefully(newFolderNodeRef, addACEs, removeACEs, EnumACLPropagation.PROPAGATE, policies); + objectId.value = propertiesUtil.getProperty(newFolderNodeRef, CMISDictionaryModel.PROP_OBJECT_ID, null); } catch (CMISServiceException e) { @@ -470,7 +463,7 @@ public class DMObjectServicePort extends DMAbstractServicePort implements Object NodeRef objectNodeRef = resolvePathInfo(path); if (null == objectNodeRef) { - throw ExceptionUtil.createCmisException("Path to Folder was not specified or Folder Path is invalid", EnumServiceException.INVALID_ARGUMENT); + throw ExceptionUtil.createCmisException("Path to Folder was not specified or Folder Path is invalid", EnumServiceException.OBJECT_NOT_FOUND); } PropertyFilter propertyFilter = createPropertyFilter(filter); CmisObjectType object = createCmisObject(objectNodeRef, propertyFilter, includeRelationships, diff --git a/source/java/org/alfresco/repo/cmis/ws/utils/ExceptionUtil.java b/source/java/org/alfresco/repo/cmis/ws/utils/ExceptionUtil.java index ba127ab866..f5119c8fa5 100644 --- a/source/java/org/alfresco/repo/cmis/ws/utils/ExceptionUtil.java +++ b/source/java/org/alfresco/repo/cmis/ws/utils/ExceptionUtil.java @@ -28,6 +28,7 @@ import org.alfresco.repo.cmis.ws.CmisException; import org.alfresco.repo.cmis.ws.CmisFaultType; import org.alfresco.repo.cmis.ws.EnumServiceException; import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.model.FileExistsException; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.InvalidNodeRefException; @@ -46,6 +47,7 @@ public abstract class ExceptionUtil CLASS_TO_ENUM_EXCEPTION_MAPPING.put(InvalidNodeRefException.class.getName(), EnumServiceException.INVALID_ARGUMENT); CLASS_TO_ENUM_EXCEPTION_MAPPING.put(ContentIOException.class.getName(), EnumServiceException.NOT_SUPPORTED); CLASS_TO_ENUM_EXCEPTION_MAPPING.put(CMISQueryException.class.getName(), EnumServiceException.INVALID_ARGUMENT); + CLASS_TO_ENUM_EXCEPTION_MAPPING.put(FileExistsException.class.getName(), EnumServiceException.NAME_CONSTRAINT_VIOLATION); } public static CmisException createCmisException(String message, EnumServiceException exceptionType) diff --git a/source/java/org/alfresco/repo/web/scripts/RepoStore.java b/source/java/org/alfresco/repo/web/scripts/RepoStore.java index d35e85eb60..173d6a7878 100644 --- a/source/java/org/alfresco/repo/web/scripts/RepoStore.java +++ b/source/java/org/alfresco/repo/web/scripts/RepoStore.java @@ -34,6 +34,7 @@ import java.util.regex.Pattern; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.repo.search.impl.lucene.LuceneQueryParser; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.tenant.TenantAdminService; import org.alfresco.repo.tenant.TenantDeployer; @@ -331,7 +332,7 @@ public class RepoStore extends AbstractStore implements TenantDeployer org.alfresco.service.cmr.repository.Path repoScriptPath = nodeService.getPath(scriptNodeRef); String id = script.getDescription().getId().substring(scriptPath.length() + (scriptPath.length() > 0 ? 1 : 0)); String query = "+PATH:\"" + repoScriptPath.toPrefixString(namespaceService) + - "//*\" +QNAME:" + id + "*"; + "//*\" +QNAME:" + lucenifyNamePattern(id) + "*"; ResultSet resultSet = searchService.query(repoStore, SearchService.LANGUAGE_LUCENE, query); try { @@ -380,7 +381,7 @@ public class RepoStore extends AbstractStore implements TenantDeployer .append(encPath.length() != 0 ? ('/' + encPath) : "") .append((includeSubPaths ? '/' : "")) .append("/*\" +QNAME:") - .append(documentPattern); + .append(lucenifyNamePattern(documentPattern)); return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { @@ -447,6 +448,52 @@ public class RepoStore extends AbstractStore implements TenantDeployer return result.toString(); } + /** + * ALF-7059: Because we can't quote QNAME patterns, and because characters like minus have special meaning, we have + * to pass the document 'pattern' though the Lucene escaper, preserving the wildcard parts. Also, because you can't + * search for whitespace in a QNAME, we have to replace whitespace with the ? wildcard + * + * @param pattern + * @return + */ + private static String lucenifyNamePattern(String pattern) + { + // Assume already escaped if the pattern includes a backslash + if (pattern.indexOf('\\') != -1) + { + return pattern; + } + StringBuilder result = new StringBuilder(pattern.length() * 2); + StringTokenizer tkn = new StringTokenizer(pattern, "\t\r\n *", true); + while (tkn.hasMoreTokens()) + { + String token = tkn.nextToken(); + if (token.length() == 1) + { + char c = token.charAt(0); + if (Character.isWhitespace(c)) + { + // We can't include whitespace in a QNAME expression so we will have to use a wildcard character and + // filter the results later + result.append('?'); + } + else if (c == '*') + { + result.append(c); + } + else + { + result.append(LuceneQueryParser.escape(token)); + } + } + else + { + result.append(LuceneQueryParser.escape(token)); + } + } + return result.toString(); + } + /* (non-Javadoc) * @see org.alfresco.web.scripts.Store#getDescriptionDocumentPaths() */ diff --git a/source/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java b/source/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java index e448299c6e..49e5d79240 100644 --- a/source/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java +++ b/source/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java @@ -22,8 +22,8 @@ import junit.framework.Test; import junit.framework.TestSuite; import org.alfresco.repo.web.scripts.action.RunningActionRestApiTest; -import org.alfresco.repo.web.scripts.activities.SiteActivitySystemTest; import org.alfresco.repo.web.scripts.activities.feed.control.FeedControlTest; +import org.alfresco.repo.web.scripts.admin.AdminWebScriptTest; import org.alfresco.repo.web.scripts.audit.AuditWebScriptTest; import org.alfresco.repo.web.scripts.blog.BlogServiceTest; import org.alfresco.repo.web.scripts.dictionary.DictionaryRestApiTest; @@ -45,7 +45,6 @@ import org.alfresco.repo.web.scripts.thumbnail.ThumbnailServiceTest; import org.alfresco.repo.web.scripts.transfer.TransferWebScriptTest; import org.alfresco.repo.web.scripts.wcm.WebProjectTest; import org.alfresco.repo.web.scripts.wcm.membership.WebProjectMembershipTest; -import org.alfresco.repo.web.scripts.wcm.sandbox.AssetTest; import org.alfresco.repo.web.scripts.wcm.sandbox.SandboxTest; import org.alfresco.repo.web.scripts.workflow.ActivitiWorkflowRestApiTest; import org.alfresco.repo.web.scripts.workflow.JBPMWorkflowRestApiTest; @@ -65,6 +64,7 @@ public class WebScriptTestSuite extends TestSuite TestWebScriptRepoServer.getTestServer(); // Add the tests + suite.addTestSuite( AdminWebScriptTest.class ); suite.addTestSuite( AuditWebScriptTest.class ); suite.addTestSuite( BlogServiceTest.class ); suite.addTestSuite( DictionaryRestApiTest.class ); diff --git a/source/java/org/alfresco/repo/web/scripts/activities/feed/SiteFeedRetrieverWebScript.java b/source/java/org/alfresco/repo/web/scripts/activities/feed/SiteFeedRetrieverWebScript.java index b6d8eaaf6f..616269c877 100644 --- a/source/java/org/alfresco/repo/web/scripts/activities/feed/SiteFeedRetrieverWebScript.java +++ b/source/java/org/alfresco/repo/web/scripts/activities/feed/SiteFeedRetrieverWebScript.java @@ -24,16 +24,17 @@ import java.util.List; import java.util.Map; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.activities.feed.FeedTaskProcessor; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.service.cmr.activities.ActivityService; import org.alfresco.util.JSONtoFmModel; -import org.springframework.extensions.webscripts.DeclarativeWebScript; -import org.springframework.extensions.webscripts.Status; -import org.springframework.extensions.webscripts.WebScriptRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.JSONException; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; /** * Java-backed WebScript to retrieve Activity Site Feed @@ -90,7 +91,7 @@ public class SiteFeedRetrieverWebScript extends DeclarativeWebScript List feedEntries = activityService.getSiteFeedEntries(siteId, format); - if (format.equals("json")) + if (format.equals(FeedTaskProcessor.FEED_FORMAT_JSON)) { model.put("feedEntries", feedEntries); model.put("siteId", siteId); diff --git a/source/java/org/alfresco/repo/web/scripts/activities/feed/UserFeedRetrieverWebScript.java b/source/java/org/alfresco/repo/web/scripts/activities/feed/UserFeedRetrieverWebScript.java index 53a42988cd..9f374ac600 100644 --- a/source/java/org/alfresco/repo/web/scripts/activities/feed/UserFeedRetrieverWebScript.java +++ b/source/java/org/alfresco/repo/web/scripts/activities/feed/UserFeedRetrieverWebScript.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.activities.feed.FeedTaskProcessor; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.service.cmr.activities.ActivityService; @@ -125,7 +126,7 @@ public class UserFeedRetrieverWebScript extends DeclarativeWebScript { List feedEntries = activityService.getUserFeedEntries(feedUserId, format, siteId, exclThisUser, exclOtherUsers); - if (format.equals("json")) + if (format.equals(FeedTaskProcessor.FEED_FORMAT_JSON)) { model.put("feedEntries", feedEntries); model.put("siteId", siteId); diff --git a/source/java/org/alfresco/repo/web/scripts/admin/AbstractAdminWebScript.java b/source/java/org/alfresco/repo/web/scripts/admin/AbstractAdminWebScript.java new file mode 100644 index 0000000000..4c64737673 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/admin/AbstractAdminWebScript.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2009-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.Collections; +import java.util.Map; + +import org.alfresco.service.cmr.admin.RepoAdminService; +import org.alfresco.service.cmr.admin.RepoUsage; +import org.alfresco.service.cmr.admin.RepoUsageStatus.RepoUsageLevel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.DeclarativeWebScript; + +/** + * Abstract implementation for scripts that access the {@link RepoAdminService}. + * + * @author Derek Hulley + * @since 3.4 + */ +public abstract class AbstractAdminWebScript extends DeclarativeWebScript +{ + public static final String JSON_KEY_LAST_UPDATE = "lastUpdate"; + public static final String JSON_KEY_USERS = "users"; + public static final String JSON_KEY_DOCUMENTS = "documents"; + public static final String JSON_KEY_LICENSE_MODE = "licenseMode"; + public static final String JSON_KEY_READ_ONLY = "readOnly"; + public static final String JSON_KEY_UPDATED = "updated"; + public static final String JSON_KEY_LICENSE_VALID_UNTIL = "licenseValidUntil"; + public static final String JSON_KEY_LEVEL = "level"; + public static final String JSON_KEY_WARNINGS = "warnings"; + public static final String JSON_KEY_ERRORS = "errors"; + + /** + * Logger that can be used by subclasses. + */ + protected final Log logger = LogFactory.getLog(this.getClass()); + + protected RepoAdminService repoAdminService; + + /** + * @param repoAdminService the service that provides the functionality + */ + public void setRepoAdminService(RepoAdminService repoAdminService) + { + this.repoAdminService = repoAdminService; + } + + /** + * Return an I18N'd message for the given key or the key itself if not present + * + * @param args arguments to replace the variables in the message + */ + protected String getI18NMessage(String key, Object ... args) + { + return I18NUtil.getMessage(key, args); + } + + /** + * Helper to assign JSON return variables based on the repository usage data. + */ + protected void putUsageInModel( + Map model, + RepoUsage repoUsage, + boolean updated) + { + model.put(JSON_KEY_LAST_UPDATE, repoUsage.getLastUpdate()); + model.put(JSON_KEY_USERS, repoUsage.getUsers()); + model.put(JSON_KEY_DOCUMENTS, repoUsage.getDocuments()); + model.put(JSON_KEY_LICENSE_MODE, repoUsage.getLicenseMode()); + model.put(JSON_KEY_READ_ONLY, repoUsage.isReadOnly()); + model.put(JSON_KEY_LICENSE_VALID_UNTIL, repoUsage.getLicenseExpiryDate()); + model.put(JSON_KEY_UPDATED, updated); + + model.put(JSON_KEY_LEVEL, RepoUsageLevel.OK.ordinal()); + model.put(JSON_KEY_WARNINGS, Collections.emptyList()); + model.put(JSON_KEY_ERRORS, Collections.emptyList()); + } +} diff --git a/source/java/org/alfresco/repo/web/scripts/admin/AdminWebScriptTest.java b/source/java/org/alfresco/repo/web/scripts/admin/AdminWebScriptTest.java new file mode 100644 index 0000000000..4e52fe6271 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/admin/AdminWebScriptTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.Date; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.admin.RepoAdminService; +import org.alfresco.service.cmr.admin.RepoUsage; +import org.alfresco.service.cmr.admin.RepoUsage.UsageType; +import org.alfresco.service.cmr.admin.RepoUsageStatus; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.license.LicenseDescriptor; +import org.json.JSONObject; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.TestWebScriptServer; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * Test the admin web scripts + * + * @author Derek Hulley + * @since 3.4 + */ +public class AdminWebScriptTest extends BaseWebScriptTest +{ + private ApplicationContext ctx; + private RepoAdminService repoAdminService; + private DescriptorService descriptorService; + private String admin; + private String guest; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + ctx = getServer().getApplicationContext(); + repoAdminService = (RepoAdminService) ctx.getBean("RepoAdminService"); + descriptorService = (DescriptorService) ctx.getBean("DescriptorService"); + admin = AuthenticationUtil.getAdminUserName(); + guest = AuthenticationUtil.getGuestUserName(); + + AuthenticationUtil.setFullyAuthenticatedUser(admin); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + } + + public void testGetRestrictions() throws Exception + { + RepoUsage restrictions = repoAdminService.getRestrictions(); + + String url = "/api/admin/restrictions"; + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); + + Response response = sendRequest(req, Status.STATUS_OK, guest); + JSONObject json = new JSONObject(response.getContentAsString()); + Long maxUsers = json.isNull(AbstractAdminWebScript.JSON_KEY_USERS) ? null : json.getLong(AbstractAdminWebScript.JSON_KEY_USERS); + assertEquals("Mismatched max users", restrictions.getUsers(), maxUsers); + Long maxDocuments = json.isNull(AbstractAdminWebScript.JSON_KEY_DOCUMENTS) ? null : json.getLong(AbstractAdminWebScript.JSON_KEY_DOCUMENTS); + assertEquals("Mismatched max documents", restrictions.getDocuments(), maxDocuments); + } + + public void testGetUsage() throws Exception + { + RepoUsageStatus usageStatus = repoAdminService.getUsageStatus(); + RepoUsage usage = usageStatus.getUsage(); + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + Date validUntil = (licenseDescriptor == null) ? null : licenseDescriptor.getValidUntil(); // might be null + Integer checkLevel = new Integer(usageStatus.getLevel().ordinal()); + + String url = "/api/admin/usage"; + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); + + Response response = sendRequest(req, Status.STATUS_OK, guest); + System.out.println(response.getContentAsString()); + JSONObject json = new JSONObject(response.getContentAsString()); + Long users = json.isNull(AbstractAdminWebScript.JSON_KEY_USERS) ? null : json.getLong(AbstractAdminWebScript.JSON_KEY_USERS); + assertEquals("Mismatched users", usage.getUsers(), users); + Long documents = json.isNull(AbstractAdminWebScript.JSON_KEY_DOCUMENTS) ? null : json.getLong(AbstractAdminWebScript.JSON_KEY_DOCUMENTS); + assertEquals("Mismatched documents", usage.getDocuments(), documents); + String licenseMode = json.isNull(AbstractAdminWebScript.JSON_KEY_LICENSE_MODE) ? null : json.getString(AbstractAdminWebScript.JSON_KEY_LICENSE_MODE); + assertEquals("Mismatched licenseMode", usage.getLicenseMode().toString(), licenseMode); + boolean readOnly = json.getBoolean(AbstractAdminWebScript.JSON_KEY_READ_ONLY); + assertEquals("Mismatched readOnly", usage.isReadOnly(), readOnly); + boolean updated = json.getBoolean(AbstractAdminWebScript.JSON_KEY_UPDATED); + assertEquals("Mismatched updated", false, updated); + Long licenseValidUntil = json.isNull(AbstractAdminWebScript.JSON_KEY_LICENSE_VALID_UNTIL) ? null : json.getLong(AbstractAdminWebScript.JSON_KEY_LICENSE_VALID_UNTIL); + assertEquals("Mismatched licenseValidUntil", + (validUntil == null) ? null : validUntil.getTime(), + licenseValidUntil); + Integer level = json.isNull(AbstractAdminWebScript.JSON_KEY_LEVEL) ? null : json.getInt(AbstractAdminWebScript.JSON_KEY_LEVEL); + assertEquals("Mismatched level", checkLevel, level); + json.getJSONArray(AbstractAdminWebScript.JSON_KEY_WARNINGS); + json.getJSONArray(AbstractAdminWebScript.JSON_KEY_ERRORS); + } + + public void testUpdateUsageWithoutPermissions() throws Exception + { + String url = "/api/admin/usage"; + TestWebScriptServer.PostRequest req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); + sendRequest(req, 401, AuthenticationUtil.getGuestRoleName()); + } + + public void testUpdateUsage() throws Exception + { + repoAdminService.updateUsage(UsageType.USAGE_ALL); + RepoUsage usage = repoAdminService.getUsage(); + + String url = "/api/admin/usage"; + TestWebScriptServer.PostRequest req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); + + Response response = sendRequest(req, Status.STATUS_OK, admin); + System.out.println(response.getContentAsString()); + JSONObject json = new JSONObject(response.getContentAsString()); + Long users = json.isNull(AbstractAdminWebScript.JSON_KEY_USERS) ? null : json.getLong(AbstractAdminWebScript.JSON_KEY_USERS); + assertEquals("Mismatched users", usage.getUsers(), users); + Long documents = json.isNull(AbstractAdminWebScript.JSON_KEY_DOCUMENTS) ? null : json.getLong(AbstractAdminWebScript.JSON_KEY_DOCUMENTS); + assertEquals("Mismatched documents", usage.getDocuments(), documents); + String licenseMode = json.isNull(AbstractAdminWebScript.JSON_KEY_LICENSE_MODE) ? null : json.getString(AbstractAdminWebScript.JSON_KEY_LICENSE_MODE); + assertEquals("Mismatched licenseMode", usage.getLicenseMode().toString(), licenseMode); + boolean readOnly = json.getBoolean(AbstractAdminWebScript.JSON_KEY_READ_ONLY); + assertEquals("Mismatched readOnly", usage.isReadOnly(), readOnly); + boolean updated = json.getBoolean(AbstractAdminWebScript.JSON_KEY_UPDATED); + assertEquals("Mismatched updated", true, updated); + } +} diff --git a/source/java/org/alfresco/repo/web/scripts/admin/RepoRestrictionsGet.java b/source/java/org/alfresco/repo/web/scripts/admin/RepoRestrictionsGet.java new file mode 100644 index 0000000000..538932db6f --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/admin/RepoRestrictionsGet.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.admin.RepoUsage; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * GET the repository {@link RepoUsage restrictions}. + * + * @author Derek Hulley + * @since 3.4 + */ +public class RepoRestrictionsGet extends AbstractAdminWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + RepoUsage restrictions = repoAdminService.getRestrictions(); + putUsageInModel(model, restrictions, false); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/admin/RepoUsageGet.java b/source/java/org/alfresco/repo/web/scripts/admin/RepoUsageGet.java new file mode 100644 index 0000000000..7847b6a7c7 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/admin/RepoUsageGet.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.admin.RepoUsage; +import org.alfresco.service.cmr.admin.RepoUsageStatus; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * GET the repository {@link RepoUsage usage}. + * + * @author Derek Hulley + * @since 3.4 + */ +public class RepoUsageGet extends AbstractAdminWebScript +{ + @Override + protected Map executeImpl(final WebScriptRequest req, final Status status, final Cache cache) + { + // Runas system to obtain the info + RunAsWork> runAs = new RunAsWork>() + { + @Override + public Map doWork() throws Exception + { + Map model = new HashMap(7); + + RepoUsageStatus usageStatus = repoAdminService.getUsageStatus(); + RepoUsage usage = usageStatus.getUsage(); + + putUsageInModel( + model, + usage, + false); + + // Add usage messages + model.put(JSON_KEY_LEVEL, usageStatus.getLevel().ordinal()); + model.put(JSON_KEY_WARNINGS, usageStatus.getWarnings()); + model.put(JSON_KEY_ERRORS, usageStatus.getErrors()); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } + }; + return AuthenticationUtil.runAs(runAs, AuthenticationUtil.getSystemUserName()); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/admin/RepoUsagePost.java b/source/java/org/alfresco/repo/web/scripts/admin/RepoUsagePost.java new file mode 100644 index 0000000000..16a8c01d33 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/admin/RepoUsagePost.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.admin.RepoUsage; +import org.alfresco.service.cmr.admin.RepoUsage.UsageType; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * GET the repository {@link RepoUsage usage}. + * + * @author Derek Hulley + * @since 3.4 + */ +public class RepoUsagePost extends AbstractAdminWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + boolean updated = repoAdminService.updateUsage(UsageType.USAGE_ALL); + RepoUsage repoUsage = repoAdminService.getUsage(); + + putUsageInModel(model, repoUsage, updated); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/archive/AbstractArchivedNodeWebScript.java b/source/java/org/alfresco/repo/web/scripts/archive/AbstractArchivedNodeWebScript.java new file mode 100644 index 0000000000..9258ee4780 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/AbstractArchivedNodeWebScript.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.archive.NodeArchiveService; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is an abstract base class for the various webscript controllers in the + * NodeArchiveService. + * + * @author Neil McErlean + * @since 3.5 + */ +public abstract class AbstractArchivedNodeWebScript extends DeclarativeWebScript +{ + public static final String NAME = "name"; + public static final String TITLE = "title"; + public static final String DESCRIPTION = "description"; + public static final String NODEREF = "nodeRef"; + public static final String ARCHIVED_BY = "archivedBy"; + public static final String ARCHIVED_DATE = "archivedDate"; + public static final String DISPLAY_PATH = "displayPath"; + public static final String USER_NAME = "userName"; + public static final String FIRST_NAME = "firstName"; + public static final String LAST_NAME = "lastName"; + public static final String NODE_TYPE = "nodeType"; + public static final String DELETED_NODES = "deletedNodes"; + + // Injected services + protected ServiceRegistry serviceRegistry; + protected NodeArchiveService nodeArchiveService; + + /** + * Sets the serviceRegistry instance + * + * @param serviceRegistry the serviceRegistry to set + */ + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + /** + * Sets the nodeArchiveService instance + * + * @param nodeArchiveService the nodeArchiveService to set + */ + public void setNodeArchiveService(NodeArchiveService nodeArchiveService) + { + this.nodeArchiveService = nodeArchiveService; + } + + protected StoreRef parseRequestForStoreRef(WebScriptRequest req) + { + // get the parameters that represent the StoreRef, we know they are present + // otherwise this webscript would not have matched + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + String storeId = templateVars.get("store_id"); + + // create the StoreRef and ensure it is valid + StoreRef storeRef = new StoreRef(storeType, storeId); + + return storeRef; + } + + protected NodeRef parseRequestForNodeRef(WebScriptRequest req) + { + // get the parameters that represent the NodeRef. They may not all be there + // for all deletednodes webscripts. + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + String storeId = templateVars.get("store_id"); + String id = templateVars.get("id"); + + if (id == null || id.trim().length() == 0) + { + return null; + } + else + { + return new NodeRef(storeType, storeId, id); + } + } + + /** + * Retrieves the named parameter as an integer, if the parameter is not present the default value is returned + * + * @param req The WebScript request + * @param paramName The name of parameter to look for + * @param defaultValue The default value that should be returned if parameter is not present in request or if it is not positive + * @return The request parameter or default value + */ + protected int getIntParameter(WebScriptRequest req, String paramName, int defaultValue) + { + String paramString = req.getParameter(paramName); + + if (paramString != null) + { + try + { + int param = Integer.valueOf(paramString); + + if (param >= 0) + { + return param; + } + } + catch (NumberFormatException e) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); + } + } + + return defaultValue; + } + + /** + * This method gets all nodes from the archive which were originally contained + * within the specified StoreRef. + */ + protected SortedSet getArchivedNodesFrom(StoreRef storeRef) + { + NodeService nodeService = serviceRegistry.getNodeService(); + NodeRef archiveRootNode = nodeService.getStoreArchiveNode(storeRef); + + List children = nodeService.getChildAssocs(archiveRootNode); + + // We must get the sys:archived children in order of their archiving. + Comparator archivedNodeSorter = new ArchivedDateComparator(); + SortedSet orderedChildren = new TreeSet(archivedNodeSorter); + for (ChildAssociationRef chAssRef : children) + { + if (nodeService.hasAspect(chAssRef.getChildRef(), ContentModel.ASPECT_ARCHIVED)) + { + orderedChildren.add(chAssRef); + } + } + return orderedChildren; + } + + /** + * This class is used to sort ChildAssociationRefs into the correct order based on archivedDate. + * + * @author Neil Mc Erlean. + */ + protected class ArchivedDateComparator implements Comparator + { + + @Override + public int compare(ChildAssociationRef chAssRef1, ChildAssociationRef chAssRef2) + { + NodeService nodeService = serviceRegistry.getNodeService(); + Date archivedDate1 = (Date)nodeService.getProperty(chAssRef1.getChildRef(), ContentModel.PROP_ARCHIVED_DATE); + Date archivedDate2 = (Date)nodeService.getProperty(chAssRef2.getChildRef(), ContentModel.PROP_ARCHIVED_DATE); + + // We want the dates to be in reverse order i.e. most recent first. Hence the reversal of 2 and 1 below. + return archivedDate2.compareTo(archivedDate1); + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/archive/AllNodeArchiveTests.java b/source/java/org/alfresco/repo/web/scripts/archive/AllNodeArchiveTests.java new file mode 100644 index 0000000000..262a39cb5e --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/AllNodeArchiveTests.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * This class is a holder for the various test classes associated with the Node Archive Service. + * It is not (at the time of writing) intended to be incorporated into the automatic build + * which will find the various test classes and run them individually. + * + * @author Neil McErlean + * @since 3.5 + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + NodeArchiveServiceRestApiTest.class +}) +public class AllNodeArchiveTests +{ + // Intentionally empty +} diff --git a/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodePut.java b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodePut.java new file mode 100644 index 0000000000..118e9aaa6a --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodePut.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.node.archive.RestoreNodeReport; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the archivednode.put webscript. + * + * @author Neil Mc Erlean + * @since 3.5 + */ +public class ArchivedNodePut extends AbstractArchivedNodeWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + NodeRef nodeRefToBeRestored = parseRequestForNodeRef(req); + if (nodeRefToBeRestored == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "nodeRef not recognised. Could not restore."); + } + + RestoreNodeReport report = nodeArchiveService.restoreArchivedNode(nodeRefToBeRestored); + + // Handling of some error scenarios + if (report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_INVALID_ARCHIVE_NODE)) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find archive node: " + nodeRefToBeRestored); + } + else if (report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_PERMISSION)) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Unable to restore archive node: " + nodeRefToBeRestored); + } + else if (report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_INVALID_PARENT) || + report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_INTEGRITY) || + report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_OTHER)) + { + throw new WebScriptException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to restore archive node: " + nodeRefToBeRestored); + } + + model.put("restoreNodeReport", report); + return model; + } +} diff --git a/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodeState.java b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodeState.java new file mode 100644 index 0000000000..5b7555bc01 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodeState.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.PathUtil; + +/** + * A simple POJO class for the state of an archived node. For easier passing to the FTL model. + * + * @author Neil McErlean + * @since 3.5 + */ +public class ArchivedNodeState +{ + private NodeRef archivedNodeRef; + private String archivedBy; + private Date archivedDate; + private String name; + private String title; + private String description; + private String displayPath; + private String firstName; + private String lastName; + private String nodeType; + + /** + * To prevent unauthorised construction. + */ + private ArchivedNodeState() { /* Intentionally empty*/ } + + public static ArchivedNodeState create(NodeRef archivedNode, ServiceRegistry serviceRegistry) + { + ArchivedNodeState result = new ArchivedNodeState(); + + NodeService nodeService = serviceRegistry.getNodeService(); + Map properties = nodeService.getProperties(archivedNode); + + result.archivedNodeRef = archivedNode; + result.archivedBy = (String) properties.get(ContentModel.PROP_ARCHIVED_BY); + result.archivedDate = (Date) properties.get(ContentModel.PROP_ARCHIVED_DATE); + result.name = (String) properties.get(ContentModel.PROP_NAME); + result.title = (String) properties.get(ContentModel.PROP_TITLE); + result.description = (String) properties.get(ContentModel.PROP_DESCRIPTION); + result.nodeType = nodeService.getType(archivedNode).toPrefixString(serviceRegistry.getNamespaceService()); + + PersonService personService = serviceRegistry.getPersonService(); + if (result.archivedBy != null && personService.personExists(result.archivedBy)) + { + NodeRef personNodeRef = personService.getPerson(result.archivedBy, false); + Map personProps = nodeService.getProperties(personNodeRef); + + result.firstName = (String) personProps.get(ContentModel.PROP_FIRSTNAME); + result.lastName = (String) personProps.get(ContentModel.PROP_LASTNAME); + } + + ChildAssociationRef originalParentAssoc = (ChildAssociationRef) properties.get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + + if (serviceRegistry.getPermissionService().hasPermission(originalParentAssoc.getParentRef(), PermissionService.READ).equals(AccessStatus.ALLOWED) + && nodeService.exists(originalParentAssoc.getParentRef())) + { + result.displayPath = PathUtil.getDisplayPath(nodeService.getPath(originalParentAssoc.getParentRef()), true); + } + else + { + result.displayPath = ""; + } + + return result; + } + + public NodeRef getNodeRef() + { + return this.archivedNodeRef; + } + + public String getArchivedBy() + { + return this.archivedBy; + } + + public Date getArchivedDate() + { + return this.archivedDate; + } + + public String getName() + { + return this.name; + } + + public String getTitle() + { + return this.title; + } + + public String getDescription() + { + return this.description; + } + + public String getDisplayPath() + { + return this.displayPath; + } + + public String getFirstName() + { + return this.firstName; + } + + public String getLastName() + { + return this.lastName; + } + + public String getNodeType() + { + return this.nodeType; + } +} diff --git a/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesDelete.java b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesDelete.java new file mode 100644 index 0000000000..0c49c17162 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesDelete.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the deletednodes.delete web script. + * + * @author Neil McErlean + * @since 3.5 + */ +public class ArchivedNodesDelete extends AbstractArchivedNodeWebScript +{ + private static Log log = LogFactory.getLog(ArchivedNodesDelete.class); + + public static final String PURGED_NODES = "purgedNodes"; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + StoreRef storeRef = parseRequestForStoreRef(req); + NodeRef nodeRef = parseRequestForNodeRef(req); + + List nodesToBePurged = new ArrayList(); + if (nodeRef != null) + { + // If there is a specific NodeRef, then that is the only Node that should be purged. + // In this case, the NodeRef points to the actual node to be purged i.e. the node in + // the archive store. + nodesToBePurged.add(nodeRef); + } + else + { + // But if there is no specific NodeRef and instead there is only a StoreRef, then + // all nodes which were originally in that StoreRef should be purged. + SortedSet archiveNodes = getArchivedNodesFrom(storeRef); + for (ChildAssociationRef chAssRef : archiveNodes) + { + nodesToBePurged.add(chAssRef.getChildRef()); + } + } + + if (log.isDebugEnabled()) + { + log.debug("Purging " + nodesToBePurged.size() + " nodes"); + } + + // Now having identified the nodes to be purged, we simply have to do it. + nodeArchiveService.purgeArchivedNodes(nodesToBePurged); + + model.put(PURGED_NODES, nodesToBePurged); + + return model; + } +} diff --git a/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesFilter.java b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesFilter.java new file mode 100644 index 0000000000..840eb55b72 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesFilter.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * This interface defines a filter for ArchivedNodes. + * + * @author Neil Mc Erlean + * @since 3.5 + */ +public interface ArchivedNodesFilter +{ + /** + * This method checks whether or not the specified {@link NodeRef} should be included, + * as defined by the concrete filter implementation. + * @param nodeRef the NodeRef to be checked for filtering. + * @return true if the {@link NodeRef} is acceptable, else false. + */ + boolean accept(NodeRef nodeRef); + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesGet.java b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesGet.java new file mode 100644 index 0000000000..31079cd7aa --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesGet.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the deletednodes.get web script. + * + * @author Neil McErlean + * @since 3.5 + */ +public class ArchivedNodesGet extends AbstractArchivedNodeWebScript +{ + private static final String MAX_ITEMS = "maxItems"; + private static final String SKIP_COUNT = "skipCount"; + + private static final String PAGING_TOTAL_ITEMS = "totalItems"; + + + List nodeFilters = new ArrayList(); + + /** + * This method is used to inject {@link ArchivedNodeFilter node filters} on this GET call. + * @param nodeFilters + */ + public void setArchivedNodeFilters(List nodeFilters) + { + this.nodeFilters = nodeFilters; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // We want to get all nodes in the archive which were originally contained in the + // following StoreRef. + StoreRef storeRef = parseRequestForStoreRef(req); + + + SortedSet orderedChildren = getArchivedNodesFrom(storeRef); + + List deletedNodes = new ArrayList(orderedChildren.size()); + + for (ChildAssociationRef chAssRef : orderedChildren) + { + NodeRef nextArchivedNode = chAssRef.getChildRef(); + boolean nodeIsFilteredOut = false; + for (ArchivedNodesFilter filter : nodeFilters) + { + if (filter.accept(nextArchivedNode) == false) + { + nodeIsFilteredOut = true; + break; // Break from the loop over filters. + } + } + if (!nodeIsFilteredOut) + { + ArchivedNodeState state = ArchivedNodeState.create(nextArchivedNode, serviceRegistry); + deletedNodes.add(state); + } + } + + // Paging parameters. + int maxItems = getIntParameter(req, MAX_ITEMS, deletedNodes.size()); + int skipCount = getIntParameter(req, SKIP_COUNT, 0); + + // Paging was required + Map pagingModel = new HashMap(); + pagingModel.put(PAGING_TOTAL_ITEMS, deletedNodes.size()); + pagingModel.put(MAX_ITEMS, maxItems); + pagingModel.put(SKIP_COUNT, skipCount); + + int endIndex = skipCount + maxItems; + if (endIndex > deletedNodes.size()) + { + endIndex = deletedNodes.size(); + } + // from skipCount (inclusive) to endIndex (exclusive) + model.put(DELETED_NODES, deletedNodes.subList(skipCount, endIndex)); + model.put("paging", pagingModel); + + return model; + } +} + diff --git a/source/java/org/alfresco/repo/web/scripts/archive/NodeArchiveServiceRestApiTest.java b/source/java/org/alfresco/repo/web/scripts/archive/NodeArchiveServiceRestApiTest.java new file mode 100644 index 0000000000..f647aeed56 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/NodeArchiveServiceRestApiTest.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.text.DateFormat; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.node.archive.NodeArchiveService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONStringer; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * This class tests the ReST API of the {@link NodeArchiveService}. + * + * @author Neil McErlean + * @since 3.5 + */ +public class NodeArchiveServiceRestApiTest extends BaseWebScriptTest +{ + // Miscellaneous constants used throughout this test class. + private static final String DATA = "data"; + private static final String ARCHIVE_URL_FORMAT = "/api/archive/{0}/{1}"; + + private static final String TEST_TITLE = "FooBarTitle"; + private static final String TEST_DESCRIPTION = "This is a FooBar description"; + + private NodeRef undeletedTestNode; + private NodeRef deletedTestNode; + + // Injected services + private NodeService nodeService; + private NodeArchiveService nodeArchiveService; + private Repository repositoryHelper; + private RetryingTransactionHelper transactionHelper; + + private StoreRef nodesOriginalStoreRef; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + + // Initialise the required services. + nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); + nodeArchiveService = (NodeArchiveService) getServer().getApplicationContext().getBean("nodeArchiveService"); // Intentionally small 'n'. + repositoryHelper = (Repository) getServer().getApplicationContext().getBean("repositoryHelper"); + transactionHelper = (RetryingTransactionHelper)getServer().getApplicationContext().getBean("retryingTransactionHelper"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create some nodes which we will delete as part of later test methods. + undeletedTestNode = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + Map props = new HashMap(); + String cmName = getClass().getSimpleName() + "_" + System.currentTimeMillis(); + props.put(ContentModel.PROP_NAME, cmName); + props.put(ContentModel.PROP_TITLE, TEST_TITLE); + props.put(ContentModel.PROP_DESCRIPTION, TEST_DESCRIPTION); + ChildAssociationRef chAssRef = nodeService.createNode(repositoryHelper.getCompanyHome(), + ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS, + ContentModel.TYPE_CONTENT, props); + + return chAssRef.getChildRef(); + } + }); + + // We need to remember the StoreRef where this node originally lived. i.e. workspace://SpacesStore + nodesOriginalStoreRef = undeletedTestNode.getStoreRef(); + + // This will ensure that there is always at least some NodeRefs in the 'trash' i.e. the archive store. + deletedTestNode = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Create the test node. + Map props = new HashMap(); + String cmName = getClass().getSimpleName() + "_" + System.currentTimeMillis(); + props.put(ContentModel.PROP_NAME, cmName); + props.put(ContentModel.PROP_TITLE, TEST_TITLE); + props.put(ContentModel.PROP_DESCRIPTION, TEST_DESCRIPTION); + ChildAssociationRef chAssRef = nodeService.createNode(repositoryHelper.getCompanyHome(), + ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS, + ContentModel.TYPE_CONTENT, props); + + // And intentionally delete it again. This will move it to the archive store. + nodeService.deleteNode(chAssRef.getChildRef()); + + // At his point the chAssRef.getChildRef NodeRef will point to the location of the + // node before it got deleted. We need to store it's NodeRef *after* deletion, which + // will point to the archive store. + NodeRef archivedNode = nodeArchiveService.getArchivedNode(chAssRef.getChildRef()); + + return archivedNode; + } + }); + } + + @Override + public void tearDown() throws Exception + { + super.tearDown(); + + // Tidy up any test nodes that are hanging around. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + if (undeletedTestNode != null && nodeService.exists(undeletedTestNode)) + { + nodeService.deleteNode(undeletedTestNode); + } + return null; + } + }); + } + + /** + * This test calls the GET REST API to read some deleted items from the archive store + * and checks the various JSON data fields. + */ + public void testGetDeletedItems() throws Exception + { + JSONObject jsonRsp = getArchivedNodes(); + + JSONObject dataObj = (JSONObject)jsonRsp.get(DATA); + assertNotNull("JSON 'data' object was null", dataObj); + + JSONArray deletedNodesArray = (JSONArray)dataObj.get(AbstractArchivedNodeWebScript.DELETED_NODES); + assertNotNull("JSON 'deletedNodesArray' object was null", deletedNodesArray); + assertTrue("Unexpectedly found 0 items in archive store", 0 != deletedNodesArray.length()); + + // We'll identify a deleted item that we put in the archive during setUp(). + JSONObject deletedNodeToTest = null; + for (int i = 0; i < deletedNodesArray.length(); i++) + { + JSONObject nextJSONObj = (JSONObject)deletedNodesArray.get(i); + String nodeRefString = nextJSONObj.getString(AbstractArchivedNodeWebScript.NODEREF); + if (nodeRefString.equals(deletedTestNode.toString())) + { + deletedNodeToTest = nextJSONObj; + break; + } + } + assertNotNull("Failed to find an expected NodeRef within the archive store.", deletedNodeToTest); + + assertEquals(AuthenticationUtil.getAdminUserName(), deletedNodeToTest.getString(AbstractArchivedNodeWebScript.ARCHIVED_BY)); + assertEquals(TEST_TITLE, deletedNodeToTest.getString(AbstractArchivedNodeWebScript.TITLE)); + assertEquals(TEST_DESCRIPTION, deletedNodeToTest.getString(AbstractArchivedNodeWebScript.DESCRIPTION)); + assertNotNull(deletedNodeToTest.getString(AbstractArchivedNodeWebScript.NAME)); + assertNotNull(deletedNodeToTest.getString(AbstractArchivedNodeWebScript.NODEREF)); + assertNotNull(deletedNodeToTest.getString(AbstractArchivedNodeWebScript.DISPLAY_PATH)); + assertNotNull(deletedNodeToTest.getString(AbstractArchivedNodeWebScript.ARCHIVED_DATE)); + assertNotNull(deletedNodeToTest.getString(AbstractArchivedNodeWebScript.FIRST_NAME)); + assertNotNull(deletedNodeToTest.getString(AbstractArchivedNodeWebScript.LAST_NAME)); + assertNotNull(deletedNodeToTest.getString(AbstractArchivedNodeWebScript.NODE_TYPE)); + + // Check that the results come back sorted by archivedDate (most recent first). + Date previousDate = null; + for (int i = 0; i < deletedNodesArray.length(); i++) + { + JSONObject nextJSONObj = deletedNodesArray.getJSONObject(i); + String nextArchivedDateString = nextJSONObj.getString(AbstractArchivedNodeWebScript.ARCHIVED_DATE); + + final String ftlDatePattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + DateFormat df = new SimpleDateFormat(ftlDatePattern); + + Date nextArchivedDate = df.parse(nextArchivedDateString); + + // Each date should be 'older' than the previous one. + if (previousDate != null) + { + assertTrue("Archived Dates were not reverse-sorted.", nextArchivedDate.before(previousDate)); + } + previousDate = nextArchivedDate; + } + } + + /** + * This method makes a REST call to get all the nodes currently in the archive store. + */ + private JSONObject getArchivedNodes() throws IOException, JSONException, + UnsupportedEncodingException + { + String url = this.getArchiveUrl(nodesOriginalStoreRef); + Response rsp = sendRequest(new GetRequest(url), 200); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + return jsonRsp; + } + + /** + * This test method purges some deleted nodes from the archive store. + */ + public void testPurgeDeletedItems() throws Exception + { + JSONObject archivedNodesJson = getArchivedNodes(); + JSONObject dataJsonObj = archivedNodesJson.getJSONObject("data"); + JSONArray archivedNodesArray = dataJsonObj.getJSONArray(AbstractArchivedNodeWebScript.DELETED_NODES); + + int archivedNodesLength = archivedNodesArray.length(); + assertTrue("Insufficient archived nodes for test to run.", archivedNodesLength > 1); + + // Take a specific archived node and purge it. + JSONObject requiredNodeInArchive = null; + for (int i = 0; i < archivedNodesLength; i++) + { + JSONObject archivedNode = archivedNodesArray.getJSONObject(i); + // We ensure in #setUp() that this NodeRef will be in the archive store. + if (archivedNode.getString(AbstractArchivedNodeWebScript.NODEREF).equals(deletedTestNode.toString())) + { + requiredNodeInArchive = archivedNode; + break; + } + } + assertNotNull("Expected node not found in archive", requiredNodeInArchive); + + // So we have identified a specific Node in the archive that we want to delete permanently (purge). + String nodeRefString = requiredNodeInArchive.getString(AbstractArchivedNodeWebScript.NODEREF); + assertTrue("nodeRef string is invalid", NodeRef.isNodeRef(nodeRefString)); + NodeRef nodeRef = new NodeRef(nodeRefString); + + // This is not the StoreRef where the node originally lived e.g. workspace://SpacesStore + // This is its current StoreRef i.e. archive://SpacesStore + final StoreRef currentStoreRef = nodeRef.getStoreRef(); + + String deleteUrl = getArchiveUrl(currentStoreRef) + "/" + nodeRef.getId(); + + // Send the DELETE REST call. + Response rsp = sendRequest(new DeleteRequest(deleteUrl), 200); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + JSONObject dataObj = jsonRsp.getJSONObject("data"); + JSONArray purgedNodesArray = dataObj.getJSONArray(ArchivedNodesDelete.PURGED_NODES); + assertEquals("Only expected one NodeRef to have been purged.", 1, purgedNodesArray.length()); + + + // Now we'll purge all the other nodes in the archive that came from the same StoreRef. + String deleteAllUrl = getArchiveUrl(this.nodesOriginalStoreRef); + + rsp = sendRequest(new DeleteRequest(deleteAllUrl), 200); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + dataObj = jsonRsp.getJSONObject("data"); + purgedNodesArray = dataObj.getJSONArray(ArchivedNodesDelete.PURGED_NODES); + + // Now retrieve all items from the archive store. There should be none. + assertEquals("Archive store was unexpectedly not empty", 0, getArchivedNodesCount()); + } + + /** + * This method makes a REST call to retrieve the contents of the archive store, returning + * the number of individual nodes contained in it. + */ + private int getArchivedNodesCount() throws IOException, JSONException, + UnsupportedEncodingException + { + JSONObject archiveContents = getArchivedNodes(); + JSONObject datatObject = archiveContents.getJSONObject(DATA); + JSONArray deletedNodesArray = datatObject.getJSONArray(AbstractArchivedNodeWebScript.DELETED_NODES); + return deletedNodesArray.length(); + } + + /** + * This test method restores some deleted nodes from the archive store. + */ + public void testRestoreDeletedItems() throws Exception + { + JSONObject archivedNodesJson = getArchivedNodes(); + JSONObject dataJsonObj = archivedNodesJson.getJSONObject("data"); + JSONArray archivedNodesArray = dataJsonObj.getJSONArray(AbstractArchivedNodeWebScript.DELETED_NODES); + + int archivedNodesLength = archivedNodesArray.length(); + assertTrue("Insufficient archived nodes for test to run.", archivedNodesLength > 1); + + // Take a specific archived node and restore it. + JSONObject firstArchivedNode = archivedNodesArray.getJSONObject(0); + + // So we have identified a specific Node in the archive that we want to restore. + String nodeRefString = firstArchivedNode.getString(AbstractArchivedNodeWebScript.NODEREF); + assertTrue("nodeRef string is invalid", NodeRef.isNodeRef(nodeRefString)); + NodeRef nodeRef = new NodeRef(nodeRefString); + + // This is not the StoreRef where the node originally lived e.g. workspace://SpacesStore + // This is its current StoreRef i.e. archive://SpacesStore + final StoreRef currentStoreRef = nodeRef.getStoreRef(); + + String restoreUrl = getArchiveUrl(currentStoreRef) + "/" + nodeRef.getId(); + + + int archivedNodesCountBeforeRestore = getArchivedNodesCount(); + + // Send the PUT REST call. + String jsonString = new JSONStringer().object() + .key("restoreLocation").value("") + .endObject() + .toString(); + Response rsp = sendRequest(new PutRequest(restoreUrl, jsonString, "application/json"), 200); + + assertEquals("Expected archive to shrink by one", archivedNodesCountBeforeRestore - 1, getArchivedNodesCount()); + } + + /** + * This method gives the 'archive' REST URL for the specified StoreRef. + */ + private String getArchiveUrl(StoreRef storeRef) + { + String result = MessageFormat.format(ARCHIVE_URL_FORMAT, storeRef.getProtocol(), storeRef.getIdentifier()); + return result; + } +} diff --git a/source/java/org/alfresco/repo/web/scripts/archive/NodeTypeFilter.java b/source/java/org/alfresco/repo/web/scripts/archive/NodeTypeFilter.java new file mode 100644 index 0000000000..cd68776e7f --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/archive/NodeTypeFilter.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.archive; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * This class is used to filter nodes based on node type. + * + * @author Neil McErlean + * @since 3.5 + */ +public class NodeTypeFilter implements ArchivedNodesFilter +{ + private NodeService nodeService; + private NamespaceService namespaceService; + private List excludedTypes; + + /** + * This method sets the NamespaceService object. + * @param namespaceService the namespaceService. + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * This method sets the NodeService object. + * @param nodeService the node service. + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the List of node types to exclude. These node types should be in the + * short form e.g. cm:myType. + * + * @param excludedTypesStg a List of node types which are to be excluded. + */ + public void setExcludedTypes(List excludedTypesStg) + { + // Convert the Strings to QNames. + this.excludedTypes = new ArrayList(excludedTypesStg.size()); + for (String s : excludedTypesStg) + { + QName typeQName = QName.createQName(s, namespaceService); + this.excludedTypes.add(typeQName); + } + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.web.scripts.archive.ArchivedNodesFilter#accept(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean accept(NodeRef nodeRef) + { + boolean typeIsExcluded = this.excludedTypes.contains(nodeService.getType(nodeRef)); + return !typeIsExcluded; + } +} + diff --git a/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java b/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java index a5e7c4b236..238cecae53 100644 --- a/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java +++ b/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java @@ -18,7 +18,6 @@ */ package org.alfresco.repo.web.scripts.audit; -import java.io.IOException; import java.net.URL; import java.util.Date; import java.util.Map; @@ -258,13 +257,22 @@ public class AuditWebScriptTest extends BaseWebScriptTest AuthenticationUtil.runAs(failureWork, AuthenticationUtil.getSystemUserName()); } - public void testClearAuditRepo() throws Exception + public synchronized void testClearAuditRepo() throws Exception { - long now = System.currentTimeMillis(); + long now = System.currentTimeMillis() - 10L; // Accuracy can be a problem long future = Long.MAX_VALUE; loginWithFailure(getName()); + // Wait for the background thread to run + try + { + this.wait(100); + } + catch (Throwable e) + { + } + // Delete audit entries that could not have happened String url = "/api/audit/clear/" + APP_REPOTEST_NAME + "?fromTime=" + future; TestWebScriptServer.PostRequest req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); @@ -273,14 +281,25 @@ public class AuditWebScriptTest extends BaseWebScriptTest int cleared = json.getInt(AbstractAuditWebScript.JSON_KEY_CLEARED); assertEquals("Could not have cleared more than 0", 0, cleared); - // Delete the entry (at least) - url = "/api/audit/clear/" + APP_REPOTEST_NAME + "?fromTime=" + now + "&toTime=" + future; - req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); - response = sendRequest(req, Status.STATUS_OK, admin); - json = new JSONObject(response.getContentAsString()); - cleared = json.getInt(AbstractAuditWebScript.JSON_KEY_CLEARED); + // ALF-3055 : auditing of failures is now asynchronous, so loop 60 times with a + // 1 second sleep to ensure that the audit is processed + for(int i = 0; i < 60; i++) + { + // Delete the entry (at least) + url = "/api/audit/clear/" + APP_REPOTEST_NAME + "?fromTime=" + now + "&toTime=" + future; + req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + cleared = json.getInt(AbstractAuditWebScript.JSON_KEY_CLEARED); + if (cleared > 0) + { + break; + } + + Thread.sleep(1000); + } assertTrue("Should have cleared at least 1 entry", cleared > 0); - + // Delete all entries url = "/api/audit/clear/" + APP_REPOTEST_NAME;; req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); diff --git a/source/java/org/alfresco/repo/web/scripts/comment/ScriptCommentService.java b/source/java/org/alfresco/repo/web/scripts/comment/ScriptCommentService.java index eacc8c63e5..9de78da18c 100644 --- a/source/java/org/alfresco/repo/web/scripts/comment/ScriptCommentService.java +++ b/source/java/org/alfresco/repo/web/scripts/comment/ScriptCommentService.java @@ -85,7 +85,7 @@ public class ScriptCommentService extends BaseScopableProcessorExtension { NodeRef forumFolder = assocs.get(0).getChildRef(); - Map props = new HashMap(1); + Map props = new HashMap(1, 1.0f); props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME); commentsFolder = nodeService.createNode( forumFolder, @@ -103,10 +103,8 @@ public class ScriptCommentService extends BaseScopableProcessorExtension return commentsFolder; } - }, AuthenticationUtil.getAdminUserName()); + }, AuthenticationUtil.getSystemUserName()); return new ScriptNode(commentsFolder, serviceRegistry, getScope()); } - - } diff --git a/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java b/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java index 7931e3ca12..03c2cb75f5 100644 --- a/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java +++ b/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -46,6 +46,8 @@ import org.alfresco.service.namespace.QName; import org.alfresco.util.TempFileProvider; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; import org.springframework.extensions.webscripts.AbstractWebScript; import org.springframework.extensions.webscripts.Cache; import org.springframework.extensions.webscripts.ScriptProcessor; @@ -64,7 +66,7 @@ import de.schlichtherle.io.FileOutputStream; * * @author Roy Wetherall */ -public class StreamContent extends AbstractWebScript +public class StreamContent extends AbstractWebScript implements ResourceLoaderAware { // Logger private static final Log logger = LogFactory.getLog(StreamContent.class); @@ -78,8 +80,14 @@ public class StreamContent extends AbstractWebScript protected PermissionService permissionService; protected NodeService nodeService; protected ContentService contentService; - protected MimetypeService mimetypeService; - + protected MimetypeService mimetypeService; + protected ResourceLoader resourceLoader; + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) + { + this.resourceLoader = resourceLoader; + } /** * @param mimetypeService @@ -173,7 +181,9 @@ public class StreamContent extends AbstractWebScript NodeRef nodeRef = (NodeRef)model.get("contentNode"); if (nodeRef == null) { - throw new WebScriptException("The content node was not specified so the content cannot be streamed to the client"); + throw new WebScriptException( + "The content node was not specified so the content cannot be streamed to the client: " + + executeScript.getContent().getPathDescription()); } QName propertyQName = null; String contentProperty = (String)model.get("contentProperty"); @@ -377,7 +387,7 @@ public class StreamContent extends AbstractWebScript * * @param req The request * @param res The response - * @param resourcePath The resource path the content is required for + * @param resourcePath The classpath resource path the content is required for * @param attach Indicates whether the content should be streamed as an attachment or not * @throws IOException */ @@ -392,7 +402,7 @@ public class StreamContent extends AbstractWebScript * * @param req The request * @param res The response - * @param resourcePath The resource path the content is required for + * @param resourcePath The classpath resource path the content is required for. * @param attach Indicates whether the content should be streamed as an attachment or not * @param attachFileName Optional file name to use when attach is true * @throws IOException @@ -411,14 +421,21 @@ public class StreamContent extends AbstractWebScript ext = resourcePath.substring(extIndex); } + // We need to retrieve the modification date/time from the resource itself. + StringBuilder sb = new StringBuilder("classpath:").append(resourcePath); + final String classpathResource = sb.toString(); + + long resourceLastModified = resourceLoader.getResource(classpathResource).lastModified(); + // create temporary file File file = TempFileProvider.createTempFile("streamContent-", ext); - InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourcePath); - OutputStream os = new FileOutputStream(file); - FileCopyUtils.copy(is, os); + + InputStream is = resourceLoader.getResource(classpathResource).getInputStream(); + OutputStream os = new FileOutputStream(file); + FileCopyUtils.copy(is, os); - // stream the contents of the file - streamContent(req, res, file, attach, attachFileName); + // stream the contents of the file, but using the modifiedDate of the original resource. + streamContent(req, res, file, resourceLastModified, attach, attachFileName); } /** @@ -437,17 +454,36 @@ public class StreamContent extends AbstractWebScript } /** - * Streams content back to client from a given resource path. + * Streams content back to client from a given File. The Last-Modified header will reflect the + * given file's modification timestamp. * * @param req The request * @param res The response - * @param resourcePath The resource path the content is required for + * @param file The file whose content is to be streamed. * @param attach Indicates whether the content should be streamed as an attachment or not * @param attachFileName Optional file name to use when attach is true * @throws IOException */ protected void streamContent(WebScriptRequest req, WebScriptResponse res, File file, boolean attach, String attachFileName) throws IOException + { + streamContent(req, res, file, null, attach, attachFileName); + } + + /** + * Streams content back to client from a given File. + * + * @param req The request + * @param res The response + * @param file The file whose content is to be streamed. + * @param modifiedTime The modified datetime to use for the streamed content. If null the + * file's timestamp will be used. + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + protected void streamContent(WebScriptRequest req, WebScriptResponse res, File file, Long modifiedTime, + boolean attach, String attachFileName) throws IOException { if (logger.isDebugEnabled()) logger.debug("Retrieving content from file " + file.getAbsolutePath() + " (attach: " + attach + ")"); @@ -466,12 +502,13 @@ public class StreamContent extends AbstractWebScript reader.setMimetype(mimetype); reader.setEncoding("UTF-8"); - Date lastModified = new Date(file.lastModified()); + long lastModified = modifiedTime == null ? file.lastModified() : modifiedTime; + Date lastModifiedDate = new Date(lastModified); - streamContentImpl(req, res, reader, attach, lastModified, - String.valueOf(lastModified.getTime()), attachFileName); + streamContentImpl(req, res, reader, attach, lastModifiedDate, + String.valueOf(lastModifiedDate.getTime()), attachFileName); } - + /** * Stream content implementation * diff --git a/source/java/org/alfresco/repo/web/scripts/dictionary/DictionaryRestApiTest.java b/source/java/org/alfresco/repo/web/scripts/dictionary/DictionaryRestApiTest.java index c51fe96bdb..8e0eacdbf2 100644 --- a/source/java/org/alfresco/repo/web/scripts/dictionary/DictionaryRestApiTest.java +++ b/source/java/org/alfresco/repo/web/scripts/dictionary/DictionaryRestApiTest.java @@ -107,8 +107,8 @@ public class DictionaryRestApiTest extends BaseWebScriptTest private void validateAssociationDef(JSONObject result) throws Exception { assertEquals("cm:avatar", result.get("name")); - assertEquals("", result.get("title")); - assertEquals("", result.get("description")); + assertEquals("Avatar", result.get("title")); + assertEquals("The person's avatar image", result.get("description")); assertEquals(false, result.get("isChildAssociation")); assertEquals(false, result.get("protected")); @@ -714,7 +714,7 @@ public class DictionaryRestApiTest extends BaseWebScriptTest } - public void testGetAssociatoinDef() throws Exception + public void testGetAssociationDef() throws Exception { GetRequest req = new GetRequest(URL_SITES + "/cm_person/association/cm_avatar"); Response response = sendRequest(req, 200); @@ -745,7 +745,7 @@ public class DictionaryRestApiTest extends BaseWebScriptTest assertEquals(200,response.getStatus()); } - public void testGetAssociatoinDefs() throws Exception + public void testGetAssociationDefs() throws Exception { //validate with associationfilter=>all and classname=>wca_form GetRequest req = new GetRequest(URL_SITES + "/wca_form/associations"); diff --git a/source/java/org/alfresco/repo/web/scripts/forms/AbstractTestFormRestApi.java b/source/java/org/alfresco/repo/web/scripts/forms/AbstractTestFormRestApi.java index 70270bd250..9a629b8997 100644 --- a/source/java/org/alfresco/repo/web/scripts/forms/AbstractTestFormRestApi.java +++ b/source/java/org/alfresco/repo/web/scripts/forms/AbstractTestFormRestApi.java @@ -28,6 +28,7 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.model.Repository; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.repo.web.scripts.BaseWebScriptTest; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; @@ -37,6 +38,7 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.GUID; import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.Response; @@ -69,6 +71,7 @@ public abstract class AbstractTestFormRestApi extends BaseWebScriptTest private ContentService contentService; private Repository repositoryHelper; protected NodeRef containerNodeRef; + protected TransactionService transactionService; @Override protected void setUp() throws Exception @@ -81,103 +84,119 @@ public abstract class AbstractTestFormRestApi extends BaseWebScriptTest this.repositoryHelper = (Repository) getServer().getApplicationContext().getBean( "repositoryHelper"); this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); + this.transactionService = (TransactionService) getServer().getApplicationContext() + .getBean("transactionService"); - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ - NodeRef companyHomeNodeRef = this.repositoryHelper.getCompanyHome(); + @Override + public Void execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - String guid = GUID.generate(); + NodeRef companyHomeNodeRef = AbstractTestFormRestApi.this.repositoryHelper.getCompanyHome(); - // Create a test file (not a folder) - FileInfo referencingDoc = this.fileFolderService.create(companyHomeNodeRef, - "referencingDoc" + guid + ".txt", ContentModel.TYPE_CONTENT); - referencingDocNodeRef = referencingDoc.getNodeRef(); + String guid = GUID.generate(); - // Add an aspect. - Map aspectProps = new HashMap(2); - aspectProps.put(ContentModel.PROP_TITLE, TEST_FORM_TITLE); - aspectProps.put(ContentModel.PROP_DESCRIPTION, TEST_FORM_DESCRIPTION); - nodeService.addAspect(referencingDocNodeRef, ContentModel.ASPECT_TITLED, aspectProps); + // Create a test file (not a folder) + FileInfo referencingDoc = AbstractTestFormRestApi.this.fileFolderService.create(companyHomeNodeRef, + "referencingDoc" + guid + ".txt", ContentModel.TYPE_CONTENT); + referencingDocNodeRef = referencingDoc.getNodeRef(); - // Write some content into the node. - ContentWriter contentWriter = this.contentService.getWriter(referencingDoc.getNodeRef(), - ContentModel.PROP_CONTENT, true); - contentWriter.setEncoding("UTF-8"); - contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - contentWriter.putContent("The quick brown fox jumped over the lazy dog."); - - // Create a folder - will use this for child-association testing - FileInfo associatedDocsFolder = - this.fileFolderService.create(companyHomeNodeRef, "testFolder" + guid, ContentModel.TYPE_FOLDER); - - testFolderNodeRef = associatedDocsFolder.getNodeRef(); - - this.associatedDoc_A = createTestNode("associatedDoc_A" + guid); - this.associatedDoc_B = createTestNode("associatedDoc_B" + guid); - this.associatedDoc_C = createTestNode("associatedDoc_C" + guid); - this.associatedDoc_D = createTestNode("associatedDoc_D" + guid); - this.associatedDoc_E = createTestNode("associatedDoc_E" + guid); + // Add an aspect. + Map aspectProps = new HashMap(2); + aspectProps.put(ContentModel.PROP_TITLE, TEST_FORM_TITLE); + aspectProps.put(ContentModel.PROP_DESCRIPTION, TEST_FORM_DESCRIPTION); + nodeService.addAspect(referencingDocNodeRef, ContentModel.ASPECT_TITLED, aspectProps); - // Now create associations between the referencing and the two node refs. - aspectProps.clear(); - this.nodeService.addAspect(this.referencingDocNodeRef, ContentModel.ASPECT_REFERENCING, aspectProps); - this.nodeService.createAssociation(this.referencingDocNodeRef, associatedDoc_A, ContentModel.ASSOC_REFERENCES); - this.nodeService.createAssociation(this.referencingDocNodeRef, associatedDoc_B, ContentModel.ASSOC_REFERENCES); - // Leave the 3rd, 4th and 5th nodes without associations as they may be created in - // other test code. + // Write some content into the node. + ContentWriter contentWriter = AbstractTestFormRestApi.this.contentService.getWriter(referencingDoc.getNodeRef(), + ContentModel.PROP_CONTENT, true); + contentWriter.setEncoding("UTF-8"); + contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + contentWriter.putContent("The quick brown fox jumped over the lazy dog."); + + // Create a folder - will use this for child-association testing + FileInfo associatedDocsFolder = + AbstractTestFormRestApi.this.fileFolderService.create(companyHomeNodeRef, "testFolder" + guid, ContentModel.TYPE_FOLDER); + + testFolderNodeRef = associatedDocsFolder.getNodeRef(); + + AbstractTestFormRestApi.this.associatedDoc_A = createTestNode("associatedDoc_A" + guid); + AbstractTestFormRestApi.this.associatedDoc_B = createTestNode("associatedDoc_B" + guid); + AbstractTestFormRestApi.this.associatedDoc_C = createTestNode("associatedDoc_C" + guid); + AbstractTestFormRestApi.this.associatedDoc_D = createTestNode("associatedDoc_D" + guid); + AbstractTestFormRestApi.this.associatedDoc_E = createTestNode("associatedDoc_E" + guid); - // Create a container for the children. - HashMap containerProps = new HashMap(); - this.containerNodeRef = nodeService.createNode(companyHomeNodeRef, ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "testContainer" + guid), - ContentModel.TYPE_CONTAINER, - containerProps).getChildRef(); - - this.childDoc_A = createTestNode("childDoc_A" + guid); - this.childDoc_B = createTestNode("childDoc_B" + guid); - this.childDoc_C = createTestNode("childDoc_C" + guid); - this.childDoc_D = createTestNode("childDoc_D" + guid); - this.childDoc_E = createTestNode("childDoc_E" + guid); - - // Now create the pre-test child-associations. - this.nodeService.addChild(containerNodeRef, childDoc_A, ContentModel.ASSOC_CHILDREN, QName.createQName("childA")); - this.nodeService.addChild(containerNodeRef, childDoc_B, ContentModel.ASSOC_CHILDREN, QName.createQName("childB")); - // The other childDoc nodes will be added as children over the REST API as part - // of later test code. + // Now create associations between the referencing and the two node refs. + aspectProps.clear(); + AbstractTestFormRestApi.this.nodeService.addAspect(AbstractTestFormRestApi.this.referencingDocNodeRef, ContentModel.ASPECT_REFERENCING, aspectProps); + AbstractTestFormRestApi.this.nodeService.createAssociation(AbstractTestFormRestApi.this.referencingDocNodeRef, associatedDoc_A, ContentModel.ASSOC_REFERENCES); + AbstractTestFormRestApi.this.nodeService.createAssociation(AbstractTestFormRestApi.this.referencingDocNodeRef, associatedDoc_B, ContentModel.ASSOC_REFERENCES); + // Leave the 3rd, 4th and 5th nodes without associations as they may be created in + // other test code. - // Create and store the urls to the referencingNode - StringBuilder builder = new StringBuilder(); - builder.append("/api/node/workspace/").append(referencingDocNodeRef.getStoreRef().getIdentifier()) - .append("/").append(referencingDocNodeRef.getId()).append("/formprocessor"); - this.referencingNodeUpdateUrl = builder.toString(); - - builder = new StringBuilder(); - builder.append("/api/node/workspace/").append(containerNodeRef.getStoreRef().getIdentifier()) - .append("/").append(containerNodeRef.getId()).append("/formprocessor"); - this.containingNodeUpdateUrl = builder.toString(); - - // Store the original properties of this node - this.refNodePropertiesAfterCreation = nodeService.getProperties(referencingDocNodeRef); - - refNodePropertiesAfterCreation.toString(); + // Create a container for the children. + HashMap containerProps = new HashMap(); + AbstractTestFormRestApi.this.containerNodeRef = nodeService.createNode(companyHomeNodeRef, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "testContainer" + guid), + ContentModel.TYPE_CONTAINER, + containerProps).getChildRef(); + + AbstractTestFormRestApi.this.childDoc_A = createTestNode("childDoc_A" + guid); + AbstractTestFormRestApi.this.childDoc_B = createTestNode("childDoc_B" + guid); + AbstractTestFormRestApi.this.childDoc_C = createTestNode("childDoc_C" + guid); + AbstractTestFormRestApi.this.childDoc_D = createTestNode("childDoc_D" + guid); + AbstractTestFormRestApi.this.childDoc_E = createTestNode("childDoc_E" + guid); + + // Now create the pre-test child-associations. + AbstractTestFormRestApi.this.nodeService.addChild(containerNodeRef, childDoc_A, ContentModel.ASSOC_CHILDREN, QName.createQName("childA")); + AbstractTestFormRestApi.this.nodeService.addChild(containerNodeRef, childDoc_B, ContentModel.ASSOC_CHILDREN, QName.createQName("childB")); + // The other childDoc nodes will be added as children over the REST API as part + // of later test code. + + // Create and store the urls to the referencingNode + StringBuilder builder = new StringBuilder(); + builder.append("/api/node/workspace/").append(referencingDocNodeRef.getStoreRef().getIdentifier()) + .append("/").append(referencingDocNodeRef.getId()).append("/formprocessor"); + AbstractTestFormRestApi.this.referencingNodeUpdateUrl = builder.toString(); + + builder = new StringBuilder(); + builder.append("/api/node/workspace/").append(containerNodeRef.getStoreRef().getIdentifier()) + .append("/").append(containerNodeRef.getId()).append("/formprocessor"); + AbstractTestFormRestApi.this.containingNodeUpdateUrl = builder.toString(); + + // Store the original properties of this node + AbstractTestFormRestApi.this.refNodePropertiesAfterCreation = nodeService.getProperties(referencingDocNodeRef); + + refNodePropertiesAfterCreation.toString(); + return null; + }}); } @Override public void tearDown() { - nodeService.deleteNode(this.referencingDocNodeRef); - nodeService.deleteNode(this.associatedDoc_A); - nodeService.deleteNode(this.associatedDoc_B); - nodeService.deleteNode(this.associatedDoc_C); - nodeService.deleteNode(this.associatedDoc_D); - nodeService.deleteNode(this.associatedDoc_E); - nodeService.deleteNode(this.childDoc_A); - nodeService.deleteNode(this.childDoc_B); - nodeService.deleteNode(this.childDoc_C); - nodeService.deleteNode(this.childDoc_D); - nodeService.deleteNode(this.childDoc_E); - nodeService.deleteNode(this.testFolderNodeRef); - nodeService.deleteNode(this.containerNodeRef); + this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ + + @Override + public Void execute() throws Throwable + { + nodeService.deleteNode(AbstractTestFormRestApi.this.referencingDocNodeRef); + nodeService.deleteNode(AbstractTestFormRestApi.this.associatedDoc_A); + nodeService.deleteNode(AbstractTestFormRestApi.this.associatedDoc_B); + nodeService.deleteNode(AbstractTestFormRestApi.this.associatedDoc_C); + nodeService.deleteNode(AbstractTestFormRestApi.this.associatedDoc_D); + nodeService.deleteNode(AbstractTestFormRestApi.this.associatedDoc_E); + nodeService.deleteNode(AbstractTestFormRestApi.this.childDoc_A); + nodeService.deleteNode(AbstractTestFormRestApi.this.childDoc_B); + nodeService.deleteNode(AbstractTestFormRestApi.this.childDoc_C); + nodeService.deleteNode(AbstractTestFormRestApi.this.childDoc_D); + nodeService.deleteNode(AbstractTestFormRestApi.this.childDoc_E); + nodeService.deleteNode(AbstractTestFormRestApi.this.testFolderNodeRef); + nodeService.deleteNode(AbstractTestFormRestApi.this.containerNodeRef); + return null; + }}); } protected Response sendGetReq(String url, int expectedStatusCode) diff --git a/source/java/org/alfresco/repo/web/scripts/invite/InviteServiceTest.java b/source/java/org/alfresco/repo/web/scripts/invite/InviteServiceTest.java index 9880009194..d99ace83ef 100644 --- a/source/java/org/alfresco/repo/web/scripts/invite/InviteServiceTest.java +++ b/source/java/org/alfresco/repo/web/scripts/invite/InviteServiceTest.java @@ -154,79 +154,86 @@ public class InviteServiceTest extends BaseWebScriptTest // Create new invitee email address list this.inviteeEmailAddrs = new ArrayList(); - - // - // various setup operations which need to be run as system user - // - AuthenticationUtil.runAs(new RunAsWork() - { - public Object doWork() throws Exception + + this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ + + @Override + public Void execute() throws Throwable { - // Create inviter person - createPerson(PERSON_FIRSTNAME, PERSON_LASTNAME, USER_INVITER, INVITER_EMAIL); - - // Create inviter2 person - createPerson(PERSON_FIRSTNAME, PERSON_LASTNAME, USER_INVITER_2, INVITER_EMAIL_2); + // + // various setup operations which need to be run as system user + // + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + // Create inviter person + createPerson(PERSON_FIRSTNAME, PERSON_LASTNAME, USER_INVITER, INVITER_EMAIL); + + // Create inviter2 person + createPerson(PERSON_FIRSTNAME, PERSON_LASTNAME, USER_INVITER_2, INVITER_EMAIL_2); + + return null; + } + }, AuthenticationUtil.getSystemUserName()); + // The creation of sites is heavily dependent on the authenticated user. We must ensure that, + // when doing the runAs below, the user both 'runAs' and 'fullyAuthenticated'. In order for + // this to be the case, the security context MUST BE EMPTY now. We could do the old + // "defensive clear", but really there should not be any lurking authentications on this thread + // after the context starts up. If there are, that is a bug, and we fail explicitly here. + String residuallyAuthenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser(); + assertNull( + "Residual authentication on context-initiating thread (this thread):" + residuallyAuthenticatedUser, + residuallyAuthenticatedUser); + + // + // various setup operations which need to be run as inviter user + // + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + // Create first site for Inviter to invite Invitee to + SiteInfo siteInfo = siteService.getSite(SITE_SHORT_NAME_INVITE_1); + if (siteInfo == null) + { + siteService.createSite("InviteSitePreset", SITE_SHORT_NAME_INVITE_1, + "InviteSiteTitle", "InviteSiteDescription", SiteVisibility.PUBLIC); + } + + // Create second site for inviter to invite invitee to + siteInfo = siteService.getSite(SITE_SHORT_NAME_INVITE_2); + if (siteInfo == null) + { + siteService.createSite("InviteSitePreset", SITE_SHORT_NAME_INVITE_2, + "InviteSiteTitle", "InviteSiteDescription", SiteVisibility.PUBLIC); + } + + // Create third site for inviter to invite invitee to + siteInfo = InviteServiceTest.this.siteService.getSite(SITE_SHORT_NAME_INVITE_3); + if (siteInfo == null) + { + siteService.createSite( + "InviteSitePreset", SITE_SHORT_NAME_INVITE_3, + "InviteSiteTitle", "InviteSiteDescription", SiteVisibility.PUBLIC); + } + + // set inviter2's role on third site to collaborator + String inviterSiteRole = siteService.getMembersRole(SITE_SHORT_NAME_INVITE_3, USER_INVITER_2); + if ((inviterSiteRole == null) || (inviterSiteRole.equals(SiteModel.SITE_COLLABORATOR) == false)) + { + siteService.setMembership(SITE_SHORT_NAME_INVITE_3, USER_INVITER_2, SiteModel.SITE_COLLABORATOR); + } + + return null; + } + }, USER_INVITER); + + // Do tests as inviter user + InviteServiceTest.this.authenticationComponent.setCurrentUser(USER_INVITER); return null; - } - }, AuthenticationUtil.getSystemUserName()); - - // The creation of sites is heavily dependent on the authenticated user. We must ensure that, - // when doing the runAs below, the user both 'runAs' and 'fullyAuthenticated'. In order for - // this to be the case, the security context MUST BE EMPTY now. We could do the old - // "defensive clear", but really there should not be any lurking authentications on this thread - // after the context starts up. If there are, that is a bug, and we fail explicitly here. - String residuallyAuthenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser(); - assertNull( - "Residual authentication on context-initiating thread (this thread):" + residuallyAuthenticatedUser, - residuallyAuthenticatedUser); - - // - // various setup operations which need to be run as inviter user - // - AuthenticationUtil.runAs(new RunAsWork() - { - public Object doWork() throws Exception - { - // Create first site for Inviter to invite Invitee to - SiteInfo siteInfo = siteService.getSite(SITE_SHORT_NAME_INVITE_1); - if (siteInfo == null) - { - siteService.createSite("InviteSitePreset", SITE_SHORT_NAME_INVITE_1, - "InviteSiteTitle", "InviteSiteDescription", SiteVisibility.PUBLIC); - } - - // Create second site for inviter to invite invitee to - siteInfo = siteService.getSite(SITE_SHORT_NAME_INVITE_2); - if (siteInfo == null) - { - siteService.createSite("InviteSitePreset", SITE_SHORT_NAME_INVITE_2, - "InviteSiteTitle", "InviteSiteDescription", SiteVisibility.PUBLIC); - } - - // Create third site for inviter to invite invitee to - siteInfo = InviteServiceTest.this.siteService.getSite(SITE_SHORT_NAME_INVITE_3); - if (siteInfo == null) - { - siteService.createSite( - "InviteSitePreset", SITE_SHORT_NAME_INVITE_3, - "InviteSiteTitle", "InviteSiteDescription", SiteVisibility.PUBLIC); - } - - // set inviter2's role on third site to collaborator - String inviterSiteRole = siteService.getMembersRole(SITE_SHORT_NAME_INVITE_3, USER_INVITER_2); - if ((inviterSiteRole == null) || (inviterSiteRole.equals(SiteModel.SITE_COLLABORATOR) == false)) - { - siteService.setMembership(SITE_SHORT_NAME_INVITE_3, USER_INVITER_2, SiteModel.SITE_COLLABORATOR); - } - - return null; - } - }, USER_INVITER); - - // Do tests as inviter user - this.authenticationComponent.setCurrentUser(USER_INVITER); + }}); } /** @@ -275,53 +282,56 @@ public class InviteServiceTest extends BaseWebScriptTest // // run various teardown operations which need to be run as 'admin' // - RunAsWork runAsWork = new RunAsWork() - { - public Object doWork() throws Exception + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ + + @Override + public Void execute() throws Throwable { - // delete the inviter - deletePersonByUserName(USER_INVITER); - - // delete all invitee people - for (String inviteeEmail : InviteServiceTest.this.inviteeEmailAddrs) + RunAsWork runAsWork = new RunAsWork() { - // - // delete all people with given email address - // - - Set people = InviteServiceTest.this.personService - .getPeopleFilteredByProperty( - ContentModel.PROP_EMAIL, inviteeEmail); - for (NodeRef person : people) + public Object doWork() throws Exception { - String userName = DefaultTypeConverter.INSTANCE - .convert(String.class, InviteServiceTest.this.nodeService.getProperty( - person, ContentModel.PROP_USERNAME)); + // delete the inviter + deletePersonByUserName(USER_INVITER); - // delete person - deletePersonByUserName(userName); + // delete all invitee people + for (String inviteeEmail : InviteServiceTest.this.inviteeEmailAddrs) + { + // + // delete all people with given email address + // + + Set people = InviteServiceTest.this.personService.getPeopleFilteredByProperty( + ContentModel.PROP_EMAIL, inviteeEmail); + for (NodeRef person : people) + { + String userName = DefaultTypeConverter.INSTANCE.convert(String.class, + InviteServiceTest.this.nodeService.getProperty(person, ContentModel.PROP_USERNAME)); + + // delete person + deletePersonByUserName(userName); + } + } + + // delete invite sites + siteService.deleteSite(SITE_SHORT_NAME_INVITE_1); + siteService.deleteSite(SITE_SHORT_NAME_INVITE_2); + siteService.deleteSite(SITE_SHORT_NAME_INVITE_3); + + return null; } + }; + AuthenticationUtil.runAs(runAsWork, AuthenticationUtil.getSystemUserName()); + + // cancel all active invite workflows + WorkflowDefinition wfDef = InviteServiceTest.this.workflowService.getDefinitionByName(WF_DEFINITION_INVITE); + List workflowList = InviteServiceTest.this.workflowService.getActiveWorkflows(wfDef.id); + for (WorkflowInstance workflow : workflowList) + { + InviteServiceTest.this.workflowService.cancelWorkflow(workflow.id); } - - // delete invite sites - siteService.deleteSite(SITE_SHORT_NAME_INVITE_1); - siteService.deleteSite(SITE_SHORT_NAME_INVITE_2); - siteService.deleteSite(SITE_SHORT_NAME_INVITE_3); - return null; - } - }; - AuthenticationUtil.runAs(runAsWork, AuthenticationUtil.getSystemUserName()); - - // cancel all active invite workflows - WorkflowDefinition wfDef = InviteServiceTest.this.workflowService - .getDefinitionByName(WF_DEFINITION_INVITE); - List workflowList = InviteServiceTest.this.workflowService - .getActiveWorkflows(wfDef.id); - for (WorkflowInstance workflow : workflowList) - { - InviteServiceTest.this.workflowService.cancelWorkflow(workflow.id); - } + }}); } private void addUserToGroup(String groupName, String userName) diff --git a/source/java/org/alfresco/repo/web/scripts/person/PersonServiceTest.java b/source/java/org/alfresco/repo/web/scripts/person/PersonServiceTest.java index 98e041e8a0..d0ddd26d45 100644 --- a/source/java/org/alfresco/repo/web/scripts/person/PersonServiceTest.java +++ b/source/java/org/alfresco/repo/web/scripts/person/PersonServiceTest.java @@ -102,15 +102,8 @@ public class PersonServiceTest extends BaseWebScriptTest String adminUser = this.authenticationComponent.getSystemUserName(); this.authenticationComponent.setCurrentUser(adminUser); - /* - * TODO: glen.johnson at alfresco dot com - - * When DELETE /people/{userid} becomes a requirement and is subsequently implemented, - * include this section to tidy-up people's resources created during the execution of the test - * - */ for (String userName : this.createdPeople) { - // deleteRequest(URL_PEOPLE + "/" + userName, 0); personService.deletePerson(userName); } @@ -192,14 +185,14 @@ public class PersonServiceTest extends BaseWebScriptTest return new JSONObject(response.getContentAsString()); } + @SuppressWarnings("unused") public void testGetPeople() throws Exception { // Test basic GET people with no filters == - Response response = sendRequest(new GetRequest(URL_PEOPLE), 200); - System.out.println(response.getContentAsString()); } + @SuppressWarnings("unused") public void testGetPerson() throws Exception { // Get a person that doesn't exist diff --git a/source/java/org/alfresco/repo/web/scripts/person/UserCSVUploadGet.java b/source/java/org/alfresco/repo/web/scripts/person/UserCSVUploadGet.java new file mode 100644 index 0000000000..1ae0284ba3 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/person/UserCSVUploadGet.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.person; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVStrategy; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * Webscript implementation for giving information on uploading + * users via a CSV. + * + * @author Nick Burch + * @since 3.5 + */ +public class UserCSVUploadGet extends DeclarativeWebScript +{ + public static final String MODEL_CSV = "csv"; + public static final String MODEL_EXCEL = "excel"; + + private DictionaryService dictionaryService; + + /** + * @param dictionaryService the DictionaryService to set + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + Map model = new HashMap(); + model.put("success", Boolean.TRUE); + + // What format are they after? + String format = req.getFormat(); + if ("csv".equals(format) || "xls".equals(format) || + "xlsx".equals(format) || "excel".equals(format)) + { + try + { + generateTemplateFile(format, req, status, model); + return model; + } + catch (IOException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Unable to generate template file", e); + } + } + + // Give them the help and the upload form + return model; + } + + /** + * Generates a template file to help the user figure out what to + * put into their CSV + */ + private void generateTemplateFile(String format, WebScriptRequest req, Status status, Map model) + throws IOException + { + Pattern p = Pattern.compile("([A-Z][a-z]+)([A-Z].*)"); + + String[] headings = new String[UserCSVUploadPost.COLUMNS.length]; + String[] descriptions = new String[UserCSVUploadPost.COLUMNS.length]; + for (int i=0; i 0) + { + // Add a description for it too + if (draw == null) + { + draw = sheet.createDrawingPatriarch(); + } + ClientAnchor ca = wb.getCreationHelper().createClientAnchor(); + ca.setCol1(c.getColumnIndex()); + ca.setCol2(c.getColumnIndex()+1); + ca.setRow1(hr.getRowNum()); + ca.setRow2(hr.getRowNum()+2); + + Comment cmt = draw.createCellComment(ca); + cmt.setAuthor(""); + cmt.setString(wb.getCreationHelper().createRichTextString(descriptions[i])); + cmt.setVisible(false); + c.setCellComment(cmt); + } + } + + // Add an empty data row + sheet.createRow(1); + + // Save it for the template + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + wb.write(baos); + model.put(MODEL_EXCEL, baos.toByteArray()); + } + } + + @Override + protected Map createTemplateParameters(WebScriptRequest req, WebScriptResponse res, + Map customParams) + { + Map model = super.createTemplateParameters(req, res, customParams); + // We sometimes need to monkey around to do the binary output... + model.put("req", req); + model.put("res", res); + model.put("writeExcel", new WriteExcel(res, model, req.getFormat())); + return model; + } + + public static class WriteExcel + { + private String format; + private WebScriptResponse res; + private Map model; + + private WriteExcel(WebScriptResponse res, Map model, String format) + { + this.res = res; + this.model = model; + this.format = format; + } + + public void write() throws IOException + { + String filename = "ExampleUserUpload." + format; + + // If it isn't a CSV, reset so we can send binary + if (!"csv".equals(format)) + { + res.reset(); + } + + // Tell the browser it's a file download + res.addHeader("Content-Disposition", "attachment; filename=" + filename); + + // Now send that data + if ("csv".equals(format)) + { + res.getWriter().append((String)model.get(MODEL_CSV)); + } + else + { + // Set the mimetype, as we've reset + if ("xlsx".equals(format)) + { + res.setContentType(MimetypeMap.MIMETYPE_OPENXML_SPREADSHEET); + } + else + { + res.setContentType(MimetypeMap.MIMETYPE_EXCEL); + } + + // Send the raw excel bytes + byte[] excel = (byte[])model.get(MODEL_EXCEL); + res.getOutputStream().write(excel); + } + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/person/UserCSVUploadPost.java b/source/java/org/alfresco/repo/web/scripts/person/UserCSVUploadPost.java new file mode 100644 index 0000000000..38b3b9e379 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/person/UserCSVUploadPost.java @@ -0,0 +1,588 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.person; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.person.PersonServiceImpl; +import org.alfresco.repo.tenant.TenantDomainMismatchException; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVStrategy; +import org.apache.commons.lang.mutable.MutableInt; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.util.PaneInformation; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * Webscript implementation for the POST method for uploading a + * CSV of users to have them created. + * + * @author Nick Burch + * @since 3.5 + */ +public class UserCSVUploadPost extends DeclarativeWebScript +{ + protected static final QName[] COLUMNS = new QName[] { + ContentModel.PROP_USERNAME, ContentModel.PROP_FIRSTNAME, + ContentModel.PROP_LASTNAME, ContentModel.PROP_EMAIL, + null, + ContentModel.PROP_PASSWORD, ContentModel.PROP_ORGANIZATION, + ContentModel.PROP_JOBTITLE, ContentModel.PROP_LOCATION, + ContentModel.PROP_TELEPHONE, ContentModel.PROP_MOBILE, + ContentModel.PROP_SKYPE, ContentModel.PROP_INSTANTMSG, + ContentModel.PROP_GOOGLEUSERNAME, + ContentModel.PROP_COMPANYADDRESS1, ContentModel.PROP_COMPANYADDRESS2, + ContentModel.PROP_COMPANYADDRESS3, ContentModel.PROP_COMPANYPOSTCODE, + ContentModel.PROP_COMPANYTELEPHONE, ContentModel.PROP_COMPANYFAX, + ContentModel.PROP_COMPANYEMAIL + }; + + private static final String ERROR_BAD_FORM = "person.err.userCSV.invalidForm"; + private static final String ERROR_NO_FILE = "person.err.userCSV.noFile"; + private static final String ERROR_CORRUPT_FILE = "person.err.userCSV.corruptFile"; + private static final String ERROR_GENERAL = "person.err.userCSV.general"; + private static final String ERROR_BLANK_COLUMN = "person.err.userCSV.blankColumn"; + private static final String MSG_CREATED = "person.msg.userCSV.created"; + private static final String MSG_EXISTING = "person.msg.userCSV.existing"; + + private static Log logger = LogFactory.getLog(UserCSVUploadPost.class); + + private MutableAuthenticationService authenticationService; + private AuthorityService authorityService; + private PersonService personService; + private TenantService tenantService; + private DictionaryService dictionaryService; + private RetryingTransactionHelper retryingTransactionHelper; + + /** + * @param authenticationService the AuthenticationService to set + */ + public void setAuthenticationService(MutableAuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * @param authorityService the AuthorityService to set + */ + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + /** + * @param personService the PersonService to set + */ + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * @param tenantService the TenantService to set + */ + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + /** + * @param dictionaryService the DictionaryService to set + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param retryingTransactionHelper the helper for running transactions to set + */ + public void setTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) + { + this.retryingTransactionHelper = retryingTransactionHelper; + } + + + /** + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + final List> users = new ArrayList>(); + final ResourceBundle rb = getResources(); + + + // Try to load the user details from the upload + FormData form = (FormData)req.parseContent(); + if (form == null || !form.getIsMultiPart()) + { + throw new ResourceBundleWebScriptException(Status.STATUS_BAD_REQUEST, rb, ERROR_BAD_FORM); + } + + boolean processed = false; + for (FormData.FormField field : form.getFields()) + { + if (field.getIsFile()) + { + processUpload( + field.getInputStream(), + field.getFilename(), + users); + processed = true; + break; + } + } + + if (!processed) + { + throw new ResourceBundleWebScriptException(Status.STATUS_BAD_REQUEST, rb, ERROR_NO_FILE); + } + + // Should we send emails? + boolean sendEmails = true; + if (req.getParameter("email") != null) + { + sendEmails = Boolean.parseBoolean(req.getParameter("email")); + } + + if (form.hasField("email")) + { + sendEmails = Boolean.parseBoolean(form.getParameters().get("email")[0]); + } + + // Now process the users + final MutableInt totalUsers = new MutableInt(0); + final MutableInt addedUsers = new MutableInt(0); + final Map results = new HashMap(); + final boolean doSendEmails = sendEmails; + + // Do the work in a new transaction, so that if we hit a problem + // during the commit stage (eg too many users) then we get to + // hear about it, and handle it ourselves. + // Otherwise, commit exceptions occur deep inside RepositoryContainer + // and we can't control the status code + RetryingTransactionCallback work = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + try + { + doAddUsers(totalUsers, addedUsers, results, users, rb, doSendEmails); + return null; + } + catch (Throwable t) + { + // Make sure we rollback from this + UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); + if (userTrx != null && userTrx.getStatus() != javax.transaction.Status.STATUS_MARKED_ROLLBACK) + { + try + { + userTrx.setRollbackOnly(); + } + catch (Throwable t2) {} + } + // Report the problem further down + throw t; + } + } + }; + + try + { + retryingTransactionHelper.doInTransaction(work); + } + catch (Throwable t) + { + // Tell the client of the problem + if (t instanceof WebScriptException) + { + // We've already wrapped it properly, all good + throw (WebScriptException)t; + } + else + { + // Something unexpected has ripped up + // Return the details with a 200, so that Share does the right thing + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, rb, ERROR_GENERAL, t); + } + } + + + // If we get here, then adding the users didn't throw any exceptions, + // so tell the client which users went in and which didn't + Map model = new HashMap(); + model.put("totalUsers", totalUsers); + model.put("addedUsers", addedUsers); + model.put("users", results); + + return model; + } + + private void doAddUsers(final MutableInt totalUsers, final MutableInt addedUsers, + final Map results, final List> users, + final ResourceBundle rb, final boolean sendEmails) + { + for (Map user : users) + { + totalUsers.setValue( totalUsers.intValue()+1 ); + + // Grab the username, and do any MT magic on it + String username = user.get(ContentModel.PROP_USERNAME); + try + { + username = PersonServiceImpl.updateUsernameForTenancy(username, tenantService); + } + catch (TenantDomainMismatchException e) + { + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, rb, + ERROR_GENERAL, e); + } + + // Do they already exist? + if (personService.personExists(username)) + { + results.put(username, rb.getString(MSG_EXISTING)); + if (logger.isDebugEnabled()) + { + logger.debug("Not creating user as already exists: " + username + " for " + user); + } + } + else + { + String password = user.get(ContentModel.PROP_PASSWORD); + user.remove(ContentModel.PROP_PASSWORD); // Not for the person service + + try + { + // Add the person + personService.createPerson( (Map)(Map)user ); + + // Add the underlying user + authenticationService.createAuthentication(username, password.toCharArray()); + + // If required, notify the user + if (sendEmails) + { + personService.notifyPerson(username, password); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Creating user from upload: " + username + " for " + user); + } + + // All done + String msg = MessageFormat.format( + rb.getString(MSG_CREATED), + new Object[] { user.get(ContentModel.PROP_EMAIL) }); + results.put(username, msg); + addedUsers.setValue( addedUsers.intValue()+1 ); + } + catch (Throwable t) + { + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, rb, ERROR_GENERAL, t); + } + } + } + } + + protected void processUpload(InputStream input, String filename, List> users) + { + try + { + if (filename != null && filename.length() > 0) + { + if (filename.endsWith(".csv")) + { + processCSVUpload(input, users); + return; + } + if (filename.endsWith(".xls")) + { + processXLSUpload(input, users); + return; + } + if (filename.endsWith(".xlsx")) + { + processXLSXUpload(input, users); + return; + } + } + + // If in doubt, assume it's probably a .csv + processCSVUpload(input, users); + } + catch (IOException e) + { + // Return the error as a 200 so the user gets a friendly + // display of the error message in share + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, getResources(), + ERROR_CORRUPT_FILE, e); + } + } + + protected void processCSVUpload(InputStream input, List> users) + throws IOException + { + InputStreamReader reader = new InputStreamReader(input, Charset.forName("UTF-8")); + CSVParser csv = new CSVParser(reader, CSVStrategy.EXCEL_STRATEGY); + String[][] data = csv.getAllValues(); + if(data != null && data.length > 0) + { + processSpreadsheetUpload(data, users); + } + } + + protected void processXLSUpload(InputStream input, List> users) + throws IOException + { + Workbook wb = new HSSFWorkbook(input); + processSpreadsheetUpload(wb, users); + } + + protected void processXLSXUpload(InputStream input, List> users) + throws IOException + { + Workbook wb = new XSSFWorkbook(input); + processSpreadsheetUpload(wb, users); + } + + private void processSpreadsheetUpload(Workbook wb, List> users) + throws IOException + { + if (wb.getNumberOfSheets() > 1) + { + logger.info("Uploaded Excel file has " + wb.getNumberOfSheets() + + " sheets, ignoring all except the first one"); + } + + int firstRow = 0; + Sheet s = wb.getSheetAt(0); + DataFormatter df = new DataFormatter(); + + String[][] data = new String[s.getLastRowNum()+1][]; + + // If there is a heading freezepane row, skip it + PaneInformation pane = s.getPaneInformation(); + if (pane != null && pane.isFreezePane() && pane.getHorizontalSplitTopRow() > 0) + { + firstRow = pane.getHorizontalSplitTopRow(); + logger.debug("Skipping excel freeze header of " + firstRow + " rows"); + } + + // Process each row in turn, getting columns up to our limit + for (int row=firstRow; row <= s.getLastRowNum(); row++) + { + Row r = s.getRow(row); + if (r != null) + { + String[] d = new String[COLUMNS.length]; + for (int cn=0; cn> users) + { + // What we consider to be the literal string "user name" when detecting + // if a row is an example/header one or not + // Note - all of these want to be lower case! + List usernameIsUsername = new ArrayList(); + // The English literals + usernameIsUsername.add("username"); + usernameIsUsername.add("user name"); + // And the localised form too if found + PropertyDefinition unPD = dictionaryService.getProperty(ContentModel.PROP_USERNAME); + if (unPD != null) + { + if(unPD.getTitle() != null) + usernameIsUsername.add(unPD.getTitle().toLowerCase()); + if(unPD.getDescription() != null) + usernameIsUsername.add(unPD.getDescription().toLowerCase()); + } + + + // Process the contents of the spreadsheet + for (int lineNumber=0; lineNumber user = new HashMap(); + String[] userData = data[lineNumber]; + + if (userData == null || userData.length == 0 || + (userData.length == 1 && userData[0].trim().length() == 0)) + { + // Empty line, skip + continue; + } + + boolean required = true; + for (int i=0; i i) + { + value = userData[i]; + } + if (value == null || value.length() == 0) + { + if (required) + { + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, getResources(), + ERROR_BLANK_COLUMN, new Object[] { + COLUMNS[i].getLocalName(), (i+1), (lineNumber+1)}); + } + } + else + { + user.put(COLUMNS[i], value); + } + } + + // If no password was given, use their surname + if (!user.containsKey(ContentModel.PROP_PASSWORD)) + { + user.put(ContentModel.PROP_PASSWORD, user.get(ContentModel.PROP_LASTNAME)); + } + + // Skip any user who looks like an example file heading + // i.e. a username of "username" or "user name" + String username = user.get(ContentModel.PROP_USERNAME).toLowerCase(); + if (usernameIsUsername.contains(username)) + { + // Skip + } + else + { + // Looks like a real line, keep it + users.add(user); + } + } + } + + protected static class ResourceBundleWebScriptException extends WebScriptException + { + private String message; + + public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId, Object... args) + { + super(status, msgId, args); + buildMessageIfPossible(rb, msgId, args); + } + + public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId) + { + super(status, msgId); + buildMessageIfPossible(rb, msgId, new Object[0]); + } + + public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId, Throwable cause) + { + super(status, msgId, cause); + buildMessageIfPossible(rb, msgId, new Object[0]); + } + + public String getMessage() + { + return message; + } + + private void buildMessageIfPossible(ResourceBundle rb, String msgId, Object... args) + { + String msg = rb.getString(msgId); + if (msg != null) + { + if (args == null || args.length == 0) + { + message = msg; + } + else + { + message = MessageFormat.format(msg, args); + } + } + if (getCause() != null) + { + message = message + "\n" + getCause().getMessage(); + } + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/rating/RatingDelete.java b/source/java/org/alfresco/repo/web/scripts/rating/RatingDelete.java index bc80e2d6eb..6ca5cc1972 100644 --- a/source/java/org/alfresco/repo/web/scripts/rating/RatingDelete.java +++ b/source/java/org/alfresco/repo/web/scripts/rating/RatingDelete.java @@ -38,6 +38,10 @@ import org.springframework.extensions.webscripts.WebScriptRequest; */ public class RatingDelete extends AbstractRatingWebScript { + private final static String AVERAGE_RATING = "averageRating"; + private final static String RATINGS_TOTAL = "ratingsTotal"; + private final static String RATINGS_COUNT = "ratingsCount"; + @Override protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) { @@ -55,7 +59,9 @@ public class RatingDelete extends AbstractRatingWebScript } model.put(NODE_REF, nodeRef.toString()); - model.put("rating", deletedRating); + model.put(AVERAGE_RATING, ratingService.getAverageRating(nodeRef, ratingSchemeName)); + model.put(RATINGS_TOTAL, ratingService.getTotalRating(nodeRef, ratingSchemeName)); + model.put(RATINGS_COUNT, ratingService.getRatingsCount(nodeRef, ratingSchemeName)); return model; } diff --git a/source/java/org/alfresco/repo/web/scripts/rating/RatingPost.java b/source/java/org/alfresco/repo/web/scripts/rating/RatingPost.java index 842b8d387b..6d806e61a5 100644 --- a/source/java/org/alfresco/repo/web/scripts/rating/RatingPost.java +++ b/source/java/org/alfresco/repo/web/scripts/rating/RatingPost.java @@ -44,6 +44,10 @@ public class RatingPost extends AbstractRatingWebScript // Url format private final static String NODE_RATINGS_URL_FORMAT = "/api/node/{0}/ratings"; + private final static String AVERAGE_RATING = "averageRating"; + private final static String RATINGS_TOTAL = "ratingsTotal"; + private final static String RATINGS_COUNT = "ratingsCount"; + @Override protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) { @@ -87,6 +91,9 @@ public class RatingPost extends AbstractRatingWebScript model.put(RATED_NODE, ratedNodeUrl); model.put(RATING, rating); model.put(RATING_SCHEME, schemeName); + model.put(AVERAGE_RATING, ratingService.getAverageRating(nodeRefToBeRated, schemeName)); + model.put(RATINGS_TOTAL, ratingService.getTotalRating(nodeRefToBeRated, schemeName)); + model.put(RATINGS_COUNT, ratingService.getRatingsCount(nodeRefToBeRated, schemeName)); } catch (IOException iox) { diff --git a/source/java/org/alfresco/repo/web/scripts/rating/RatingRestApiTest.java b/source/java/org/alfresco/repo/web/scripts/rating/RatingRestApiTest.java index 9a901b8d16..c5724f2c94 100644 --- a/source/java/org/alfresco/repo/web/scripts/rating/RatingRestApiTest.java +++ b/source/java/org/alfresco/repo/web/scripts/rating/RatingRestApiTest.java @@ -152,9 +152,11 @@ public class RatingRestApiTest extends BaseWebScriptTest assertEquals(LIKES_RATING_SCHEME, scheme1.getString(NAME)); assertEquals(1.0, scheme1.getDouble(MIN_RATING)); assertEquals(1.0, scheme1.getDouble(MAX_RATING)); + assertTrue(scheme1.getBoolean("selfRatingAllowed")); assertEquals(FIVE_STAR_RATING_SCHEME, scheme2.getString(NAME)); assertEquals(1.0, scheme2.getDouble(MIN_RATING)); assertEquals(5.0, scheme2.getDouble(MAX_RATING)); + assertFalse(scheme2.getBoolean("selfRatingAllowed")); } public void testGetRatingsFromUnratedNodeRef() throws Exception @@ -171,8 +173,8 @@ public class RatingRestApiTest extends BaseWebScriptTest assertNotNull("JSON 'data' object was null", dataObj); assertEquals(testNode.toString(), dataObj.getString(NODE_REF)); - final JSONArray ratingsArray = dataObj.getJSONArray(RATINGS); - assertEquals(0, ratingsArray.length()); + final JSONObject ratingsObject = dataObj.getJSONObject(RATINGS); + assertEquals(0, ratingsObject.length()); // Unrated content JSONObject statsObject = dataObj.getJSONObject(NODE_STATISTICS); @@ -180,6 +182,11 @@ public class RatingRestApiTest extends BaseWebScriptTest assertEquals("Average rating was wrong.", -1.0, likesStats.getDouble(AVERAGE_RATING)); assertEquals("Ratings count rating was wrong.", 0, likesStats.getInt(RATINGS_COUNT)); assertEquals("Ratings total was wrong.", 0.0, likesStats.getDouble(RATINGS_TOTAL)); + + JSONObject fiveStarStats = statsObject.getJSONObject(FIVE_STAR_RATING_SCHEME); + assertEquals("Average rating was wrong.", -1.0, fiveStarStats.getDouble(AVERAGE_RATING)); + assertEquals("Ratings count rating was wrong.", 0, fiveStarStats.getInt(RATINGS_COUNT)); + assertEquals("Ratings total was wrong.", 0.0, fiveStarStats.getDouble(RATINGS_TOTAL)); } /** @@ -201,13 +208,13 @@ public class RatingRestApiTest extends BaseWebScriptTest .endObject() .toString(); - Response rsp = sendRequest(new PostRequest(testNodeRatingUrl, + Response postRsp = sendRequest(new PostRequest(testNodeRatingUrl, jsonString, APPLICATION_JSON), 200); - String rspContent = rsp.getContentAsString(); + String postRspString = postRsp.getContentAsString(); // Get the returned URL and validate - JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + JSONObject jsonRsp = new JSONObject(new JSONTokener(postRspString)); JSONObject dataObj = (JSONObject)jsonRsp.get(DATA); assertNotNull("JSON 'data' object was null", dataObj); @@ -215,21 +222,34 @@ public class RatingRestApiTest extends BaseWebScriptTest assertEquals(testNodeRatingUrl, returnedUrl); assertEquals(FIVE_STAR_RATING_SCHEME, dataObj.getString("ratingScheme")); assertEquals(userOneRatingValue, (float)dataObj.getDouble("rating")); + assertEquals(userOneRatingValue, (float)dataObj.getDouble("averageRating")); + assertEquals(userOneRatingValue, (float)dataObj.getDouble("ratingsTotal")); + assertEquals(1, dataObj.getInt("ratingsCount")); + + // And a second rating + jsonString = new JSONStringer().object() + .key("rating").value(1) + .key("ratingScheme").value(LIKES_RATING_SCHEME) + .endObject() + .toString(); + + sendRequest(new PostRequest(testNodeRatingUrl, jsonString, APPLICATION_JSON), 200); + // Now GET the ratings via that returned URL - rsp = sendRequest(new GetRequest(testNodeRatingUrl), 200); + Response getRsp = sendRequest(new GetRequest(testNodeRatingUrl), 200); + String getRspString = getRsp.getContentAsString(); - jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + jsonRsp = new JSONObject(new JSONTokener(getRspString)); dataObj = (JSONObject)jsonRsp.get(DATA); assertNotNull("JSON 'data' object was null", dataObj); - // There should only be the one rating in there. - final JSONArray ratingsArray = dataObj.getJSONArray(RATINGS); - assertEquals(1, ratingsArray.length()); - JSONObject recoveredRating = (JSONObject)ratingsArray.get(0); + // There should be two ratings in there. + final JSONObject ratingsObject = dataObj.getJSONObject(RATINGS); + assertEquals(2, ratingsObject.length()); + JSONObject recoveredRating = ratingsObject.getJSONObject(FIVE_STAR_RATING_SCHEME); assertEquals(userOneRatingValue, (float)recoveredRating.getDouble("rating")); - assertEquals(FIVE_STAR_RATING_SCHEME, recoveredRating.getString("ratingScheme")); // As well as the average, total ratings. JSONObject statsObject = dataObj.getJSONObject(NODE_STATISTICS); @@ -237,6 +257,11 @@ public class RatingRestApiTest extends BaseWebScriptTest assertEquals("Average rating was wrong.", userOneRatingValue, (float)fiveStarStats.getDouble(AVERAGE_RATING)); assertEquals("Ratings count rating was wrong.", 1, fiveStarStats.getInt(RATINGS_COUNT)); assertEquals("Ratings total was wrong.", userOneRatingValue, (float)fiveStarStats.getDouble(RATINGS_TOTAL)); + + JSONObject likesStats = statsObject.getJSONObject(LIKES_RATING_SCHEME); + assertEquals("Average rating was wrong.", 1f, (float)likesStats.getDouble(AVERAGE_RATING)); + assertEquals("Ratings count rating was wrong.", 1, likesStats.getInt(RATINGS_COUNT)); + assertEquals("Ratings total was wrong.", 1f, (float)likesStats.getDouble(RATINGS_TOTAL)); // Now POST a second new rating to the testNode - as User Two. @@ -249,32 +274,36 @@ public class RatingRestApiTest extends BaseWebScriptTest .endObject() .toString(); - rsp = sendRequest(new PostRequest(testNodeRatingUrl, + postRsp = sendRequest(new PostRequest(testNodeRatingUrl, jsonString, APPLICATION_JSON), 200); - rspContent = rsp.getContentAsString(); + postRspString = postRsp.getContentAsString(); // Get the returned URL and validate - jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + jsonRsp = new JSONObject(new JSONTokener(postRspString)); dataObj = (JSONObject)jsonRsp.get(DATA); assertNotNull("JSON 'data' object was null", dataObj); returnedUrl = dataObj.getString("ratedNodeUrl"); - // Again GET the ratings via that returned URL - rsp = sendRequest(new GetRequest(returnedUrl), 200); + assertEquals((userOneRatingValue + userTwoRatingValue) / 2, (float)dataObj.getDouble("averageRating")); + assertEquals(userOneRatingValue + userTwoRatingValue, (float)dataObj.getDouble("ratingsTotal")); + assertEquals(2, dataObj.getInt("ratingsCount")); - jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + // Again GET the ratings via that returned URL + getRsp = sendRequest(new GetRequest(returnedUrl), 200); + getRspString = getRsp.getContentAsString(); + + jsonRsp = new JSONObject(new JSONTokener(getRspString)); dataObj = (JSONObject)jsonRsp.get(DATA); assertNotNull("JSON 'data' object was null", dataObj); // There should still only be the one rating in the results - because we're running // as UserTwo and should not see UserOne's rating. - final JSONArray userTwoRatingsArray = dataObj.getJSONArray(RATINGS); - assertEquals(1, userTwoRatingsArray.length()); - JSONObject secondRating = (JSONObject)userTwoRatingsArray.get(0); + final JSONObject userTwoRatings = dataObj.getJSONObject(RATINGS); + assertEquals(1, userTwoRatings.length()); + JSONObject secondRating = (JSONObject)userTwoRatings.getJSONObject(FIVE_STAR_RATING_SCHEME); assertEquals(userTwoRatingValue, (float)secondRating.getDouble("rating")); - assertEquals(FIVE_STAR_RATING_SCHEME, secondRating.getString("ratingScheme")); // Now the average should have changed. statsObject = dataObj.getJSONObject(NODE_STATISTICS); @@ -287,22 +316,20 @@ public class RatingRestApiTest extends BaseWebScriptTest // Now DELETE user two's rating. - // Now POST a second new rating to the testNode - as User Two. AuthenticationUtil.setFullyAuthenticatedUser(USER_TWO); - - rsp = sendRequest(new DeleteRequest(testNodeRatingUrl + "/" + FIVE_STAR_RATING_SCHEME), 200); - rspContent = rsp.getContentAsString(); + sendRequest(new DeleteRequest(testNodeRatingUrl + "/" + FIVE_STAR_RATING_SCHEME), 200); // GET the ratings again. Although user_one's rating will still be there, // user two can't see it and so we should see zero ratings. - rsp = sendRequest(new GetRequest(returnedUrl), 200); + getRsp = sendRequest(new GetRequest(returnedUrl), 200); + getRspString = getRsp.getContentAsString(); - jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + jsonRsp = new JSONObject(new JSONTokener(getRspString)); dataObj = (JSONObject)jsonRsp.get(DATA); assertNotNull("JSON 'data' object was null", dataObj); - final JSONArray remainingRatings = dataObj.getJSONArray(RATINGS); + final JSONObject remainingRatings = dataObj.getJSONObject(RATINGS); assertEquals(0, remainingRatings.length()); // Now the average should have changed. diff --git a/source/java/org/alfresco/repo/web/scripts/replication/ReplicationRestApiTest.java b/source/java/org/alfresco/repo/web/scripts/replication/ReplicationRestApiTest.java index 31493d0672..b2c3c35a2f 100644 --- a/source/java/org/alfresco/repo/web/scripts/replication/ReplicationRestApiTest.java +++ b/source/java/org/alfresco/repo/web/scripts/replication/ReplicationRestApiTest.java @@ -57,6 +57,7 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.Response; */ public class ReplicationRestApiTest extends BaseWebScriptTest { + private static final String URL_REPLICATION_SERVICE_STATUS = "/api/replication-service-status"; private static final String URL_DEFINITION = "api/replication-definition/"; private static final String URL_DEFINITIONS = "api/replication-definitions"; private static final String URL_RUNNING_ACTION = "api/running-action/"; @@ -74,6 +75,23 @@ public class ReplicationRestApiTest extends BaseWebScriptTest private Repository repositoryHelper; private NodeRef dataDictionary; + /** + * @since 3.5 + */ + public void testReplicationServiceIsEnabled() throws Exception + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + Response response = sendRequest(new GetRequest(URL_REPLICATION_SERVICE_STATUS), 200); + assertEquals(Status.STATUS_OK, response.getStatus()); + + String jsonStr = response.getContentAsString(); + JSONObject json = new JSONObject(jsonStr); + JSONObject data = json.getJSONObject("data"); + assertNotNull(data); + assertTrue("ReplicationService was unexpectedly disabled.", data.getBoolean(ReplicationServiceStatusGet.ENABLED)); + } + public void testReplicationDefinitionsGet() throws Exception { Response response; diff --git a/source/java/org/alfresco/repo/web/scripts/replication/ReplicationServiceStatusGet.java b/source/java/org/alfresco/repo/web/scripts/replication/ReplicationServiceStatusGet.java new file mode 100644 index 0000000000..98893568bf --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/replication/ReplicationServiceStatusGet.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.replication; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.replication.ReplicationService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Neil Mc Erlean + * @since 3.5 + */ +public class ReplicationServiceStatusGet extends DeclarativeWebScript +{ + public static final String ENABLED = "enabled"; + + private ReplicationService replicationService; + + /** + * Used to inject the {@link ReplicationService}. + * + * @param replicationService + */ + public void setReplicationService(ReplicationService replicationService) + { + this.replicationService = replicationService; + } + + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + boolean serviceEnabled = replicationService.isEnabled(); + + model.put(ENABLED, serviceEnabled); + + return model; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/servlet/BasicHttpAuthenticatorFactory.java b/source/java/org/alfresco/repo/web/scripts/servlet/BasicHttpAuthenticatorFactory.java index 7775667b14..f82089abb0 100644 --- a/source/java/org/alfresco/repo/web/scripts/servlet/BasicHttpAuthenticatorFactory.java +++ b/source/java/org/alfresco/repo/web/scripts/servlet/BasicHttpAuthenticatorFactory.java @@ -22,7 +22,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.web.util.auth.Authorization; import org.alfresco.service.cmr.security.AuthenticationService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.extensions.surf.util.Base64; import org.springframework.extensions.webscripts.Authenticator; import org.springframework.extensions.webscripts.WebScriptException; @@ -30,8 +33,6 @@ import org.springframework.extensions.webscripts.Description.RequiredAuthenticat import org.springframework.extensions.webscripts.servlet.ServletAuthenticatorFactory; import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; import org.springframework.extensions.webscripts.servlet.WebScriptServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; /** * HTTP Basic Authentication @@ -153,27 +154,26 @@ public class BasicHttpAuthenticatorFactory implements ServletAuthenticatorFactor { throw new WebScriptException("Authorization '" + authorizationParts[0] + "' not supported."); } - String decodedAuthorisation = new String(Base64.decode(authorizationParts[1])); - String[] parts = decodedAuthorisation.split(":"); - if (parts.length == 1) + String decodedAuthorisation = new String(Base64.decode(authorizationParts[1])); + Authorization auth = new Authorization(decodedAuthorisation); + if (auth.isTicket()) { if (logger.isDebugEnabled()) - logger.debug("Authenticating (BASIC HTTP) ticket " + parts[0]); + logger.debug("Authenticating (BASIC HTTP) ticket " + auth.getTicket()); // assume a ticket has been passed - authenticationService.validate(parts[0]); + authenticationService.validate(auth.getTicket()); authorized = true; } else { if (logger.isDebugEnabled()) - logger.debug("Authenticating (BASIC HTTP) user " + parts[0]); + logger.debug("Authenticating (BASIC HTTP) user " + auth.getUserName()); - String username = parts[0]; // No longer need a special call to authenticate as guest // Leave guest name resolution up to the services - authenticationService.authenticate(username, parts[1].toCharArray()); + authenticationService.authenticate(auth.getUserName(), auth.getPassword().toCharArray()); authorized = true; } } diff --git a/source/java/org/alfresco/repo/web/scripts/site/SiteExportGet.java b/source/java/org/alfresco/repo/web/scripts/site/SiteExportGet.java new file mode 100644 index 0000000000..357c76c3d1 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/site/SiteExportGet.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2005-2011 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 . + */ +package org.alfresco.repo.web.scripts.site; + +import java.io.File; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.exporter.ACPExportPackageHandler; +import org.alfresco.repo.management.subsystems.ChildApplicationContextManager; +import org.alfresco.repo.security.authentication.RepositoryAuthenticationDao; +import org.alfresco.service.cmr.avm.AVMNodeDescriptor; +import org.alfresco.service.cmr.avm.AVMService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.view.AVMZipExporterService; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.Location; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipOutputStream; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Exports a Site as a zip of ACPs and an AVM dump + * + * @author Nick Burch + * @since 3.5 + */ +public class SiteExportGet extends AbstractWebScript +{ + private static final String SITE_AVM_ROOT = "sitestore:/alfresco/site-data/"; + private static final List USERS_NOT_TO_EXPORT = Arrays.asList( + new String[] { "admin", "guest" }); + + private AVMZipExporterService avmZipExporterService; + private AVMService avmService; + private SiteService siteService; + private ExporterService exporterService; + private MimetypeService mimetypeService; + private AuthorityService authorityService; + private ChildApplicationContextManager authenticationContextManager; + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + // Grab the site + String siteName = + req.getServiceMatch().getTemplateVars().get("shortname"); + SiteInfo site = siteService.getSite(siteName); + if (site == null) + { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Site found with that short name"); + } + + // Set things up to return them a zip file + res.setContentType( + MimetypeMap.MIMETYPE_ZIP); + + res.setHeader( + "Content-Disposition", + "attachment; fileName=" + siteName + "-export.zip"); + + ZipOutputStream mainZip = new ZipOutputStream( + res.getOutputStream()); + CloseIgnoringOutputStream outputForNesting = + new CloseIgnoringOutputStream(mainZip); + + // Export the AVM details (dashboard etc) + mainZip.putNextEntry(new ZipEntry("AVM.zip")); + doAVMExport(site, outputForNesting); + + // Export the Site's Contents + mainZip.putNextEntry(new ZipEntry("Contents.acp")); + doSiteACPExport(site, outputForNesting); + + // Export the users who are members of the site's groups + // Also includes the list of their site related groups + mainZip.putNextEntry(new ZipEntry("People.acp")); + doPeopleACPExport(site, outputForNesting); + + // Export the Site's groups listings + mainZip.putNextEntry(new ZipEntry("Groups.txt")); + doGroupExport(site, outputForNesting); + + // Export the User (authentication) details of those people + // Only does this if the repository based authenticator is enabled + RepositoryAuthenticationDao authenticationDao = null; + for(String contextName : authenticationContextManager.getInstanceIds()) + { + ApplicationContext ctx = authenticationContextManager.getApplicationContext(contextName); + try + { + authenticationDao = (RepositoryAuthenticationDao) + ctx.getBean(RepositoryAuthenticationDao.class); + } catch(NoSuchBeanDefinitionException e) {} + } + if (authenticationDao == null) + { + mainZip.putNextEntry(new ZipEntry("Users_Skipped_As_Wrong_Authentication.txt")); + String text = "Users were not exported because the Authentication\n"+ + "Subsystem you are using is not repository based"; + outputForNesting.write(text.getBytes("ASCII")); + } + else + { + mainZip.putNextEntry(new ZipEntry("Users.acp")); + doUserACPExport(site, outputForNesting, authenticationDao); + } + + // Finish up + mainZip.close(); + } + + protected void doAVMExport(SiteInfo site, CloseIgnoringOutputStream writeTo) throws IOException + { + String pageRoot = SITE_AVM_ROOT + "pages/site/" + site.getShortName(); + String componentRoot = SITE_AVM_ROOT + "components"; + String componentMatch = "~" + site.getShortName() + "~"; + + // Nest the zip + ZipOutputStream zip = new ZipOutputStream(writeTo); + + // First up, do the page info + avmZipExporterService.export(zip, -1, pageRoot, true); + + // Now do the components + for (AVMNodeDescriptor node : avmService.getDirectoryListing(-1, componentRoot).values()) + { + if (node.getName().contains(componentMatch)) + { + avmZipExporterService.export(zip, node, false); + } + } + + // Finish the AVM zip + zip.close(); + } + + protected void doSiteACPExport(SiteInfo site, CloseIgnoringOutputStream writeTo) throws IOException + { + // Build the parameters + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(new Location(site.getNodeRef())); + parameters.setCrawlChildNodes(true); + parameters.setCrawlSelf(true); + parameters.setCrawlContent(true); + + // And the export handler + ACPExportPackageHandler handler = new ACPExportPackageHandler( + writeTo, + new File(site.getShortName() + ".xml"), + new File(site.getShortName()), + mimetypeService); + + // Do the export + exporterService.exportView(handler, parameters, null); + } + + protected void doPeopleACPExport(SiteInfo site, CloseIgnoringOutputStream writeTo) throws IOException + { + // Find the root group + String siteGroup = buildSiteGroup(site); + + // Now get all people in the child groups + Set siteUsers = authorityService.findAuthorities( + AuthorityType.USER, siteGroup, false, null, null); + + // Turn these all into NodeRefs + List peopleNodes = new ArrayList(siteUsers.size()); + for (String authority : siteUsers) + { + if (!USERS_NOT_TO_EXPORT.contains(authority)) + { + peopleNodes.add(authorityService.getAuthorityNodeRef(authority)); + } + } + + + // Build the parameters + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(new Location(peopleNodes.toArray(new NodeRef[peopleNodes.size()]))); + parameters.setCrawlChildNodes(true); + parameters.setCrawlSelf(true); + parameters.setCrawlContent(true); + + // And the export handler + ACPExportPackageHandler handler = new ACPExportPackageHandler( + writeTo, + new File(site.getShortName() + "-people.xml"), + new File(site.getShortName() + "-people"), + mimetypeService); + + // Do the export + exporterService.exportView(handler, parameters, null); + } + + protected void doGroupExport(SiteInfo site, CloseIgnoringOutputStream writeTo) throws IOException + { + // Find the root group + String siteGroup = buildSiteGroup(site); + + // Get all the child groups of the site (but not children of them) + Set siteGroups = authorityService.findAuthorities( + AuthorityType.GROUP, siteGroup, true, null, null); + + // For each group, get all the people + // (Flattens any intermediate groups) + // Then, invert it to get the groups per person + Map> memberships = new HashMap>(); + for(String group : siteGroups) + { + Set groupUsers = authorityService.findAuthorities( + AuthorityType.USER, group, false, null, null); + + for (String user : groupUsers) + { + if (!USERS_NOT_TO_EXPORT.contains(user)) + { + if (!memberships.containsKey(user)) + { + memberships.put(user, new ArrayList()); + } + memberships.get(user).add(group); + } + } + } + + // Do a simple text based export + // user=group1,group2 + PrintWriter out = new PrintWriter(new OutputStreamWriter(writeTo, "UTF-8")); + for (String user : memberships.keySet()) + { + out.print(user); + out.print('='); + + boolean first = true; + for (String group : memberships.get(user)) + { + if (first) + { + first = false; + } + else + { + out.print(','); + } + out.print(group); + } + out.println(); + } + out.close(); + } + + protected void doUserACPExport(SiteInfo site, CloseIgnoringOutputStream writeTo, + RepositoryAuthenticationDao authenticationDao) throws IOException + { + List exportNodes = new ArrayList(); + + // Identify all the users + String siteGroup = buildSiteGroup(site); + Set siteUsers = authorityService.findAuthorities( + AuthorityType.USER, siteGroup, false, null, null); + + // Now export them, and only them + for (String user : siteUsers) + { + if (USERS_NOT_TO_EXPORT.contains(user)) + { + // Don't export these core users like admin + } + else + { + //NodeRef personNodeRef = authorityService.getAuthorityNodeRef(user); + NodeRef userNodeRef = authenticationDao.getUserOrNull(user); + exportNodes.add(userNodeRef); + } + } + + // Build the parameters + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(new Location(exportNodes.toArray(new NodeRef[exportNodes.size()]))); + parameters.setCrawlChildNodes(true); + parameters.setCrawlSelf(true); + parameters.setCrawlContent(true); + + // And the export handler + ACPExportPackageHandler handler = new ACPExportPackageHandler( + writeTo, + new File(site.getShortName() + "-users.xml"), + new File(site.getShortName() + "-users"), + mimetypeService); + + // Do the export + exporterService.exportView(handler, parameters, null); + } + + protected String buildSiteGroup(SiteInfo site) + { + return "GROUP_site_" + site.getShortName(); + } + + protected static class CloseIgnoringOutputStream extends FilterOutputStream + { + public CloseIgnoringOutputStream(OutputStream out) + { + super(out); + } + + @Override + public void close() throws IOException + { + // Flushes, but doesn't close + flush(); + } + } + + public void setAvmZipExporterService(AVMZipExporterService avmZipExporterService) + { + this.avmZipExporterService = avmZipExporterService; + } + + public void setAvmService(AVMService avmService) + { + this.avmService = avmService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setExporterService(ExporterService exporterService) + { + this.exporterService = exporterService; + } + + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setAuthenticationContextManager(ChildApplicationContextManager authenticationContextManager) + { + this.authenticationContextManager = authenticationContextManager; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/site/SiteServiceTest.java b/source/java/org/alfresco/repo/web/scripts/site/SiteServiceTest.java index 2d6b7681f3..4b6ed636f9 100644 --- a/source/java/org/alfresco/repo/web/scripts/site/SiteServiceTest.java +++ b/source/java/org/alfresco/repo/web/scripts/site/SiteServiceTest.java @@ -171,7 +171,8 @@ public class SiteServiceTest extends BaseWebScriptTest Response response = sendRequest(new GetRequest(URL_SITES), 200); JSONArray result = new JSONArray(response.getContentAsString()); assertNotNull(result); - assertEquals("Sites exist prior to running test", 0, result.length()); + int sitesBefore = result.length(); + assertTrue("There should be at least one site present", sitesBefore > 0); createSite("myPreset", GUID.generate(), "myTitle", "myDescription", SiteVisibility.PUBLIC, 200); createSite("myPreset", GUID.generate(), "myTitle", "myDescription", SiteVisibility.PUBLIC, 200); @@ -182,7 +183,7 @@ public class SiteServiceTest extends BaseWebScriptTest response = sendRequest(new GetRequest(URL_SITES), 200); result = new JSONArray(response.getContentAsString()); assertNotNull(result); - assertEquals(5, result.length()); + assertEquals(5 + sitesBefore, result.length()); response = sendRequest(new GetRequest(URL_SITES + "?size=3"), 200); result = new JSONArray(response.getContentAsString()); @@ -192,7 +193,7 @@ public class SiteServiceTest extends BaseWebScriptTest response = sendRequest(new GetRequest(URL_SITES + "?size=13"), 200); result = new JSONArray(response.getContentAsString()); assertNotNull(result); - assertEquals(5, result.length()); + assertEquals(5 + sitesBefore, result.length()); } /** @@ -390,7 +391,7 @@ public class SiteServiceTest extends BaseWebScriptTest // Update the role by returning the data. newMember.put("role", SiteModel.SITE_COLLABORATOR); - response = sendRequest(new PutRequest(URL_SITES + "/" + shortName + URL_MEMBERSHIPS + "/" + USER_TWO, newMember.toString(), "application/json"), 200); + response = sendRequest(new PutRequest(URL_SITES + "/" + shortName + URL_MEMBERSHIPS, newMember.toString(), "application/json"), 200); JSONObject result = new JSONObject(response.getContentAsString()); // Check the result @@ -446,7 +447,7 @@ public class SiteServiceTest extends BaseWebScriptTest // Now send the returned value back with a new role (COLLABORATOR) newMember.put("role", SiteModel.SITE_COLLABORATOR); - response = sendRequest(new PutRequest(URL_SITES + "/" + shortName + URL_MEMBERSHIPS + "/" + USER_TWO, newMember.toString(), "application/json"), 200); + response = sendRequest(new PutRequest(URL_SITES + "/" + shortName + URL_MEMBERSHIPS, newMember.toString(), "application/json"), 200); JSONObject updateResult = new JSONObject(response.getContentAsString()); assertEquals("role not correct", SiteModel.SITE_COLLABORATOR, updateResult.getString("role")); diff --git a/source/java/org/alfresco/repo/web/scripts/transfer/BeginTransferCommandProcessor.java b/source/java/org/alfresco/repo/web/scripts/transfer/BeginTransferCommandProcessor.java index fb201f7b04..53458d85a2 100644 --- a/source/java/org/alfresco/repo/web/scripts/transfer/BeginTransferCommandProcessor.java +++ b/source/java/org/alfresco/repo/web/scripts/transfer/BeginTransferCommandProcessor.java @@ -23,8 +23,10 @@ import java.io.StringWriter; import org.alfresco.repo.transfer.RepoTransferReceiverImpl; import org.alfresco.repo.transfer.TransferCommons; +import org.alfresco.repo.transfer.TransferVersionImpl; import org.alfresco.service.cmr.transfer.TransferException; import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.alfresco.service.cmr.transfer.TransferVersion; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.extensions.webscripts.Status; @@ -60,15 +62,19 @@ public class BeginTransferCommandProcessor implements CommandProcessor { String [] fromRepositoryIdValues = req.getParameterValues(TransferCommons.PARAM_FROM_REPOSITORYID); String [] transferToSelfValues = req.getParameterValues(TransferCommons.PARAM_ALLOW_TRANSFER_TO_SELF); - + String [] editionValues = req.getParameterValues(TransferCommons.PARAM_VERSION_EDITION); + String [] majorValues = req.getParameterValues(TransferCommons.PARAM_VERSION_MAJOR); + String [] minorValues = req.getParameterValues(TransferCommons.PARAM_VERSION_MINOR); + String [] revisionValues = req.getParameterValues(TransferCommons.PARAM_VERSION_REVISION); + String fromRepositoryId = null; - if(fromRepositoryIdValues.length > 0) + if(fromRepositoryIdValues != null && fromRepositoryIdValues.length > 0) { fromRepositoryId = fromRepositoryIdValues[0]; } boolean transferToSelf = false; - if(transferToSelfValues.length > 0) + if(transferToSelfValues != null && transferToSelfValues.length > 0) { if(transferToSelfValues[0].equalsIgnoreCase("true")) { @@ -76,18 +82,53 @@ public class BeginTransferCommandProcessor implements CommandProcessor } } + String edition = "Unknown"; + if(editionValues != null && editionValues.length > 0) + { + edition = editionValues[0]; + } + String major = "0"; + if(majorValues != null && majorValues.length > 0) + { + major = majorValues[0]; + } + String minor = "0"; + if(minorValues != null && minorValues.length > 0) + { + minor = minorValues[0]; + } + String revision = "0"; + if(revisionValues != null && revisionValues.length > 0) + { + revision = revisionValues[0]; + } + + TransferVersion fromVersion = new TransferVersionImpl(major, minor, revision, edition); + // attempt to start the transfer - transferId = receiver.start(fromRepositoryId, transferToSelf); + transferId = receiver.start(fromRepositoryId, transferToSelf, fromVersion); // Create a temporary folder into which we can place transferred files receiver.getStagingFolder(transferId); + + TransferVersion version = receiver.getVersion(); // return the unique transfer id (the lock id) - StringWriter stringWriter = new StringWriter(300); + StringWriter stringWriter = new StringWriter(1000); JSONWriter jsonWriter = new JSONWriter(stringWriter); jsonWriter.startObject(); - jsonWriter.writeValue("transferId", transferId); + + jsonWriter.writeValue(TransferCommons.PARAM_TRANSFER_ID, transferId); + + if(version != null) + { + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_EDITION, version.getEdition()); + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_MAJOR, version.getVersionMajor()); + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_MINOR, version.getVersionMinor()); + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_REVISION, version.getVersionRevision()); + } jsonWriter.endObject(); + String response = stringWriter.toString(); resp.setContentType("application/json"); diff --git a/source/java/org/alfresco/repo/web/util/auth/Authorization.java b/source/java/org/alfresco/repo/web/util/auth/Authorization.java new file mode 100644 index 0000000000..a3f6218b78 --- /dev/null +++ b/source/java/org/alfresco/repo/web/util/auth/Authorization.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.util.auth; + +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.util.ParameterCheck; + +/** + * Helper to process username / password pairs passed to the remote tier + * + * Identifies whether username / password is a ticket. + * + * Is ticket, if one of the following is true: + * + * a) Username == "ROLE_TICKET" (in any case) + * b) Username is not specified (i.e. null) + * c) Username is zero length + */ +public class Authorization +{ + public static String TICKET_USERID = PermissionService.ROLE_PREFIX + "TICKET"; + + private String username; + private String password; + private String ticket; + + /** + * Construct + * + * @param authorization + */ + public Authorization(String authorization) + { + ParameterCheck.mandatoryString("authorization", authorization); + String[] parts = authorization.split(":"); + if (parts.length == 1) + { + setUser(null, parts[0]); + } + else if (parts.length == 2) + { + setUser(parts[0], parts[1]); + } + else + { + throw new IllegalArgumentException("authorization does not consist of username and password"); + } + } + + /** + * Construct + * + * @param username + * @param password + */ + public Authorization(String username, String password) + { + setUser(username, password); + } + + private void setUser(String username, String password) + { + this.username = username; + this.password = password; + if (username == null || username.length() == 0 || username.equalsIgnoreCase(TICKET_USERID)) + { + + this.ticket = password; + } + } + + public String getUserName() + { + return username; + } + + public String getPassword() + { + return password; + } + + public boolean isTicket() + { + return ticket != null; + } + + public String getTicket() + { + return ticket; + } + +} diff --git a/source/java/org/alfresco/repo/web/util/auth/AuthorizationTest.java b/source/java/org/alfresco/repo/web/util/auth/AuthorizationTest.java new file mode 100644 index 0000000000..1ffb7d6b45 --- /dev/null +++ b/source/java/org/alfresco/repo/web/util/auth/AuthorizationTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.util.auth; + +import junit.framework.TestCase; + + +/** + * Test Authorization + */ +public class AuthorizationTest extends TestCase +{ + private static String USER = "user"; + private static String PASSWORD = "pass"; + + public void testInvalidAuthorization() + { + try + { + new Authorization(null); + fail(); + } + catch(IllegalArgumentException e) + { + } + try + { + new Authorization("username:password:invalid"); + fail(); + } + catch(IllegalArgumentException e) + { + } + } + + public void testAuthorization() + { + Authorization auth1 = new Authorization(USER, PASSWORD); + assertUserPass(USER, PASSWORD, auth1); + Authorization auth2 = new Authorization("", PASSWORD); + assertTicket("", PASSWORD, auth2); + Authorization auth3 = new Authorization(null, PASSWORD); + assertTicket(null, PASSWORD, auth3); + Authorization auth4 = new Authorization(Authorization.TICKET_USERID, PASSWORD); + assertTicket(Authorization.TICKET_USERID, PASSWORD, auth4); + Authorization auth5 = new Authorization(Authorization.TICKET_USERID.toLowerCase(), PASSWORD); + assertTicket(Authorization.TICKET_USERID.toLowerCase(), PASSWORD, auth5); + } + + public void testUserPass() + { + Authorization auth1 = new Authorization(USER + ":" + PASSWORD); + assertUserPass(USER, PASSWORD, auth1); + Authorization auth2 = new Authorization(":" + PASSWORD); + assertTicket("", PASSWORD, auth2); + Authorization auth3 = new Authorization(PASSWORD); + assertTicket(null, PASSWORD, auth3); + Authorization auth4 = new Authorization(Authorization.TICKET_USERID + ":" + PASSWORD); + assertTicket(Authorization.TICKET_USERID, PASSWORD, auth4); + Authorization auth5 = new Authorization(Authorization.TICKET_USERID.toLowerCase() + ":" + PASSWORD); + assertTicket(Authorization.TICKET_USERID.toLowerCase(), PASSWORD, auth5); + } + + private void assertUserPass(String user, String pass, Authorization auth) + { + assertEquals(user, auth.getUserName()); + assertEquals(pass, auth.getPassword()); + assertFalse(auth.isTicket()); + assertNull(auth.getTicket()); + } + + private void assertTicket(String user, String pass, Authorization auth) + { + assertEquals(user, auth.getUserName()); + assertEquals(pass, auth.getPassword()); + assertTrue(auth.isTicket()); + assertEquals(pass, auth.getTicket()); + } + +} diff --git a/source/java/org/alfresco/repo/webdav/AbstractMoveOrCopyMethod.java b/source/java/org/alfresco/repo/webdav/AbstractMoveOrCopyMethod.java index c2d4169505..0f35708215 100644 --- a/source/java/org/alfresco/repo/webdav/AbstractMoveOrCopyMethod.java +++ b/source/java/org/alfresco/repo/webdav/AbstractMoveOrCopyMethod.java @@ -20,6 +20,7 @@ package org.alfresco.repo.webdav; import javax.servlet.http.HttpServletResponse; +import org.alfresco.model.ContentModel; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; @@ -112,7 +113,9 @@ public abstract class AbstractMoveOrCopyMethod extends HierarchicalMethod try { destInfo = getDAVHelper().getNodeForPath(rootNodeRef, destPath, servletPath); - if (!hasOverWrite()) + // ALF-7079 fix, if destInfo is working copy then content will be updated later + boolean isDestWorkingCopy = getNodeService().hasAspect(destInfo.getNodeRef(), ContentModel.ASPECT_WORKING_COPY); + if (!hasOverWrite() && !isDestWorkingCopy) { if (logger.isDebugEnabled()) { @@ -121,8 +124,8 @@ public abstract class AbstractMoveOrCopyMethod extends HierarchicalMethod // it exists and we may not overwrite throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); } - // delete the destination node if it is not the same as the source node - if (!destInfo.getNodeRef().equals(sourceInfo.getNodeRef())) + // delete the destination node if it is not the same as the source node and not a working copy + if (!destInfo.getNodeRef().equals(sourceInfo.getNodeRef()) && !isDestWorkingCopy) { checkNode(destInfo); diff --git a/source/java/org/alfresco/repo/webdav/DeleteMethod.java b/source/java/org/alfresco/repo/webdav/DeleteMethod.java index 737d0f8518..a38ac6869d 100644 --- a/source/java/org/alfresco/repo/webdav/DeleteMethod.java +++ b/source/java/org/alfresco/repo/webdav/DeleteMethod.java @@ -22,6 +22,7 @@ import java.util.List; import javax.servlet.http.HttpServletResponse; +import org.alfresco.model.ContentModel; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; @@ -96,7 +97,11 @@ public class DeleteMethod extends WebDAVMethod checkNode(fileInfo); - // delete it - fileFolderService.delete(fileInfo.getNodeRef()); + // ALF-7079 fix, working copies are not deleted at all + if (!getNodeService().hasAspect(fileInfo.getNodeRef(), ContentModel.ASPECT_WORKING_COPY)) + { + // delete it + fileFolderService.delete(fileInfo.getNodeRef()); + } } } diff --git a/source/java/org/alfresco/repo/webdav/MTNodesCache.java b/source/java/org/alfresco/repo/webdav/MTNodesCache.java new file mode 100644 index 0000000000..b71d740715 --- /dev/null +++ b/source/java/org/alfresco/repo/webdav/MTNodesCache.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.webdav; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; + +/** + * In-memory cache that stores nodeRefs per tenant. + * It is initialized using path to node and allows to retrieve nodeRef for current tenant. + * + * @author Stas Sokolovsky + */ +public class MTNodesCache +{ + private NodeService nodeService; + + private SearchService searchService; + + private NamespaceService namespaceService; + + private TenantService tenantService; + + private Map nodesCache = new ConcurrentHashMap(); + + private String path = null; + + private NodeRef defaultNode = null; + + /** + * Constructor + * + * @param storeRef Store reference + * @param path Path to node + * @param nodeService NodeService + * @param searchService SearchService + * @param namespaceService NamespaceService + * @param tenantService TenantService + */ + public MTNodesCache(StoreRef storeRef, String path, NodeService nodeService, SearchService searchService, NamespaceService namespaceService, TenantService tenantService) + { + this.nodeService = nodeService; + this.searchService = searchService; + this.namespaceService = namespaceService; + this.tenantService = tenantService; + + if (nodeService.exists(storeRef) == false) + { + throw new RuntimeException("No store for path: " + storeRef); + } + + NodeRef storeRootNodeRef = nodeService.getRootNode(storeRef); + + List nodeRefs = searchService.selectNodes(storeRootNodeRef, path, null, namespaceService, false); + + if (nodeRefs.size() > 1) + { + throw new RuntimeException("Multiple possible children for : \n" + " path: " + path + "\n" + " results: " + nodeRefs); + } + else if (nodeRefs.size() == 0) + { + throw new RuntimeException("Node is not found for : \n" + " root path: " + path); + } + + this.path = path; + defaultNode = nodeRefs.get(0); + + } + + /** + * Returns nodeRef for current user tenant + * + * @return nodeRef Node Reference + */ + public NodeRef getNodeForCurrentTenant() + { + NodeRef result = null; + + if (!tenantService.isEnabled()) + { + result = defaultNode; + } + else + { + String domain = tenantService.getCurrentUserDomain(); + if (nodesCache.containsKey(domain)) + { + result = nodesCache.get(domain); + } + else + { + result = tenantService.getRootNode(nodeService, searchService, namespaceService, path, defaultNode); + nodesCache.put(domain, result); + } + } + return result; + } + +} diff --git a/source/java/org/alfresco/repo/webdav/MoveMethod.java b/source/java/org/alfresco/repo/webdav/MoveMethod.java index cc71d87736..bb0047379f 100644 --- a/source/java/org/alfresco/repo/webdav/MoveMethod.java +++ b/source/java/org/alfresco/repo/webdav/MoveMethod.java @@ -22,10 +22,15 @@ import java.util.List; import javax.servlet.http.HttpServletResponse; +import org.alfresco.model.ContentModel; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; /** * Implements the WebDAV MOVE method @@ -50,13 +55,21 @@ public class MoveMethod extends AbstractMoveOrCopyMethod { NodeRef rootNodeRef = getRootNodeRef(); - String path = getPath(); - List pathElements = getDAVHelper().splitAllPaths(path); - FileInfo fileInfo = null; + String sourcePath = getPath(); + List sourcePathElements = getDAVHelper().splitAllPaths(sourcePath); + FileInfo sourceFileInfo = null; + + String destPath = getDestinationPath(); + List destPathElements = getDAVHelper().splitAllPaths(destPath); + FileInfo destFileInfo = null; + + NodeService nodeService = getNodeService(); + try { // get the node to move - fileInfo = fileFolderService.resolveNamePath(rootNodeRef, pathElements); + sourceFileInfo = fileFolderService.resolveNamePath(rootNodeRef, sourcePathElements); + destFileInfo = fileFolderService.resolveNamePath(rootNodeRef, destPathElements); } catch (FileNotFoundException e) { @@ -64,12 +77,46 @@ public class MoveMethod extends AbstractMoveOrCopyMethod { logger.debug("Node not found: " + getPath()); } - throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + + if (sourceFileInfo == null) + { + // nothing to move + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } } - checkNode(fileInfo); + checkNode(sourceFileInfo); - // It is move operation - fileFolderService.move(sourceNodeRef, sourceParentNodeRef, destParentNodeRef, name); + // ALF-7079 fix, if source is working copy then it is just copied to destination + if (nodeService.hasAspect(sourceNodeRef, ContentModel.ASPECT_WORKING_COPY)) + { + // replace move with copy action for working copies + fileFolderService.copy(sourceNodeRef, destParentNodeRef, name); + } + // ALF-7079 fix, if destination exists and is working copy then its content is updated with + // source content and source is deleted + else if (destFileInfo != null && nodeService.hasAspect(destFileInfo.getNodeRef(), ContentModel.ASPECT_WORKING_COPY)) + { + // copy only content for working copy destination + ContentService contentService = getContentService(); + ContentReader reader = contentService.getReader(sourceNodeRef, ContentModel.PROP_CONTENT); + ContentWriter contentWriter = contentService.getWriter(destFileInfo.getNodeRef(), ContentModel.PROP_CONTENT, true); + contentWriter.putContent(reader); + + fileFolderService.delete(sourceNodeRef); + } + else + { + if (sourceParentNodeRef.equals(destParentNodeRef)) + { + // It is rename method + fileFolderService.rename(sourceNodeRef, name); + } + else + { + // It is move operation + fileFolderService.moveFrom(sourceNodeRef, sourceParentNodeRef, destParentNodeRef, name); + } + } } } diff --git a/source/java/org/alfresco/repo/webdav/PropFindMethod.java b/source/java/org/alfresco/repo/webdav/PropFindMethod.java index 15ff31d9ca..c865a9002e 100644 --- a/source/java/org/alfresco/repo/webdav/PropFindMethod.java +++ b/source/java/org/alfresco/repo/webdav/PropFindMethod.java @@ -163,6 +163,15 @@ public class PropFindMethod extends WebDAVMethod } } + /** + * @return Returns true always + */ + @Override + protected boolean isReadOnly() + { + return true; + } + /** * Execute the main WebDAV request processing * diff --git a/source/java/org/alfresco/repo/webdav/PropPatchMethod.java b/source/java/org/alfresco/repo/webdav/PropPatchMethod.java index abf7230257..3a877256c5 100644 --- a/source/java/org/alfresco/repo/webdav/PropPatchMethod.java +++ b/source/java/org/alfresco/repo/webdav/PropPatchMethod.java @@ -43,7 +43,16 @@ public class PropPatchMethod extends PropFindMethod // Properties to patch protected ArrayList m_propertyActions = null; - + + /** + * @return Returns false always + */ + @Override + protected boolean isReadOnly() + { + return false; + } + @Override protected void executeImpl() throws WebDAVServerException, Exception { diff --git a/source/java/org/alfresco/repo/webdav/PutMethod.java b/source/java/org/alfresco/repo/webdav/PutMethod.java index 0768c40f5f..2deee22791 100644 --- a/source/java/org/alfresco/repo/webdav/PutMethod.java +++ b/source/java/org/alfresco/repo/webdav/PutMethod.java @@ -36,6 +36,7 @@ import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.namespace.QName; +import org.springframework.dao.ConcurrencyFailureException; /** * Implements the WebDAV PUT method @@ -135,8 +136,8 @@ public class PutMethod extends WebDAVMethod } catch (FileExistsException ee) { - // bad path - throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST); + // ALF-7079 fix, retry: it looks like concurrent access (file not found but file exists) + throw new ConcurrencyFailureException("Concurrent access was detected.", ee); } } diff --git a/source/java/org/alfresco/repo/webdav/UnlockMethod.java b/source/java/org/alfresco/repo/webdav/UnlockMethod.java index ecfbe4d9ba..c3b627d8f0 100644 --- a/source/java/org/alfresco/repo/webdav/UnlockMethod.java +++ b/source/java/org/alfresco/repo/webdav/UnlockMethod.java @@ -141,15 +141,17 @@ public class UnlockMethod extends WebDAVMethod LockStatus lockSts = lockService.getLockStatus(lockNodeInfo.getNodeRef()); if (lockSts == LockStatus.LOCK_OWNER) { - // Unlock the node if it is not a Working Copy (ALF-4479) - if (!nodeService.hasAspect(lockNodeInfo.getNodeRef(), ContentModel.ASPECT_WORKING_COPY)) - { - lockService.unlock(lockNodeInfo.getNodeRef()); - } + lockService.unlock(lockNodeInfo.getNodeRef()); nodeService.removeProperty(lockNodeInfo.getNodeRef(), WebDAVModel.PROP_OPAQUE_LOCK_TOKEN); nodeService.removeProperty(lockNodeInfo.getNodeRef(), WebDAVModel.PROP_LOCK_DEPTH); nodeService.removeProperty(lockNodeInfo.getNodeRef(), WebDAVModel.PROP_LOCK_SCOPE); + // Return the cm:lockable aspect to working copy (ALF-4479, ALF-7079) + if (nodeService.hasAspect(lockNodeInfo.getNodeRef(), ContentModel.ASPECT_WORKING_COPY)) + { + nodeService.addAspect(lockNodeInfo.getNodeRef(), ContentModel.ASPECT_LOCKABLE, null); + } + // Indicate that the unlock was successful m_response.setStatus(HttpServletResponse.SC_NO_CONTENT); diff --git a/source/java/org/alfresco/repo/webdav/WebDAVHelper.java b/source/java/org/alfresco/repo/webdav/WebDAVHelper.java index 5fdd63e4f0..e7de1aa565 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVHelper.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVHelper.java @@ -61,7 +61,7 @@ public class WebDAVHelper public static final char PathSeperatorChar = '/'; // Logging - private static Log logger = LogFactory.getLog("org.alfresco.protocol.webdav"); + private static Log logger = LogFactory.getLog("org.alfresco.webdav.protocol"); // Service registry private ServiceRegistry m_serviceRegistry; diff --git a/source/java/org/alfresco/repo/webdav/WebDAVMethod.java b/source/java/org/alfresco/repo/webdav/WebDAVMethod.java index 0213f44d62..b757d513b9 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVMethod.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVMethod.java @@ -28,8 +28,12 @@ import java.io.OutputStream; import java.util.Date; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; @@ -44,6 +48,7 @@ import org.alfresco.model.WebDAVModel; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.lock.LockService; import org.alfresco.service.cmr.lock.LockStatus; import org.alfresco.service.cmr.model.FileFolderService; @@ -86,6 +91,16 @@ public abstract class WebDAVMethod // Output formatted XML in the response private static final boolean XMLPrettyPrint = true; + + // Mapping of User-Agent pattern to response status code + // used to determine which status code should be returned for AccessDeniedException + + private static final Map accessDeniedStatusCodes = new LinkedHashMap(); + static + { + accessDeniedStatusCodes.put("(darwin)|(macintosh)", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + accessDeniedStatusCodes.put(".*", HttpServletResponse.SC_FORBIDDEN); + } // Servlet request/response @@ -253,7 +268,14 @@ public abstract class WebDAVMethod { this.m_requestBody = TempFileProvider.createTempFile("webdav_" + req.getMethod() + "_", ".bin"); OutputStream out = new FileOutputStream(this.m_requestBody); - FileCopyUtils.copy(req.getInputStream(), out); + int bytesRead = FileCopyUtils.copy(req.getInputStream(), out); + + // ALF-7377: check for corrupt request + int contentLength = req.getIntHeader(WebDAV.HEADER_CONTENT_LENGTH); + if (contentLength >= 0 && contentLength != bytesRead) + { + throw new IOException("Request body does not have specified Content Length"); + } } return this.m_requestBody; } @@ -314,7 +336,7 @@ public abstract class WebDAVMethod catch (AccessDeniedException e) { // Return a forbidden status - throw new WebDAVServerException(HttpServletResponse.SC_FORBIDDEN, e); + throw new WebDAVServerException(getStatusForAccessDeniedException(), e); } catch (Throwable e) { @@ -838,7 +860,8 @@ public abstract class WebDAVMethod { if (nodeLockInfo.getToken() == null) { - if (nodeLockInfo.getSharedLockTokens() == null) + CheckOutCheckInService checkOutCheckInService = m_davHelper.getServiceRegistry().getCheckOutCheckInService(); + if (nodeLockInfo.getSharedLockTokens() == null && checkOutCheckInService.getWorkingCopy(fileInfo.getNodeRef()) == null) { return nodeLockInfo; } @@ -1202,7 +1225,29 @@ public abstract class WebDAVMethod } return result; } - + + /** + * Determines status code for AccessDeniedException based on client's HTTP headers. + * + * @return Returns status code + */ + protected int getStatusForAccessDeniedException() + { + if (m_request != null && m_request.getHeader(WebDAV.HEADER_USER_AGENT) != null) + { + String userAgent = m_request.getHeader(WebDAV.HEADER_USER_AGENT).toLowerCase(); + + for (Entry entry : accessDeniedStatusCodes.entrySet()) + { + if (Pattern.compile(entry.getKey()).matcher(userAgent).find()) + { + return entry.getValue(); + } + } + } + return HttpServletResponse.SC_UNAUTHORIZED; + } + /** * Class used for storing conditions which comes with "If" header of the request * diff --git a/source/java/org/alfresco/repo/webdav/WebDAVServlet.java b/source/java/org/alfresco/repo/webdav/WebDAVServlet.java index c925a7fce6..f40bba7dac 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVServlet.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVServlet.java @@ -20,17 +20,16 @@ package org.alfresco.repo.webdav; import java.io.IOException; import java.util.Hashtable; -import java.util.List; +import java.util.Properties; import javax.servlet.ServletConfig; import javax.servlet.ServletException; +import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.transaction.UserTransaction; -import org.alfresco.filesys.ServerConfigurationBean; -import org.alfresco.jlan.server.config.ServerConfigurationAccessor; import org.alfresco.repo.security.authentication.AuthenticationContext; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.ServiceRegistry; @@ -41,6 +40,7 @@ import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.web.context.WebApplicationContext; @@ -54,7 +54,6 @@ import org.springframework.web.context.support.WebApplicationContextUtils; */ public class WebDAVServlet extends HttpServlet { - private static final long serialVersionUID = 6900069445027527165L; // Logging @@ -65,8 +64,7 @@ public class WebDAVServlet extends HttpServlet private static final String INTERNAL_SERVER_ERROR = "Internal Server Error: "; // Init parameter names - public static final String KEY_STORE = "store"; - public static final String KEY_ROOT_PATH = "rootPath"; + private static final String BEAN_INIT_PARAMS = "webdav.initParams"; // Service registry, used by methods to find services to process requests private ServiceRegistry m_serviceRegistry; @@ -74,20 +72,14 @@ public class WebDAVServlet extends HttpServlet // Transaction service, each request is wrapped in a transaction private TransactionService m_transactionService; - // Tenant service - private TenantService m_tenantService; - // WebDAV method handlers - protected Hashtable m_davMethods; + protected Hashtable> m_davMethods; // Root node - private NodeRef m_rootNodeRef; + private MTNodesCache m_rootNodes; // WebDAV helper class private WebDAVHelper m_davHelper; - - // Root path - private String m_rootPath; /** * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, @@ -97,7 +89,7 @@ public class WebDAVServlet extends HttpServlet IOException { long startTime = 0; - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { startTime = System.currentTimeMillis(); } @@ -180,11 +172,9 @@ public class WebDAVServlet extends HttpServlet } finally { - if (logger.isDebugEnabled()) + if (logger.isInfoEnabled()) { - long endTime = System.currentTimeMillis(); - long duration = endTime - startTime; - logger.debug(request.getMethod() + " took " + duration + "ms to execute"); + logger.info(request.getMethod() + " took " + (System.currentTimeMillis()-startTime) + "ms to execute ["+request.getRequestURI()+"]"); } } } @@ -206,7 +196,7 @@ public class WebDAVServlet extends HttpServlet logger.debug("WebDAV request " + strHttpMethod + " on path " + request.getRequestURI()); - Class methodClass = m_davMethods.get(strHttpMethod); + Class methodClass = m_davMethods.get(strHttpMethod); WebDAVMethod method = null; if ( methodClass != null) @@ -215,19 +205,8 @@ public class WebDAVServlet extends HttpServlet { // Create the handler method - method = (WebDAVMethod) methodClass.newInstance(); - NodeRef rootNodeRef = m_rootNodeRef; - if (m_tenantService.isEnabled()) - { - WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); - NodeService nodeService = (NodeService) context.getBean("NodeService"); - SearchService searchService = (SearchService) context.getBean("SearchService"); - NamespaceService namespaceService = (NamespaceService) context.getBean("NamespaceService"); - - // note: rootNodeRef is required (for storeRef part) - rootNodeRef = m_tenantService.getRootNode(nodeService, searchService, namespaceService, m_rootPath, rootNodeRef); - } - + method = methodClass.newInstance(); + NodeRef rootNodeRef = m_rootNodes.getNodeForCurrentTenant(); method.setDetails(request, response, m_davHelper, rootNodeRef); } catch (Exception ex) @@ -262,11 +241,28 @@ public class WebDAVServlet extends HttpServlet { return; } + + // Get global configuration properties + WebApplicationContext wc = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + WebDAVInitParameters initParams = (WebDAVInitParameters) wc.getBean(BEAN_INIT_PARAMS); + // Render this servlet permanently unavailable if its enablement property is not set + if (!initParams.getEnabled()) + { + throw new UnavailableException("WebDAV not enabled."); + } + + // Get root paths + + String storeValue = initParams.getStoreName(); + String rootPath = initParams.getRootPath(); + + // Get beans + m_serviceRegistry = (ServiceRegistry)context.getBean(ServiceRegistry.SERVICE_REGISTRY); m_transactionService = m_serviceRegistry.getTransactionService(); - m_tenantService = (TenantService) context.getBean("tenantService"); + TenantService tenantService = (TenantService) context.getBean("tenantService"); AuthenticationService authService = (AuthenticationService) context.getBean("authenticationService"); NodeService nodeService = (NodeService) context.getBean("NodeService"); SearchService searchService = (SearchService) context.getBean("SearchService"); @@ -275,27 +271,13 @@ public class WebDAVServlet extends HttpServlet // Create the WebDAV helper m_davHelper = new WebDAVHelper(m_serviceRegistry, authService); - - String storeValue = context.getServletContext().getInitParameter(org.alfresco.repo.webdav.WebDAVServlet.KEY_STORE); - if (storeValue == null) - { - throw new ServletException("Device missing init value: " + KEY_STORE); - } - - m_rootPath = context.getServletContext().getInitParameter(org.alfresco.repo.webdav.WebDAVServlet.KEY_ROOT_PATH); - if (m_rootPath == null) - { - throw new ServletException("Device missing init value: " + KEY_ROOT_PATH); - } - // Initialize the root node - m_rootNodeRef = getRootNode(storeValue, m_rootPath, context, nodeService, searchService, - namespaceService, m_transactionService); - + + initializeRootNode(storeValue, rootPath, context, nodeService, searchService, namespaceService, tenantService, m_transactionService); // Create the WebDAV methods table - m_davMethods = new Hashtable(); + m_davMethods = new Hashtable>(); m_davMethods.put(WebDAV.METHOD_PROPFIND, PropFindMethod.class); m_davMethods.put(WebDAV.METHOD_PROPPATCH, PropPatchMethod.class); @@ -312,90 +294,41 @@ public class WebDAVServlet extends HttpServlet m_davMethods.put(WebDAV.METHOD_UNLOCK, UnlockMethod.class); } - /** - * @param config - * @param context - * @param nodeService - * @param searchService - * @param namespaceService - */ - public static NodeRef getRootNode(String storeValue, String m_rootPath, - WebApplicationContext context, NodeService nodeService, - SearchService searchService, NamespaceService namespaceService, - TransactionService m_transactionService) - throws ServletException { - - NodeRef m_rootNodeRef = null; - - // Initialize the root node - - ServerConfigurationAccessor fileSrvConfig = (ServerConfigurationAccessor) context.getBean(ServerConfigurationBean.SERVER_CONFIGURATION); - if ( fileSrvConfig == null) - throw new ServletException("File server configuration not available"); + + /** + * @param storeValue + * @param rootPath + * @param context + * @param nodeService + * @param searchService + * @param namespaceService + * @param tenantService + * @param m_transactionService + */ + private void initializeRootNode(String storeValue, String rootPath, WebApplicationContext context, NodeService nodeService, SearchService searchService, + NamespaceService namespaceService, TenantService tenantService, TransactionService m_transactionService) + { // Use the system user as the authenticated context for the filesystem initialization AuthenticationContext authComponent = (AuthenticationContext) context.getBean("authenticationContext"); authComponent.setSystemUserAsCurrentUser(); - - + // Wrap the initialization in a transaction - + UserTransaction tx = m_transactionService.getUserTransaction(true); - + try { // Start the transaction - - if ( tx != null) + + if (tx != null) tx.begin(); - - // Get the store - if (storeValue == null) - { - throw new ServletException("Device missing init value: " + KEY_STORE); - } - StoreRef storeRef = new StoreRef(storeValue); - - // Connect to the repo and ensure that the store exists - - if (! nodeService.exists(storeRef)) - { - throw new ServletException("Store not created prior to application startup: " + storeRef); - } - NodeRef storeRootNodeRef = nodeService.getRootNode(storeRef); - - // Check the root path - if (m_rootPath == null) - { - throw new ServletException("Device missing init value: " + KEY_ROOT_PATH); - } - - // Find the root node for this device - List nodeRefs = searchService.selectNodes(storeRootNodeRef, m_rootPath, null, namespaceService, false); + m_rootNodes = new MTNodesCache(new StoreRef(storeValue), rootPath, nodeService, searchService, namespaceService, tenantService); - if (nodeRefs.size() > 1) - { - throw new ServletException("Multiple possible roots for device: \n" + - " root path: " + m_rootPath + "\n" + - " results: " + nodeRefs); - } - else if (nodeRefs.size() == 0) - { - // nothing found - throw new ServletException("No root found for device: \n" + - " root path: " + m_rootPath); - } - else - { - // we found a node - m_rootNodeRef = nodeRefs.get(0); - - } - // Commit the transaction - + tx.commit(); } catch (Exception ex) @@ -405,12 +338,61 @@ public class WebDAVServlet extends HttpServlet finally { // Clear the current system user - + authComponent.clearCurrentSecurityContext(); } - - return m_rootNodeRef; - - } + } + /** + * Bean to hold injected initialization parameters. + * + * @author Derek Hulley + * @since V3.5 Team + */ + public static class WebDAVInitParameters + { + private boolean enabled = false; + private String storeName; + private String rootPath; + public boolean getEnabled() + { + return enabled; + } + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + /** + * @return Returns the name of the store + * @throws ServletException if the store name was not set + */ + public String getStoreName() throws ServletException + { + if (!PropertyCheck.isValidPropertyString(storeName)) + { + throw new ServletException("WebDAV missing 'storeName' value."); + } + return storeName; + } + public void setStoreName(String storeName) + { + this.storeName = storeName; + } + /** + * @return Returns the WebDAV root path within the store + * @throws ServletException if the root path was not set + */ + public String getRootPath() throws ServletException + { + if (!PropertyCheck.isValidPropertyString(rootPath)) + { + throw new ServletException("WebDAV missing 'rootPath' value."); + } + return rootPath; + } + public void setRootPath(String rootPath) + { + this.rootPath = rootPath; + } + } }